Skip to content

Refactor fetchChildIssues to use IssueTracker provider abstraction instead of hardcoded per-provider utilities#633

Draft
acreeger wants to merge 2 commits intoepic/610from
refactor/issue-631__fetch-child-issues-provider
Draft

Refactor fetchChildIssues to use IssueTracker provider abstraction instead of hardcoded per-provider utilities#633
acreeger wants to merge 2 commits intoepic/610from
refactor/issue-631__fetch-child-issues-provider

Conversation

@acreeger
Copy link
Collaborator

Fixes #631

Refactor fetchChildIssues to use IssueTracker provider abstraction instead of hardcoded per-provider utilities

Problem

fetchChildIssues() in src/utils/list-children.ts bypasses the provider abstraction pattern by using hardcoded per-provider utility functions with a switch statement:

if (providerName === 'github') {
  return getSubIssues(issueNum, repo)
} else if (providerName === 'linear') {
  return getLinearChildIssues(parentIssueNumber, ...)
} else {
  logger.warn(`Unsupported issue tracker provider: ${providerName}`)
  return []
}

This means:

  • Jira is not supported for child issue fetching despite having a full provider implementation
  • Every new provider requires changes in multiple places instead of just the provider class
  • The MCP layer (JiraIssueManagementProvider) already has a working getChildIssues() method, showing the capability exists but isn't exposed through the right abstraction

Impact

  • il list --children doesn't work for Jira projects
  • Epic detection in il start can't find child issues for Jira epics
  • fetchChildIssueDetails() (used for swarm mode epic metadata) also fails for Jira since it calls fetchChildIssues() internally

Proposed Solution

Refactor fetchChildIssues() to delegate to the IssueTracker interface (or the IssueManagementProvider abstraction) instead of calling provider-specific utility functions directly. This would:

  1. Add Jira support automatically
  2. Ensure any future providers get child issue support for free
  3. Follow the existing provider pattern used throughout the codebase

Options

  1. Add getChildIssues() to the IssueTracker interface — Each provider implements it, fetchChildIssues just calls issueTracker.getChildIssues(parentId)
  2. Reuse the MCP IssueManagementProvider.getChildIssues() — Already implemented for all three providers, but couples CLI utils to the MCP layer

Option 1 is cleaner architecturally.

Files to Change

  • src/lib/IssueTracker.ts — Add getChildIssues() to interface
  • src/lib/GitHubService.ts — Implement using existing getSubIssues()
  • src/lib/providers/jira/JiraIssueTracker.ts — Implement using JQL query
  • Linear provider — Implement using existing getLinearChildIssues()
  • src/utils/list-children.ts — Refactor fetchChildIssues() to use the interface

This PR was created automatically by iloom.

…nt epic state

- MetadataManager.updateMetadata: re-throw errors after logging per project guidelines
- start.ts: replace dynamic imports of prompt.js with static import
- start.ts: revert parsed.type to 'issue' when epic child data fetch fails
- start.ts: replace process.exit(1) with throw for testability
- ignite.ts: remove unnecessary `as string` cast on issueType
- Update tests to match new behavior
@acreeger
Copy link
Collaborator Author

acreeger commented Feb 18, 2026

Complexity Assessment

Classification: SIMPLE

Metrics:

  • Estimated files affected: 8-10
  • Estimated lines of code: 150-250
  • Breaking changes: No
  • Database migrations: No
  • Cross-cutting changes: Yes (signature propagation)
  • File architecture quality: Good
  • Architectural signals triggered: Signature propagation requires coordinated updates across multiple callers
  • Overall risk level: Low

Reasoning: While the refactoring involves signature propagation across multiple callers (src/cli.ts, src/commands/start.ts, src/commands/ignite.ts, src/utils/list-children.ts) and test rewrites, this is a mechanical refactoring with an established provider pattern precedent. The architectural decisions (return type mapping, IssueTracker parameter passing) follow existing patterns in the codebase and involve no breaking changes or data migrations.

@acreeger
Copy link
Collaborator Author

acreeger commented Feb 18, 2026

Combined Analysis & Plan - Issue #631

Executive Summary

fetchChildIssues() in src/utils/list-children.ts bypasses the IssueTracker provider abstraction by hardcoding GitHub and Linear utility calls in a switch statement, leaving Jira unsupported. The fix adds getChildIssues() to the IssueTracker interface (matching the shape already used by the MCP layer), implements it in all three providers by delegating to existing utility functions, and refactors fetchChildIssues() to simply call issueTracker.getChildIssues().

Implementation Overview

High-Level Execution Phases

  1. Add getChildIssues() to IssueTracker interface: Define the method with RawChildIssue[] return type
  2. Implement in all 3 providers: GitHubService, LinearService, JiraIssueTracker -- each delegates to existing utility functions
  3. Refactor fetchChildIssues() and callers: Change signature from (parentIssueNumber, settings, repo?) to (parentIssueNumber, issueTracker, repo?), propagate to assembleChildrenData, fetchChildIssueDetails, and all callers
  4. Update tests: Rewrite list-children.test.ts fetchChildIssues tests to mock the IssueTracker instead of utility functions; add getChildIssues() tests to provider test files; update start.test.ts mocks
  5. Build verification: Run pnpm build to ensure compilation

Quick Stats

  • 10 files to modify
  • 0 new files to create
  • 0 files to delete
  • Dependencies: None

Complete Analysis and Implementation Details (click to expand)

Research Findings

Problem Space

  • Problem: fetchChildIssues() uses IssueTrackerFactory.getProviderName() + switch statement instead of polymorphic dispatch, meaning Jira has no child issue support despite the MCP layer already implementing it
  • Architectural context: The codebase uses IssueTracker interface for provider abstraction; adding getChildIssues() follows the existing pattern
  • Edge cases: GitHub issue number validation (parseInt) currently lives in fetchChildIssues() -- this moves into GitHubService.getChildIssues() where it belongs

Codebase Research

  • Entry point: src/utils/list-children.ts:88-128 - fetchChildIssues() function with switch on providerName
  • Return type alignment: RawChildIssue (line 70-75) matches ChildIssueResult in src/mcp/types.ts:120-125 -- both are { id: string; title: string; url: string; state: string }
  • Utility functions: getSubIssues() at src/utils/github.ts:502 and getLinearChildIssues() at src/utils/linear.ts:501 both return Array<{ id: string; title: string; url: string; state: string }>
  • Jira implementation: Already exists in MCP layer at src/mcp/JiraIssueManagementProvider.ts:391-403 using searchIssues("parent = KEY")
  • JiraApiClient.searchIssues: src/lib/providers/jira/JiraApiClient.ts:362 - available on the client

Affected Files

  1. src/lib/IssueTracker.ts:16-51 - Interface definition, add getChildIssues() method
  2. src/lib/GitHubService.ts - Implement getChildIssues(), delegating to getSubIssues()
  3. src/lib/LinearService.ts - Implement getChildIssues(), delegating to getLinearChildIssues()
  4. src/lib/providers/jira/JiraIssueTracker.ts - Implement getChildIssues(), using JQL parent = KEY
  5. src/utils/list-children.ts:88-128 - Refactor fetchChildIssues() to use IssueTracker; also update assembleChildrenData() and fetchChildIssueDetails()
  6. src/commands/start.ts:234-236 - Update fetchChildIssues() call to pass issueTracker instead of settings
  7. src/commands/ignite.ts:769-771 - Update fetchChildIssueDetails() call to remove settings param (signature changes from (parentIssueNumber, issueTracker, settings, repo?) to (parentIssueNumber, issueTracker, repo?))
  8. src/cli.ts:1145-1327 - Update assembleChildrenData() calls (5 sites) to pass issueTracker
  9. src/utils/list-children.test.ts - Rewrite fetchChildIssues and assembleChildrenData tests to use mock IssueTracker
  10. src/commands/start.test.ts - Update mocks and assertions for new fetchChildIssues signature

Signature Propagation Chain

fetchChildIssues(parentIssueNumber, settings, repo?)
  -> fetchChildIssues(parentIssueNumber, issueTracker, repo?)

assembleChildrenData(parentLoom, metadataManager, settings, repo?)
  -> assembleChildrenData(parentLoom, metadataManager, issueTracker, repo?)

fetchChildIssueDetails(parentIssueNumber, issueTracker, settings, repo?)
  -> fetchChildIssueDetails(parentIssueNumber, issueTracker, repo?)
  [settings param removed since issueTracker already passed, and providerName available from issueTracker.providerName]

Callers:

  • start.ts:236 calls fetchChildIssues -- already has settings, needs to create issueTracker (already does so at line 274)
  • start.ts:276 calls fetchChildIssueDetails -- already passes issueTracker
  • cli.ts calls assembleChildrenData at 5 sites -- needs to create issueTracker from settings
  • ignite.ts:769-771 calls fetchChildIssueDetails(parentIssueNumber, issueTracker, settings) -- remove settings param, becomes fetchChildIssueDetails(parentIssueNumber, issueTracker)

Implementation Plan

Automated Test Cases to Create

Test File: src/utils/list-children.test.ts (MODIFY)

The fetchChildIssues describe block needs complete rewrite to mock IssueTracker.getChildIssues() instead of getSubIssues/getLinearChildIssues utility functions. The assembleChildrenData tests also need updating since they currently mock getSubIssues directly.

Click to expand test structure (15 lines)
describe('fetchChildIssues', () => {
  // Create mock IssueTracker with getChildIssues method
  // Test: should delegate to issueTracker.getChildIssues()
  // Test: should return empty array when API fails (fault tolerance via Promise.allSettled)
  // Test: should pass repo parameter to issueTracker.getChildIssues()
  // Remove: provider-specific tests (GitHub parseInt, Linear apiToken, unsupported provider)
  //   -- those concerns now live in the provider implementations
})

describe('assembleChildrenData', () => {
  // Update all tests: replace `settings` param with mock `issueTracker`
  // Remove: mocking of getSubIssues/IssueTrackerFactory.getProviderName
  // Add: mock issueTracker.getChildIssues() instead
})

Provider test files: src/lib/GitHubService.test.ts, src/lib/LinearService.test.ts, src/lib/providers/jira/JiraIssueTracker.ts (MODIFY existing, ADD describe block)

Provider-specific getChildIssues() tests verify the logic that previously lived in fetchChildIssues() now lives in the correct provider. These tests are critical for Jira coverage since Jira support is the primary motivation for this refactor.

Click to expand provider test structure (30 lines)
// In src/lib/GitHubService.test.ts - add describe block:
describe('getChildIssues', () => {
  // Test: should call getSubIssues with parsed integer and repo
  // Test: should return empty array for non-numeric identifier (e.g., "not-a-number")
  // Test: should return results from getSubIssues mapped to {id, title, url, state}
})

// In src/lib/LinearService.test.ts - add describe block:
describe('getChildIssues', () => {
  // Test: should call getLinearChildIssues with identifier
  // Test: should pass apiToken from config when available
  // Test: should pass undefined options when no apiToken configured
})

// In src/lib/providers/jira/JiraIssueTracker.ts - add tests
// (No existing test file; add describe block to a new JiraIssueTracker.test.ts
//  OR add to existing test infrastructure if there's a shared pattern)
describe('JiraIssueTracker', () => {
  describe('getChildIssues', () => {
    // Test: should query JQL "parent = KEY" via client.searchIssues()
    // Test: should normalize identifier to uppercase before querying
    // Test: should map Jira issues to {id: key, title: summary, url, state: status.name.toLowerCase()}
    // Test: should return empty array when no children found
    // Test: should construct URL using config.host + "/browse/" + key
  })
})

Files to Modify

1. src/lib/IssueTracker.ts:16-51

Change: Add getChildIssues() method to the IssueTracker interface
Cross-cutting impact: All IssueTracker implementations must add this method

// Add after getIssueUrl (line 34):
getChildIssues(parentIdentifier: string, repo?: string): Promise<Array<{ id: string; title: string; url: string; state: string }>>

2. src/lib/GitHubService.ts

Change: Implement getChildIssues() method, delegating to existing getSubIssues() from src/utils/github.ts

// Add import: getSubIssues from '../utils/github.js'  (already imported indirectly, may need explicit import)
// Add method:
public async getChildIssues(parentIdentifier: string, repo?: string) {
  const issueNum = parseInt(parentIdentifier, 10)
  if (isNaN(issueNum)) {
    getLogger().warn(`Invalid GitHub issue number: ${parentIdentifier}`)
    return []
  }
  return getSubIssues(issueNum, repo)
}

3. src/lib/LinearService.ts

Change: Implement getChildIssues() method, delegating to existing getLinearChildIssues() from src/utils/linear.ts

// Add import: getLinearChildIssues from '../utils/linear.js'
// Add method:
public async getChildIssues(parentIdentifier: string, _repo?: string) {
  return getLinearChildIssues(parentIdentifier, this.config.apiToken ? { apiToken: this.config.apiToken } : undefined)
}

4. src/lib/providers/jira/JiraIssueTracker.ts

Change: Implement getChildIssues() method using JQL query (same pattern as src/mcp/JiraIssueManagementProvider.ts:391-403)

// Add method:
async getChildIssues(parentIdentifier: string, _repo?: string) {
  const parentKey = this.normalizeIdentifier(parentIdentifier)
  const issues = await this.client.searchIssues(`parent = ${parentKey}`)
  return issues.map(issue => ({
    id: issue.key,
    title: issue.fields.summary,
    url: `${this.config.host}/browse/${issue.key}`,
    state: issue.fields.status.name.toLowerCase(),
  }))
}

5. src/utils/list-children.ts

Change: Major refactor of fetchChildIssues(), assembleChildrenData(), and fetchChildIssueDetails() signatures

Key changes:

  • fetchChildIssues(parentIssueNumber, settings, repo?) becomes fetchChildIssues(parentIssueNumber, issueTracker, repo?)
  • Remove the switch on providerName; replace with issueTracker.getChildIssues(parentIssueNumber, repo)
  • Remove imports of getSubIssues, getLinearChildIssues, and IssueTrackerFactory
  • assembleChildrenData(parentLoom, metadataManager, settings, repo?) becomes assembleChildrenData(parentLoom, metadataManager, issueTracker, repo?)
  • fetchChildIssueDetails(parentIssueNumber, issueTracker, settings, repo?) becomes fetchChildIssueDetails(parentIssueNumber, issueTracker, repo?) -- remove settings param, use issueTracker.providerName for formatIssueNumber()
Click to expand simplified fetchChildIssues (12 lines)
export async function fetchChildIssues(
  parentIssueNumber: string,
  issueTracker: IssueTracker,
  repo?: string,
): Promise<RawChildIssue[]> {
  logger.debug('Fetching child issues', { parentIssueNumber, provider: issueTracker.providerName, repo })
  const results = await Promise.allSettled([
    issueTracker.getChildIssues(parentIssueNumber, repo),
  ])
  const result = results[0]
  if (result.status === 'fulfilled') return result.value
  logger.warn(`Failed to fetch child issues for ${parentIssueNumber}`, { error: result.reason })
  return []
}

6. src/commands/start.ts:228-276

Change: Update calls to fetchChildIssues and fetchChildIssueDetails

  • At line 234-236: Create issueTracker earlier (before fetchChildIssues call) and pass it instead of settings
  • At line 274-276: issueTracker already created at line 274; fetchChildIssueDetails call removes settings param
  • Remove IssueTrackerFactory import if no longer needed (check if it's used elsewhere in this file)

7. src/commands/ignite.ts:769-771

Change: Update fetchChildIssueDetails() call to remove settings param

Current call at line 769-771:

const childIssueDetails = await fetchChildIssueDetails(
    parentIssueNumber, issueTracker, settings
)

Becomes:

const childIssueDetails = await fetchChildIssueDetails(
    parentIssueNumber, issueTracker
)

The issueTracker is already created at line 766 via IssueTrackerFactory.create(settings), so no other changes needed in this file.

8. src/cli.ts:1144-1327

Change: Update all 5 assembleChildrenData() call sites to pass issueTracker instead of settings

JSON mode (lines 1145-1197):

  • After loading settings at line 1148, add const issueTracker = IssueTrackerFactory.create(settings)
  • Update calls at lines 1161 and 1184: replace settings with issueTracker

Text mode (lines 1222-1327):

  • After loading settings at line 1226, add const issueTracker = IssueTrackerFactory.create(textSettings) (guarded by null check)
  • Update calls at lines 1255, 1288, 1327: replace textSettings with the issueTracker

9. src/utils/list-children.test.ts (MODIFY)

Change: Rewrite fetchChildIssues tests and assembleChildrenData tests

Key test changes:

  • Remove vi.mock('./github.js') and vi.mock('./linear.js') -- no longer needed
  • Remove vi.mock('../lib/IssueTrackerFactory.js') -- no longer needed
  • Create mock IssueTracker with getChildIssues method for fetchChildIssues tests
  • Update assembleChildrenData tests to pass mock issueTracker instead of settings
  • Update fetchChildIssueDetails tests to remove settings param
  • Remove provider-specific tests from fetchChildIssues (those now belong in provider unit tests)

10. src/commands/start.test.ts (MODIFY)

Change: Update mock for fetchChildIssues calls

  • The mock at line 87 fetchChildIssues: vi.fn().mockResolvedValue([]) stays the same (module is already mocked)
  • Test assertions that check fetchChildIssues call args may need updating if signature changed
  • fetchChildIssueDetails mock at line 88 stays the same
  • May need to update assertions at lines like 2059 that check call args

Provider Test Files (MODIFY)

src/lib/GitHubService.test.ts: Add describe('getChildIssues', ...) block testing parseInt validation, delegation to getSubIssues(), and repo parameter forwarding. Mock getSubIssues from ../utils/github.js.

src/lib/LinearService.test.ts: Add describe('getChildIssues', ...) block testing delegation to getLinearChildIssues(), apiToken passthrough from config, and handling when no apiToken configured. Mock getLinearChildIssues from ../utils/linear.js.

src/lib/providers/jira/JiraIssueTracker.test.ts (NEW file): Create test file for JiraIssueTracker.getChildIssues() testing JQL query construction (parent = KEY), identifier normalization to uppercase, result mapping from Jira fields to {id, title, url, state}, URL construction from config.host, and empty results. Mock JiraApiClient.searchIssues(). This is the most important new test coverage since Jira support is the primary motivation for this issue.

Detailed Execution Order

NOTE: These steps are executed in a SINGLE implementation run.

  1. Add interface method

    • Files: /src/lib/IssueTracker.ts
    • Add getChildIssues() to IssueTracker interface at line 34 -> Verify: TypeScript reports missing implementations in providers
  2. Implement in GitHubService

    • Files: /src/lib/GitHubService.ts
    • Add getChildIssues() method using getSubIssues() -> Verify: method delegates correctly
  3. Implement in LinearService

    • Files: /src/lib/LinearService.ts
    • Add import for getLinearChildIssues, add getChildIssues() method -> Verify: passes apiToken from config
  4. Implement in JiraIssueTracker

    • Files: /src/lib/providers/jira/JiraIssueTracker.ts
    • Add getChildIssues() method using client.searchIssues() -> Verify: matches MCP implementation pattern
  5. Refactor list-children.ts

    • Files: /src/utils/list-children.ts
    • Change signatures of fetchChildIssues, assembleChildrenData, fetchChildIssueDetails
    • Remove imports of getSubIssues, getLinearChildIssues, IssueTrackerFactory
    • Keep IloomSettings import only if still needed -> Verify: clean compilation
  6. Update callers

    • Files: /src/commands/start.ts, /src/commands/ignite.ts, /src/cli.ts
    • Update fetchChildIssues, fetchChildIssueDetails, and assembleChildrenData calls to use new signatures -> Verify: pnpm build succeeds
  7. Update tests

    • Files: /src/utils/list-children.test.ts, /src/commands/start.test.ts
    • Rewrite mocks and assertions for new signatures -> Verify: pnpm test passes
  8. Add provider tests

    • Files: /src/lib/GitHubService.test.ts, /src/lib/LinearService.test.ts, /src/lib/providers/jira/JiraIssueTracker.test.ts (NEW)
    • Add getChildIssues() test coverage for all three providers -> Verify: pnpm test passes
  9. Build verification

    • Run pnpm build to confirm successful compilation

Dependencies and Configuration

None

@acreeger
Copy link
Collaborator Author

acreeger commented Feb 18, 2026

Implementation Complete

Summary

Refactored fetchChildIssues() to delegate to the IssueTracker interface instead of using hardcoded per-provider utility functions. This adds Jira child issue support and ensures future providers get child issue fetching for free. Also added JQL injection protection and simplified error handling.

Changes Made

  • src/lib/IssueTracker.ts: Added getChildIssues(parentIdentifier, repo?) to interface
  • src/lib/GitHubService.ts: Implemented getChildIssues() delegating to getSubIssues()
  • src/lib/LinearService.ts: Implemented getChildIssues() delegating to getLinearChildIssues()
  • src/lib/providers/jira/JiraIssueTracker.ts: Implemented getChildIssues() with JQL query and key format validation
  • src/utils/list-children.ts: Refactored signatures to accept IssueTracker instead of IloomSettings
  • src/commands/start.ts: Updated callers to pass IssueTracker instance
  • src/commands/ignite.ts: Removed settings param from fetchChildIssueDetails() call
  • src/cli.ts: Creates IssueTracker instances for assembleChildrenData() calls
  • src/utils/list-children.test.ts: Rewrote tests to use mock IssueTracker
  • src/commands/start.test.ts: Updated assertion for new signature
  • src/lib/providers/jira/JiraIssueTracker.test.ts: New test file (7 tests)
  • src/lib/GitHubService.test.ts: Added getChildIssues tests (3 tests)
  • src/lib/LinearService.test.ts: Added getChildIssues tests (3 tests)

Validation Results

  • Tests: 3994 passed / 4017 total (23 skipped)
  • Typecheck: Passed
  • Lint: Passed
  • Build: Passed

Code Review Fixes Applied

JQL Injection Protection (Medium Severity)

Added Jira key format validation (/^[A-Z][A-Z0-9]+-\d+$/) before interpolating into JQL query in JiraIssueTracker.getChildIssues().

Promise.allSettled Simplification (Low Severity)

Replaced Promise.allSettled wrapping a single promise with a simple try/catch in fetchChildIssues().

Additional Tests

  • Added test for invalid Jira key format rejection
  • Added test for API error propagation

…er abstraction

Replace hardcoded per-provider switch statement in fetchChildIssues() with
delegation to IssueTracker.getChildIssues() interface method. Each provider
(GitHub, Linear, Jira) now implements getChildIssues(), enabling Jira child
issue support and ensuring future providers get child issue fetching for free.

- Add getChildIssues() to IssueTracker interface
- Implement in GitHubService, LinearService, JiraIssueTracker
- Add JQL key format validation to prevent injection
- Simplify fetchChildIssues error handling (try/catch vs Promise.allSettled)
- Update all callers to pass IssueTracker instead of settings
- Rewrite list-children tests to use mock IssueTracker
- Add provider-specific getChildIssues tests (13 new tests)

Fixes #631
@acreeger acreeger force-pushed the refactor/issue-631__fetch-child-issues-provider branch from c9a7c98 to fb1c564 Compare February 18, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant

Comments