Skip to content

test(dev-workflow): expand test coverage to 100% - #212

Merged
NTCoding merged 6 commits into
mainfrom
issue-206-expand-dev-workflow-test-cover
Jan 28, 2026
Merged

NTCoding merged 6 commits into
mainfrom
issue-206-expand-dev-workflow-test-cover

Conversation

@NTCoding

@NTCoding NTCoding commented Jan 27, 2026 •

Copy link
Copy Markdown
Owner

Problem

The tools/dev-workflow package currently only has test coverage configured for a subset of files:

include: [
  'dev-workflow-hooks/handlers/**/*.ts',
  'features/complete-task/domain/task-check-marker.ts',
],

This means most source files have no coverage enforcement.

Goal

Expand coverage to include all source files in the dev-workflow package and ensure 100% coverage across all metrics:

  • lines: 100%
  • statements: 100%
  • functions: 100%
  • branches: 100% (currently set to 80%)

Files affected

  • tools/dev-workflow/vitest.config.mts - update coverage include patterns
  • All source files in tools/dev-workflow/ that need tests

Summary by CodeRabbit

  • Tests

    • Vastly expanded test coverage across features and workflows for stronger reliability.
  • Bug Fixes

    • Improved CLI error handling and exit codes for malformed or failing workflows.
    • Stop flow now prevents passthrough output when stopping and yields clearer stop reasons.
    • More robust parsing and validation to handle malformed or unexpected inputs gracefully.
  • Chores

    • Branch coverage threshold raised to 100% and schema validation tightened for stricter quality.

✏️ Tip: You can customize this high-level summary in your review settings.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Flaky Tests

Several tests rely on real timing (setTimeout-based waiting) after dynamic imports to let async side effects complete. This can be flaky across CI environments and may hide race conditions. Prefer deterministic synchronization (e.g., awaiting a returned promise, using fake timers, or exposing a callable main) rather than sleeping.

describe('dev-workflow-hooks entry point', () => {
  const originalEnv = process.env
  const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined)
  const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  const mockExit = vi.spyOn(process, 'exit').mockImplementation(vi.fn<typeof process.exit>())

  beforeEach(() => {
    vi.clearAllMocks()
    vi.resetModules()
    process.env = { ...originalEnv }
    delete process.env.CLAUDE_SDK_AGENT
  })

  afterEach(() => {
    process.env = originalEnv
  })

  it('outputs empty object when running as SDK agent (env=true)', async () => {
    process.env.CLAUDE_SDK_AGENT = 'true'
    mockCreateInterface.mockReturnValue(createMockReadline([]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockConsoleLog).toHaveBeenCalledWith('{}')
  })

  it('outputs empty object when running as SDK agent (env=1)', async () => {
    process.env.CLAUDE_SDK_AGENT = '1'
    mockCreateInterface.mockReturnValue(createMockReadline([]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockConsoleLog).toHaveBeenCalledWith('{}')
  })

  it('exits with code 2 for malformed JSON input', async () => {
    mockCreateInterface.mockReturnValue(createMockReadline(['not valid json']))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockConsoleError).toHaveBeenCalledWith('Invalid hook input: malformed JSON')
    expect(mockExit).toHaveBeenCalledWith(2)
  })

  it('exits with code 2 for invalid schema', async () => {
    const invalidInput = JSON.stringify({ invalid: 'schema' })
    mockCreateInterface.mockReturnValue(createMockReadline([invalidInput]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid hook input:'))
    expect(mockExit).toHaveBeenCalledWith(2)
  })

  it('routes PreToolUse to handler and outputs result', async () => {
    const input = {
      session_id: 'test',
      transcript_path: '/test',
      cwd: '/test',
      permission_mode: 'default',
      hook_event_name: 'PreToolUse',
      tool_name: 'Bash',
      tool_input: { command: 'ls' },
    }
    const handlerOutput = {
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'allow',
        permissionDecisionReason: 'Safe',
      },
    }
    mockHandlePreToolUse.mockReturnValue(handlerOutput)
    mockCreateInterface.mockReturnValue(createMockReadline([JSON.stringify(input)]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockHandlePreToolUse).toHaveBeenCalledWith(input)
    expect(mockConsoleLog).toHaveBeenCalledWith(JSON.stringify(handlerOutput))
  })

  it('routes PostToolUse to handler', async () => {
    const input = {
      session_id: 'test',
      transcript_path: '/test',
      cwd: '/test',
      permission_mode: 'default',
      hook_event_name: 'PostToolUse',
      tool_name: 'Bash',
      tool_response: { stdout: 'output' },
    }
    const handlerOutput = { hookSpecificOutput: { hookEventName: 'PostToolUse' } }
    mockHandlePostToolUse.mockReturnValue(handlerOutput)
    mockCreateInterface.mockReturnValue(createMockReadline([JSON.stringify(input)]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockHandlePostToolUse).toHaveBeenCalledWith(input)
  })

  it('routes Stop to handler', async () => {
    const input = {
      session_id: 'test',
      transcript_path: '/test',
      cwd: '/test',
      permission_mode: 'default',
      hook_event_name: 'Stop',
      stop_hook_active: false,
    }
    const handlerOutput = {}
    mockHandleStop.mockReturnValue(handlerOutput)
    mockCreateInterface.mockReturnValue(createMockReadline([JSON.stringify(input)]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockHandleStop).toHaveBeenCalledWith(input)
  })

  it('handles multiline JSON input', async () => {
    const input = {
      session_id: 'test',
      transcript_path: '/test',
      cwd: '/test',
      permission_mode: 'default',
      hook_event_name: 'PreToolUse',
      tool_name: 'Bash',
      tool_input: { command: 'ls' },
    }
    const lines = JSON.stringify(input, null, 2).split('\n')
    mockHandlePreToolUse.mockReturnValue({
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'allow',
        permissionDecisionReason: 'Safe',
      },
    })
    mockCreateInterface.mockReturnValue(createMockReadline(lines))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockHandlePreToolUse).toHaveBeenCalledWith(input)
  })

  it('handles Error in main and exits with code 2', async () => {
    mockHandlePreToolUse.mockImplementation(() => {
      throw new WorkflowError('Handler error')
    })

    const validInput = {
      session_id: 'test',
      transcript_path: '/test',
      cwd: '/test',
      permission_mode: 'default',
      hook_event_name: 'PreToolUse',
      tool_name: 'Bash',
      tool_input: { command: 'ls' },
    }
    mockCreateInterface.mockReturnValue(createMockReadline([JSON.stringify(validInput)]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockExit).toHaveBeenCalledWith(2)
  })

  it('handles non-Error thrown value and exits with code 2', async () => {
    mockHandlePreToolUse.mockImplementation(() => {
      throw 'string error'
    })

    const validInput = {
      session_id: 'test',
      transcript_path: '/test',
      cwd: '/test',
      permission_mode: 'default',
      hook_event_name: 'PreToolUse',
      tool_name: 'Bash',
      tool_input: { command: 'ls' },
    }
    mockCreateInterface.mockReturnValue(createMockReadline([JSON.stringify(validInput)]))

    await import('./dev-workflow-hooks')
    await new Promise((resolve) => setTimeout(resolve, 10))

    expect(mockConsoleError).toHaveBeenCalledWith('string error')
    expect(mockExit).toHaveBeenCalledWith(2)
  })
Timer Coupling

The polling logic is tested via fake timers with large time jumps. These tests can become brittle if the production polling interval/backoff changes. Consider asserting on observable behavior (calls count / eventual outputs) with minimal assumptions about exact timer durations, or centralizing the interval constant so tests and implementation share it.

describe('github.watchCI', () => {
  const originalEnv = process.env

  beforeEach(() => {
    vi.clearAllMocks()
    vi.useFakeTimers()
    process.env = {
      ...originalEnv,
      GITHUB_TOKEN: 'test-token',
    }
    mockSimpleGit.getRemotes.mockResolvedValue([
      {
        name: 'origin',
        refs: { fetch: 'https://github.com/owner/repo.git' },
      },
    ])
  })

  afterEach(() => {
    process.env = originalEnv
    vi.useRealTimers()
    vi.restoreAllMocks()
  })

  it('returns success when all checks pass', async () => {
    mockOctokit.pulls.get.mockResolvedValue({ data: { head: { sha: 'abc123' } } })
    mockOctokit.checks.listForRef.mockResolvedValue({
      data: {
        check_runs: [
          {
            name: 'test',
            status: 'completed',
            conclusion: 'success',
          },
          {
            name: 'lint',
            status: 'completed',
            conclusion: 'success',
          },
        ],
      },
    })

    const resultPromise = github.watchCI(42)
    await vi.runAllTimersAsync()
    const result = await resultPromise

    expect(result).toStrictEqual({
      failed: false,
      output: 'All checks passed',
    })
  })

  it('returns failure with details when a check fails', async () => {
    mockOctokit.pulls.get.mockResolvedValue({ data: { head: { sha: 'abc123' } } })
    mockOctokit.checks.listForRef.mockResolvedValue({
      data: {
        check_runs: [
          {
            name: 'test',
            status: 'completed',
            conclusion: 'failure',
            output: {
              summary: 'Tests failed',
              text: 'Error details',
            },
            details_url: 'https://example.com/run/123',
          },
        ],
      },
    })

    const resultPromise = github.watchCI(42)
    await vi.runAllTimersAsync()
    const result = await resultPromise

    expect(result.failed).toBe(true)
    expect(result.output).toContain('test: failure')
    expect(result.output).toContain('Summary: Tests failed')
  })

  it('waits for SHA to match when expectedSha provided', async () => {
    mockOctokit.pulls.get
      .mockResolvedValueOnce({ data: { head: { sha: 'old-sha' } } })
      .mockResolvedValueOnce({ data: { head: { sha: 'expected-sha' } } })
    mockOctokit.checks.listForRef.mockResolvedValue({
      data: {
        check_runs: [
          {
            name: 'test',
            status: 'completed',
            conclusion: 'success',
          },
        ],
      },
    })

    const resultPromise = github.watchCI(42, 'expected-sha')
    await vi.advanceTimersByTimeAsync(30000)
    await vi.advanceTimersByTimeAsync(30000)
    const result = await resultPromise

    expect(mockOctokit.pulls.get).toHaveBeenCalledTimes(2)
    expect(result.failed).toBe(false)
  })

  it('returns timeout when checks do not complete', async () => {
    mockOctokit.pulls.get.mockResolvedValue({ data: { head: { sha: 'abc123' } } })
    mockOctokit.checks.listForRef.mockResolvedValue({
      data: {
        check_runs: [
          {
            name: 'test',
            status: 'in_progress',
            conclusion: null,
          },
        ],
      },
    })

    const resultPromise = github.watchCI(42, undefined, 1000)
    await vi.advanceTimersByTimeAsync(35000)
    const result = await resultPromise

    expect(result).toStrictEqual({
      failed: true,
      output: 'CI timed out waiting for checks to complete',
    })
  })

  it('treats skipped conclusion as success', async () => {
    mockOctokit.pulls.get.mockResolvedValue({ data: { head: { sha: 'abc123' } } })
    mockOctokit.checks.listForRef.mockResolvedValue({
      data: {
        check_runs: [
          {
            name: 'optional',
            status: 'completed',
            conclusion: 'skipped',
          },
        ],
      },
    })

    const resultPromise = github.watchCI(42)
    await vi.runAllTimersAsync()
    const result = await resultPromise

    expect(result.failed).toBe(false)
  })

  it('handles check with no output details', async () => {
    mockOctokit.pulls.get.mockResolvedValue({ data: { head: { sha: 'abc123' } } })
    mockOctokit.checks.listForRef.mockResolvedValue({
      data: {
        check_runs: [
          {
            name: 'test',
            status: 'completed',
            conclusion: 'failure',
            output: null,
            details_url: null,
          },
        ],
      },
    })

    const resultPromise = github.watchCI(42)
    await vi.runAllTimersAsync()
    const result = await resultPromise

    expect(result.failed).toBe(true)
    expect(result.output).toBe('test: failure')
  })

  it('waits when no checks exist yet', async () => {
    mockOctokit.pulls.get.mockResolvedValue({ data: { head: { sha: 'abc123' } } })
    mockOctokit.checks.listForRef
      .mockResolvedValueOnce({ data: { check_runs: [] } })
      .mockResolvedValueOnce({
        data: {
          check_runs: [
            {
              name: 'test',
              status: 'completed',
              conclusion: 'success',
            },
          ],
        },
      })

    const resultPromise = github.watchCI(42)
    await vi.advanceTimersByTimeAsync(30000)
    await vi.advanceTimersByTimeAsync(30000)
    const result = await resultPromise

    expect(result.failed).toBe(false)
  })
})
Global State

The tests mutate global module state by caching getOctokit() and swapping process.env, combined with vi.resetModules() in some cases. This pattern can cause order-dependent behavior and hard-to-debug failures when tests run in parallel or when new tests are added. Consider ensuring isolation by importing the module fresh per test that depends on env, and avoiding relying on cached singletons across tests.

describe('github', () => {
  const originalEnv = process.env

  beforeEach(() => {
    vi.clearAllMocks()
    process.env = {
      ...originalEnv,
      GITHUB_TOKEN: 'test-token',
    }
    mockSimpleGit.getRemotes.mockResolvedValue([
      {
        name: 'origin',
        refs: { fetch: 'https://github.com/owner/repo.git' },
      },
    ])
  })

  afterEach(() => {
    process.env = originalEnv
    vi.restoreAllMocks()
  })

  describe('getOctokit', () => {
    it('returns cached instance on subsequent calls', () => {
      const first = getOctokit()
      const second = getOctokit()
      expect(first).toBe(second)
    })
  })

  describe('DELETED_USER_PLACEHOLDER', () => {
    it('has expected value', () => {
      expect(DELETED_USER_PLACEHOLDER).toBe('[deleted]')
    })
  })

  describe('getRepoInfo', () => {
    it('parses HTTPS URL', async () => {
      const result = await getRepoInfo()
      expect(result).toStrictEqual({
        owner: 'owner',
        repo: 'repo',
      })
    })

    it('parses SSH URL', async () => {
      mockSimpleGit.getRemotes.mockResolvedValue([
        {
          name: 'origin',
          refs: { fetch: '[email protected]:owner/repo.git' },
        },
      ])
      const result = await getRepoInfo()
      expect(result).toStrictEqual({
        owner: 'owner',
        repo: 'repo',
      })
    })

    it('parses URL without .git suffix', async () => {
      mockSimpleGit.getRemotes.mockResolvedValue([
        {
          name: 'origin',
          refs: { fetch: 'https://github.com/owner/repo' },
        },
      ])
      const result = await getRepoInfo()
      expect(result).toStrictEqual({
        owner: 'owner',
        repo: 'repo',
      })
    })

    it('throws GitHubError when no origin remote', async () => {
      mockSimpleGit.getRemotes.mockResolvedValue([])
      await expect(getRepoInfo()).rejects.toThrow('No origin remote found')
    })

    it('throws GitHubError for invalid URL', async () => {
      mockSimpleGit.getRemotes.mockResolvedValue([
        {
          name: 'origin',
          refs: { fetch: 'invalid-url' },
        },
      ])
      await expect(getRepoInfo()).rejects.toThrow('Could not parse GitHub URL')
    })
  })

  describe('findPRForBranch', () => {
    it('returns PR number when found', async () => {
      mockOctokit.pulls.list.mockResolvedValue({ data: [{ number: 42 }] })
      const result = await github.findPRForBranch('feature-branch')
      expect(result).toBe(42)
    })

    it('returns undefined when no PR found', async () => {
      mockOctokit.pulls.list.mockResolvedValue({ data: [] })
      const result = await github.findPRForBranch('feature-branch')
      expect(result).toBeUndefined()
    })
  })

  describe('findPRForBranchWithState', () => {
    it('returns PR with merged state', async () => {
      mockOctokit.pulls.list.mockResolvedValue({
        data: [
          {
            number: 42,
            html_url: 'https://github.com/o/r/pull/42',
            merged_at: '2024-01-01',
            state: 'closed',
          },
        ],
      })
      const result = await github.findPRForBranchWithState('feature')
      expect(result).toStrictEqual({
        number: 42,
        url: 'https://github.com/o/r/pull/42',
        state: 'merged',
      })
    })

    it('returns PR with open state', async () => {
      mockOctokit.pulls.list.mockResolvedValue({
        data: [
          {
            number: 42,
            html_url: 'https://github.com/o/r/pull/42',
            merged_at: null,
            state: 'open',
          },
        ],
      })
      const result = await github.findPRForBranchWithState('feature')
      expect(result?.state).toBe('open')
    })

    it('returns PR with closed state', async () => {
      mockOctokit.pulls.list.mockResolvedValue({
        data: [
          {
            number: 42,
            html_url: 'https://github.com/o/r/pull/42',
            merged_at: null,
            state: 'closed',
          },
        ],
      })
      const result = await github.findPRForBranchWithState('feature')
      expect(result?.state).toBe('closed')
    })

    it('returns undefined when no PR found', async () => {
      mockOctokit.pulls.list.mockResolvedValue({ data: [] })
      const result = await github.findPRForBranchWithState('feature')
      expect(result).toBeUndefined()
    })
  })

  describe('getIssue', () => {
    it('returns issue title and body', async () => {
      mockOctokit.issues.get.mockResolvedValue({
        data: {
          title: 'Issue title',
          body: 'Issue body',
        },
      })
      const result = await github.getIssue(123)
      expect(result).toStrictEqual({
        title: 'Issue title',
        body: 'Issue body',
      })
    })

    it('throws GitHubError when issue has no title', async () => {
      mockOctokit.issues.get.mockResolvedValue({
        data: {
          title: '',
          body: 'Issue body',
        },
      })
      await expect(github.getIssue(123)).rejects.toThrow('has no title')
    })

    it('throws GitHubError when issue has no body', async () => {
      mockOctokit.issues.get.mockResolvedValue({
        data: {
          title: 'Title',
          body: '',
        },
      })
      await expect(github.getIssue(123)).rejects.toThrow('has no body')
    })
  })

  describe('createPR', () => {
    it('creates PR and returns number and url', async () => {
      mockOctokit.pulls.create.mockResolvedValue({
        data: {
          number: 99,
          html_url: 'https://github.com/o/r/pull/99',
        },
      })
      const result = await github.createPR({
        title: 'PR Title',
        body: 'PR Body',
        branch: 'feature',
      })
      expect(result).toStrictEqual({
        number: 99,
        url: 'https://github.com/o/r/pull/99',
      })
    })

    it('uses main as default base branch', async () => {
      mockOctokit.pulls.create.mockResolvedValue({
        data: {
          number: 99,
          html_url: 'url',
        },
      })
      await github.createPR({
        title: 't',
        body: 'b',
        branch: 'f',
      })
      expect(mockOctokit.pulls.create).toHaveBeenCalledWith(
        expect.objectContaining({ base: 'main' }),
      )
    })

    it('uses custom base branch when provided', async () => {
      mockOctokit.pulls.create.mockResolvedValue({
        data: {
          number: 99,
          html_url: 'url',
        },
      })
      await github.createPR({
        title: 't',
        body: 'b',
        branch: 'f',
        base: 'develop',
      })
      expect(mockOctokit.pulls.create).toHaveBeenCalledWith(
        expect.objectContaining({ base: 'develop' }),
      )
    })
  })

  describe('getPR', () => {
    it('returns PR number and url', async () => {
      mockOctokit.pulls.get.mockResolvedValue({
        data: {
          number: 42,
          html_url: 'https://github.com/o/r/pull/42',
        },
      })
      const result = await github.getPR(42)
      expect(result).toStrictEqual({
        number: 42,
        url: 'https://github.com/o/r/pull/42',
      })
    })
  })

  describe('getPRWithState', () => {
    it('returns PR with merged state', async () => {
      mockOctokit.pulls.get.mockResolvedValue({
        data: {
          number: 42,
          html_url: 'url',
          merged_at: '2024-01-01',
          state: 'closed',
        },
      })
      const result = await github.getPRWithState(42)
      expect(result.state).toBe('merged')
    })

    it('returns PR with open state', async () => {
      mockOctokit.pulls.get.mockResolvedValue({
        data: {
          number: 42,
          html_url: 'url',
          merged_at: null,
          state: 'open',
        },
      })
      const result = await github.getPRWithState(42)
      expect(result.state).toBe('open')
    })
  })

  describe('getMergeableState', () => {
    it('returns mergeable state', async () => {
      mockOctokit.pulls.get.mockResolvedValue({ data: { mergeable_state: 'clean' } })
      const result = await github.getMergeableState(42)
      expect(result).toBe('clean')
    })
  })

  describe('addThreadReply', () => {
    it('calls graphql mutation with threadId and body', async () => {
      mockOctokit.graphql.mockResolvedValue({ data: {} })
      await github.addThreadReply('thread-id', 'reply body')
      expect(mockOctokit.graphql).toHaveBeenCalledWith(
        expect.stringContaining('addPullRequestReviewThreadReply'),
        {
          threadId: 'thread-id',
          body: 'reply body',
        },
      )
    })
  })

  describe('resolveThread', () => {
    it('calls graphql mutation with threadId', async () => {
      mockOctokit.graphql.mockResolvedValue({ data: {} })
      await github.resolveThread('thread-id')
      expect(mockOctokit.graphql).toHaveBeenCalledWith(
        expect.stringContaining('resolveReviewThread'),
        { threadId: 'thread-id' },
      )
    })
  })
})

describe('github token handling', () => {
  const originalEnv = process.env

  afterEach(() => {
    process.env = originalEnv
  })

  it('throws GitHubError when no token set', async () => {
    vi.resetModules()
    process.env = { ...originalEnv }
    delete process.env.GITHUB_TOKEN
    delete process.env.GH_TOKEN

    const { getOctokit: freshGetOctokit } = await import('./github')

    expect(() => freshGetOctokit()).toThrow('GitHub token not found')
  })

  it('uses GH_TOKEN when GITHUB_TOKEN not set', async () => {
    vi.resetModules()
    process.env = {
      ...originalEnv,
      GH_TOKEN: 'gh-token',
    }
    delete process.env.GITHUB_TOKEN

    const { getOctokit: freshGetOctokit } = await import('./github')

    expect(() => freshGetOctokit()).not.toThrow()
  })

@coderabbitai

coderabbitai Bot commented Jan 27, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds extensive Vitest test coverage across dev-workflow features and platform utilities, exports and tightens several Zod schemas, updates some handler and CLI async flows, and makes small non-functional v8-ignore comment insertions and minor config changes.

Changes

Cohort / File(s) Summary
Vitest Configuration
tools/dev-workflow/vitest.config.mts
Expanded coverage include pattern to **/*.ts, added excludes (*.d.ts, shell/index.ts), raised branch coverage threshold to 100.
Claude Hooks — Tests
tools/dev-workflow/features/claude-hooks/domain/handlers/post-tool-use-handler.spec.ts, tools/dev-workflow/features/claude-hooks/domain/handlers/pre-tool-use-handler.spec.ts, tools/dev-workflow/features/claude-hooks/domain/handlers/stop-handler.spec.ts, tools/dev-workflow/features/claude-hooks/domain/hook-input-schemas.spec.ts, tools/dev-workflow/features/claude-hooks/domain/hook-output-schemas.spec.ts, tools/dev-workflow/features/claude-hooks/entrypoint/hook-router.spec.ts, tools/dev-workflow/features/claude-hooks/use-cases/handle-hook.spec.ts
New comprehensive tests for hook handlers, input/output schemas, routing, and use-case orchestration (permission decisions, transcript parsing, error flows, stdin/stdout handling).
Claude Hooks — Schemas & Logic
tools/dev-workflow/features/claude-hooks/domain/hook-output-schemas.ts, tools/dev-workflow/features/claude-hooks/use-cases/handle-hook.ts, tools/dev-workflow/features/claude-hooks/domain/permission-decision.ts
Exported hook output schemas; added permissionDecisionReason and optional outputToUser to stop output; removed UnhandledHookEventError and simplified route switch. blockStop now includes outputToUser: { passthrough: false }.
Complete Task — Tests
tools/dev-workflow/features/complete-task/domain/pipeline-outcome.spec.ts, tools/dev-workflow/features/complete-task/domain/pull-request-draft.spec.ts, tools/dev-workflow/features/complete-task/domain/steps/fetch-feedback.spec.ts, tools/dev-workflow/features/complete-task/domain/steps/run-code-review.spec.ts, tools/dev-workflow/features/complete-task/domain/steps/submit-pull-request.spec.ts, tools/dev-workflow/features/complete-task/domain/steps/verify-build.spec.ts, tools/dev-workflow/features/complete-task/entrypoint/cli.spec.ts, tools/dev-workflow/features/complete-task/use-cases/complete-task.spec.ts
New tests covering complete-task workflow: pipeline formatting, PR draft resolution, step behaviors (code review, submit PR, verify build), and CLI wiring; extensive mocks for filesystem, git, GitHub, and agents.
Complete Task — Minor Code Comments
tools/dev-workflow/features/complete-task/domain/steps/run-code-review.ts
Added v8-ignore comment markers around error handling and a defensive validation check (no behavioral changes).
Get PR Feedback — Tests
tools/dev-workflow/features/get-pr-feedback/domain/steps/fetch-feedback.spec.ts, tools/dev-workflow/features/get-pr-feedback/entrypoint/cli.spec.ts, tools/dev-workflow/features/get-pr-feedback/use-cases/get-pr-feedback.spec.ts
New tests for PR feedback fetching, CLI parsing, context building (branch resolution, flags, --pr), and propagation of prNumber/prState/prUrl.
Respond to Feedback — Tests & Code
tools/dev-workflow/features/respond-to-feedback/domain/feedback-response.spec.ts, tools/dev-workflow/features/respond-to-feedback/entrypoint/cli.spec.ts, tools/dev-workflow/features/respond-to-feedback/entrypoint/cli.ts, tools/dev-workflow/features/respond-to-feedback/use-cases/respond-to-feedback.spec.ts, tools/dev-workflow/features/respond-to-feedback/use-cases/respond-to-feedback.ts
New tests for formatting and input validation; wrapped CLI invocation with rejection handler; made executeRespondToFeedback async and switched to await-based flow.
Platform — Parsing & Formatting Tests
tools/dev-workflow/platform/domain/branch-naming/issue-branch-parser.spec.ts, tools/dev-workflow/platform/domain/commit-format/commit-message-formatter.spec.ts, tools/dev-workflow/platform/domain/commit-format/conventional-commit-title.spec.ts
Added unit tests for branch parsing and commit message/conventional-commit validation/formatting.
Review Feedback — Tests & Schema Changes
tools/dev-workflow/platform/domain/review-feedback/feedback-location.spec.ts, tools/dev-workflow/platform/domain/review-feedback/get-pr-feedback.spec.ts, tools/dev-workflow/platform/domain/review-feedback/review-decision.spec.ts, tools/dev-workflow/platform/domain/review-feedback/review-thread.spec.ts, tools/dev-workflow/platform/domain/review-feedback/reviewer.spec.ts, tools/dev-workflow/platform/domain/review-feedback/review-decision.ts
Added many tests for review feedback utilities and exported reviewDecisionSchema with stricter validation (non-empty reviewer and enum state); updated get-pr-feedback to parse decisions via the schema.
Workflow Execution — Tests
tools/dev-workflow/platform/domain/workflow-execution/error-handler.spec.ts, tools/dev-workflow/platform/domain/workflow-execution/run-workflow.spec.ts, tools/dev-workflow/platform/domain/workflow-execution/step-result.spec.ts, tools/dev-workflow/platform/domain/workflow-execution/workflow-runner.spec.ts
New tests for error handling, runWorkflow behavior, step-result constructors, and workflow runner semantics (success/failure propagation, output formatting, exit codes).
Platform Infra — External Clients Tests & Minor Comments
tools/dev-workflow/platform/infra/external-clients/claude-agent.spec.ts, tools/dev-workflow/platform/infra/external-clients/claude-agent.ts, tools/dev-workflow/platform/infra/external-clients/cli-args.spec.ts, tools/dev-workflow/platform/infra/external-clients/git-client.spec.ts, tools/dev-workflow/platform/infra/external-clients/github-graphql-client.spec.ts, tools/dev-workflow/platform/infra/external-clients/github-rest-client.spec.ts, tools/dev-workflow/platform/infra/external-clients/github-rest-client.ts, tools/dev-workflow/platform/infra/external-clients/nx-runner.spec.ts
Large set of tests for external integrations (Claude SDK, CLI args, Git, GitHub GraphQL/REST, Nx runner); added non-functional v8-ignore comment markers in two client files.
Shell Index Tests
tools/dev-workflow/shell/index.spec.ts
New test validating public exports and schemas from shell/index.

Sequence Diagram(s)

(omitted — changes are primarily extensive tests, schema exports, small async/handler updates; no new multi-component control flow requiring diagram)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐇 I hopped through tests both big and small,

Stubbing agents, hooks, and Git's call;
Schemas exported, async made neat,
Mocks in place—no flaky feat!
Cheers from my warren — coverage, complete!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'test(dev-workflow): expand test coverage to 100%' accurately reflects the main objective of the changeset: expanding test coverage for the dev-workflow package to reach 100%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jan 27, 2026 •

Copy link
Copy Markdown

Deploying living-architecture with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0eddde3
Status: ✅  Deploy successful!
Preview URL: https://a5a7ce4a.living-architecture.pages.dev
Branch Preview URL: https://issue-206-expand-dev-workflo.living-architecture.pages.dev

View logs

Comment thread tools/dev-workflow/respond-to-feedback/respond-to-feedback.spec.ts Outdated
Comment thread tools/dev-workflow/complete-task/steps/code-review.spec.ts Outdated
Comment thread tools/dev-workflow/external-clients/nx.spec.ts Outdated
Comment thread tools/dev-workflow/complete-task/steps/code-review.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.claude/agents/code-review.md (1)

14-18: Fix the step list to match the new two-section output contract.
Step 5 still says “Output your findings as structured JSON,” which contradicts the Criteria + JSON requirement below. Update it to avoid conflicting instructions.

🔧 Proposed update
-5. Output your findings as structured JSON
+5. Output your findings using the two-section format (Criteria Checked + JSON Result)
🤖 Fix all issues with AI agents
In `@tools/dev-workflow/complete-task/resolve-pr-details.spec.ts`:
- Around line 87-118: Update the two tests that expect WorkflowError to also
assert the error message content: in the "throws WorkflowError when PR body is
missing" and "throws WorkflowError for invalid conventional commit format"
tests, wrap the call to resolvePRDetails(undefined, undefined) with a function
that captures the thrown error and assert its message (e.g., using
toThrowError(/expected message/) or expect(() => ...).toThrowError('exact
message')); reference the same mockParseArg setup and ensure messages match the
WorkflowError text produced by resolvePRDetails so tests validate both error
type and message.

In `@tools/dev-workflow/complete-task/steps/code-review.spec.ts`:
- Around line 164-173: Update the two failing tests to also assert the
AgentError message text: when mockReadFile.mockRejectedValue(new
AgentError('File not found')) ensure the promise from
codeReview.execute(createContext()) rejects with an AgentError whose message
equals or matches "File not found" (e.g., use rejects.toThrowError(/File not
found/) or equivalent), and when mockReadFile.mockRejectedValue('string error')
assert that codeReview.execute(createContext()) rejects with an AgentError whose
message contains or matches "string error" in addition to verifying the error
type (keep using mockReadFile, codeReview.execute, AgentError, and createContext
to locate the tests).

In `@tools/dev-workflow/complete-task/steps/verify-build.spec.ts`:
- Around line 12-15: In the describe('verifyBuild') test suite add an afterEach
hook to restore spies after each test: append afterEach(() =>
vi.restoreAllMocks()) to complement the existing beforeEach(() =>
vi.clearAllMocks()) so that mocks created in this file are fully restored
between tests and match the pattern used in run-workflow.spec.ts and
get-pr-feedback.spec.ts.

In `@tools/dev-workflow/conventions/conventions.spec.ts`:
- Around line 99-125: The current tests for prDetailsSchema only cover valid
inputs; add tests that assert the schema rejects null, undefined, missing
required fields, and wrong types. Add new it() cases that call
prDetailsSchema.parse with null and undefined and expect a throw (e.g.,
expect(() => prDetailsSchema.parse(null)).toThrow()), test objects missing
prTitle/commitMessage to ensure parse throws, and include an invalid-type case
(e.g., issueNumber as a string) using either parse with expect(...).toThrow() or
prDetailsSchema.safeParse(...) and assert result.success === false; reference
prDetailsSchema.parse and prDetailsSchema.safeParse in your assertions.

In `@tools/dev-workflow/dev-workflow-hooks/claude-code-output-schemas.spec.ts`:
- Around line 1-63: Add tests that assert the schemas reject null/undefined and
missing-required-field shapes: for preToolUseOutputSchema add cases where the
parsed input is null and undefined and where hookSpecificOutput is missing
permissionDecisionReason (use expect(() =>
preToolUseOutputSchema.parse(...)).toThrow()); for postToolUseOutputSchema add
tests that parse null and undefined inputs and a case with hookSpecificOutput
present but set to null/undefined; for stopOutputSchema add tests parsing null
and undefined and an object with decision set to an invalid value or missing
when required to ensure it throws. Reference the existing test names/symbols
preToolUseOutputSchema, postToolUseOutputSchema, and stopOutputSchema and follow
the same it/describe patterns.

In `@tools/dev-workflow/errors.spec.ts`:
- Around line 8-23: Consolidate the three separate tests in the "GitError" suite
into a single spec: replace the three it(...) blocks with one it(...) that
constructs const error = new GitError('git failed') and asserts all properties
together (expect(error.name).toBe('GitError'), expect(error.message).toBe('git
failed'), and expect(error).toBeInstanceOf(Error)); keep the
describe('GitError', ...) wrapper and the same message string to preserve
behavior and coverage.

In `@tools/dev-workflow/external-clients/github.spec.ts`:
- Around line 22-26: The octokitMock class constructor currently returns a value
which violates Biome's noConstructorReturn rule; update the constructor in
octokitMock so it does not return octokit but instead copies the mock methods
onto the instance (e.g., call Object.assign(this, octokit) inside the
constructor) so the class instance behaves like the mock without returning from
the constructor.

Comment thread tools/dev-workflow/complete-task/resolve-pr-details.spec.ts Outdated
Comment thread tools/dev-workflow/complete-task/steps/code-review.spec.ts Outdated
Comment thread tools/dev-workflow/complete-task/steps/verify-build.spec.ts Outdated
Comment thread tools/dev-workflow/conventions/conventions.spec.ts Outdated
Comment thread tools/dev-workflow/dev-workflow-hooks/claude-code-output-schemas.spec.ts Outdated
Comment thread tools/dev-workflow/errors.spec.ts Outdated
Comment thread tools/dev-workflow/external-clients/github.spec.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tools/dev-workflow/complete-task/steps/code-review.spec.ts`:
- Around line 122-144: The test asserts only result.type === 'failure' but the
title expects a specific failure kind "fix_review"; update the assertion after
calling codeReview.execute(createContext()) to verify the failure detail matches
"fix_review" (e.g., assert result.failureType === 'fix_review' or
result.subtype/metadata that your code uses to indicate the specific failure) so
the test title and assertions align (use the actual property name on the result
object such as result.failureType/result.subtype/result.details).

Comment thread tools/dev-workflow/complete-task/steps/code-review.spec.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tools/dev-workflow/external-clients/github.spec.ts`:
- Around line 112-125: Update the tests for getRepoInfo to assert error
properties separately: instead of only expecting the message string (e.g.,
rejects.toThrow('No origin remote found')), capture the rejected error and
assert its type/instance (instanceof GitHubError), its name property (error.name
=== 'GitHubError'), and its message property (error.message includes the
expected text); apply the same pattern to the invalid URL test and the other
error test blocks mentioned so each path checks instanceof GitHubError,
error.name, and error.message explicitly.

Comment thread tools/dev-workflow/external-clients/github.spec.ts Outdated
@NTCoding
NTCoding force-pushed the issue-206-expand-dev-workflow-test-cover branch from 5c39af2 to d840a6a Compare January 28, 2026 08:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/dev-workflow/platform/infra/external-clients/github-rest-client.ts (1)

12-29: Remove v8 ignore blocks; make these branches testable instead.

These paths are reachable (env var selection, polling delays, timeout) and should be covered rather than excluded. Prefer deterministic tests (e.g., set env vars in tests, use fake timers or inject a sleep/time provider) and remove the ignore markers.

💡 Suggested change (remove ignore markers)
-/* v8 ignore start - env var handling, tests set GITHUB_TOKEN directly */
 function getGitHubToken(): string {
   const githubToken = process.env.GITHUB_TOKEN
   const ghToken = process.env.GH_TOKEN
@@
   throw new GitHubError(
     'GitHub token not found. Set GITHUB_TOKEN or GH_TOKEN environment variable.',
   )
 }
-/* v8 ignore stop */
@@
-      /* v8 ignore start - polling branch, difficult to test timing-dependent code */
       if (expectedSha && pr.head.sha !== expectedSha) {
         await new Promise((resolve) => setTimeout(resolve, pollInterval))
         continue
       }
-      /* v8 ignore stop */
@@
-      /* v8 ignore start - polling delay, timing-dependent */
       await new Promise((resolve) => setTimeout(resolve, pollInterval))
     }
 
     return {
       failed: true,
       output: 'CI timed out waiting for checks to complete',
     }
-    /* v8 ignore stop */

Based on learnings: "Avoid using /* v8 ignore next -- preserve */ coverage skip comments unless code is unreachable and required for compiler satisfaction - prefer extracting code or writing tests instead."

Also applies to: 278-344

🤖 Fix all issues with AI agents
In `@tools/dev-workflow/features/claude-hooks/domain/hook-output-schemas.spec.ts`:
- Around line 180-196: The tests use toBeDefined() which doesn't confirm the
correct union variant; update the two cases to assert distinguishing properties:
for the PreToolUse case verify hookSpecificOutput exists and that
hookSpecificOutput.hookEventName equals 'PreToolUse' (and optionally
permissionDecision or permissionDecisionReason), and for the StopOutput case
assert the continue property is true to prove the StopOutput variant is
constructed; locate these changes around the HookOutput, PreToolUseOutput,
StopOutput examples in the spec.

In `@tools/dev-workflow/features/claude-hooks/entrypoint/hook-router.spec.ts`:
- Around line 61-75: The test is coupling to implementation by asserting
mockRouteToHandler was called; remove that brittle assertion and rely solely on
outcome assertions instead. Update the spec for the 'outputs handler result for
valid input' case to delete the
expect(mockRouteToHandler).toHaveBeenCalledWith(...) line and keep/strengthen
assertions on capturedOutput (and any other printed/result values) to verify
behavior; locate symbols mockRouteToHandler, capturedOutput, mockParseHookInput
and the import of './hook-router' to make this change.

In `@tools/dev-workflow/features/claude-hooks/entrypoint/hook-router.ts`:
- Around line 35-39: Remove the v8 coverage ignore comments around the reachable
catch blocks in hook-router.ts (the try/catch that handles
parseHookInput/readStdin and the other two catch ranges noted at 55-66 and
72-77); instead leave the catch handlers intact (they should return { success:
false }) and add unit/integration tests that exercise those branches by mocking
readStdin to provide invalid JSON or by mocking parseHookInput to throw,
asserting the function returns { success: false } so those paths are covered by
tests.

In
`@tools/dev-workflow/features/complete-task/domain/steps/fetch-feedback.spec.ts`:
- Around line 83-99: The test 'truncates long thread bodies in summary'
currently only checks result.type; update it to also assert that the returned
summary includes the truncated thread body by calling
fetchPRFeedback.execute(ctx) and verifying result.details (or the field that
contains the summary) contains longBody.slice(0, 100) + '...'; locate this test
block using mockGetPRFeedback and fetchPRFeedback.execute and add an expectation
that result.details.includes(truncatedBody) so the truncation behavior is
actually validated.
- Around line 29-35: Update the test case that calls fetchPRFeedback.execute
(the one using createContext({ prNumber: undefined })) to assert specific
failure details in addition to result.type === 'failure': check that
result.details (or the failure message field returned) contains the string "No
PR number available" and assert the failure kind/type is "fix_errors" (e.g.,
result.failureType === 'fix_errors' or the equivalent property returned by
fetchPRFeedback.execute) so the spec verifies the exact failure shape produced
by fetch-feedback.ts.

In
`@tools/dev-workflow/features/complete-task/domain/steps/submit-pull-request.spec.ts`:
- Around line 54-66: The test uses toHaveBeenCalledWith() with no arguments for
mockGit.stageAll which is equivalent to toHaveBeenCalled(); update the assertion
in submit-pull-request.spec.ts to assert mockGit.stageAll was called using
toHaveBeenCalled() instead of toHaveBeenCalledWith(), leaving the mockGit.commit
assertion unchanged to reference the commit message.

In `@tools/dev-workflow/features/complete-task/use-cases/complete-task.spec.ts`:
- Around line 31-53: Replace the manual type guards
isContextBuilder/isResultFormatter with local Zod schemas and validate using
schema.safeParse() inside getContextBuilder and getResultFormatter; pull the
builder/formatter from mockRunWorkflow.mock.calls[0][1] and [0][2], safeParse
and on failure throw the same TypeError messages, returning the parsed value on
success. Keep the Zod schemas (e.g., ContextBuilderSchema,
ResultFormatterSchema) declared only in this test file and do not export them,
and update getContextBuilder/getResultFormatter to use these schemas instead of
the typeof checks.

In
`@tools/dev-workflow/features/get-pr-feedback/domain/steps/fetch-feedback.spec.ts`:
- Around line 62-75: The test is coupling to implementation by asserting on the
mock call; remove the internal mock assertion and rely on behavioral checks
instead: delete the expect(mockGetPRFeedback).not.toHaveBeenCalled() line in the
'returns merged state without fetching feedback when merged' test that calls
fetchFeedback.execute(ctx), keeping the assertSuccess(result) and
expect(result.output).toHaveProperty('state', 'merged') assertions to validate
behavior; ensure mockGetPRFeedback is not referenced in this spec so the test
only asserts output state.

In
`@tools/dev-workflow/features/get-pr-feedback/use-cases/get-pr-feedback.spec.ts`:
- Around line 75-101: Remove the implementation-coupling assertions that check
internal mocks (mockCurrentBranch, mockHasFlag, mockParseArg) in the
get-pr-feedback.spec.ts tests and instead assert the outputs of the context
builder returned by getContextBuilder(); specifically, after calling
executeGetPRFeedback() and await getContextBuilder()(), verify the context
object contains the expected branch, includeResolved flag, and parsed pr value
(rather than asserting the internal mock calls). Keep the existing test that
ensures runWorkflow is called with steps and a context builder
(executeGetPRFeedback and mockRunWorkflow) but replace the three tests
referencing mockCurrentBranch, mockHasFlag, and mockParseArg with assertions on
the context properties produced by getContextBuilder().
- Around line 46-55: The test defines a manual type guard isContextBuilder and
uses it in getContextBuilder to extract the callback from
mockRunWorkflow.mock.calls[0][1]; instead of that runtime check, remove the
isContextBuilder helper and simply cast the extracted value to ContextBuilder
inside getContextBuilder (e.g., const builder = mockRunWorkflow.mock.calls[0][1]
as ContextBuilder) and return it, relying on test assertions to validate the
context shape; keep references to mockRunWorkflow, getContextBuilder, and
ContextBuilder so the replacement is easy to locate.

In `@tools/dev-workflow/features/respond-to-feedback/entrypoint/cli.ts`:
- Around line 4-8: Remove the v8 coverage-ignore comments around the CLI
error-handling block and add a test in cli.spec.ts that mocks
executeRespondToFeedback to return a rejected promise so the catch path is
exercised; specifically, edit the file containing
executeRespondToFeedback().catch(...) to delete the /* v8 ignore */ comments and
in cli.spec.ts stub or spy on executeRespondToFeedback to reject and assert that
the process error logging and exit behavior occur (or that error handling is
invoked) to restore coverage for the error path.

In
`@tools/dev-workflow/features/respond-to-feedback/use-cases/respond-to-feedback.spec.ts`:
- Around line 154-170: The tests currently assert Zod's internal error code
("invalid_type") which is brittle; update the three specs to simply expect
executeRespondToFeedback() to reject (e.g., await
expect(executeRespondToFeedback()).rejects.toThrow()) or to assert on a stable
user-facing message instead of "invalid_type". Then add a small dedicated
validation helper (e.g., validateCliArgs or validateRespondToFeedbackArgs)
inside respond-to-feedback.ts and call it from executeRespondToFeedback(); this
helper should check for missing threadId, action, or message and throw clear,
stable Error messages that the tests can assert against, removing direct
reliance on Zod internals.

In
`@tools/dev-workflow/platform/domain/branch-naming/issue-branch-parser.spec.ts`:
- Around line 1-26: Add three edge-case unit tests to
issue-branch-parser.spec.ts for parseIssueNumber: one verifying "issue-0"
returns 0 (or documents expected behavior), one verifying "issue-" returns
undefined, and one verifying "ISSUE-123" documents case-sensitivity (either
returns 123 or undefined). Place tests alongside existing cases using the same
describe/it structure and reference parseIssueNumber so the behavior for zero,
missing number, and uppercase prefix is explicitly asserted.

In
`@tools/dev-workflow/platform/domain/commit-format/commit-message-formatter.spec.ts`:
- Around line 12-16: Update the test "handles title with special characters" to
use an exact equality assertion against the full formatted output from
formatCommitMessage rather than .toContain(); locate the test and replace the
loose expect(result).toContain(...) checks with a single
expect(result).toBe(expected) that includes the full commit message string (the
title with the quotes, the required double-newline separator, and the
"Co-Authored-By:" trailer) so the test verifies the exact formatting produced by
formatCommitMessage.

In
`@tools/dev-workflow/platform/domain/commit-format/conventional-commit-title.spec.ts`:
- Around line 50-60: Update the tests so the guidance message regex is asserted
in the same cases that assert the error type: modify the "throws WorkflowError
for invalid format" and "throws WorkflowError for missing colon" specs to expect
ConventionalCommitTitle.parse(...) to throw a WorkflowError AND include the
guidance message (use toThrow with the message/regex), then remove the separate
"throws error message with guidance" test; target the existing test cases that
call ConventionalCommitTitle.parse to fold the message assertion into them.

In `@tools/dev-workflow/platform/domain/review-feedback/review-decision.ts`:
- Line 10: The exported type ReviewState (derived from reviewStateSchema) is
unused and causing CI warnings; either remove the export or actually consume the
type. To fix, either delete the export by changing "export type ReviewState =
z.infer<typeof reviewStateSchema>" to "type ReviewState = z.infer<typeof
reviewStateSchema>" or remove the declaration entirely if not needed, or use
ReviewState in a public API (e.g., annotate function return types or params with
ReviewState) so the export is consumed; reference the symbol names ReviewState
and reviewStateSchema to locate the declaration to update.

In `@tools/dev-workflow/platform/domain/review-feedback/review-thread.spec.ts`:
- Around line 110-143: Tests for formatThreadForOutput are missing a case for
file-level locations; add a new it block in review-thread.spec.ts that
constructs a thread object with type 'active', threadId, author via
Reviewer.createFromGitHubLogin, body, and location { type: 'file-level', file:
'src/file.ts' }, call formatThreadForOutput(thread) and assert the resulting
location equals the expected file-level string (e.g., 'src/file.ts') to cover
that branch.

In `@tools/dev-workflow/platform/domain/review-feedback/reviewer.spec.ts`:
- Around line 24-29: Add a unit test that covers the null-login placeholder
case: call Reviewer.createFromGitHubLogin(null) and assert that
reviewer.toString() returns '[deleted]'; place this alongside the existing
toString test so both valid login (Reviewer.createFromGitHubLogin('octocat'))
and null-login behaviors are verified using the toString method.

In
`@tools/dev-workflow/platform/domain/workflow-execution/workflow-runner.spec.ts`:
- Around line 11-22: Split the combined test in the describe('WorkflowError')
block into three focused it() tests: one asserting error.message equals 'Test
error' (use new WorkflowError('Test error')), one asserting error.name equals
'WorkflowError' (construct accordingly), and one asserting the instance check
(expect(new WorkflowError('Test')).toBeInstanceOf(Error)); update the spec so
each property (message, name, instanceof) has its own it() case for clearer
failures and debuggability.

In `@tools/dev-workflow/platform/infra/external-clients/claude-agent.spec.ts`:
- Around line 21-27: The test groups a few assertions into one spec — split the
single `it` block for `ClaudeQueryError` into separate tests: one `it` that
constructs `new ClaudeQueryError('test message')` and asserts `error.name ===
'ClaudeQueryError'`, a second `it` that asserts `error.message === 'test
message'`, and a third `it` that asserts `error instanceof Error` (or
`expect(error).toBeInstanceOf(Error)`), keeping the
`describe('ClaudeQueryError', ...)` and using the same `ClaudeQueryError`
constructor to locate the code.

In `@tools/dev-workflow/platform/infra/external-clients/git-client.spec.ts`:
- Around line 149-154: The test invokes git.currentBranch() twice when asserting
error type and message; change it to call git.currentBranch() once and store the
resulting promise (e.g., const result = git.currentBranch()) then use that
single promise in both expects (await expect(result).rejects.toThrow(GitError)
and await expect(result).rejects.toThrow('Could not determine current branch')),
keeping mockRepo.status.mockResolvedValue({ current: null }) as-is.

Comment thread tools/dev-workflow/features/claude-hooks/entrypoint/hook-router.ts Outdated
Comment thread tools/dev-workflow/platform/domain/review-feedback/reviewer.spec.ts
NTCoding and others added 2 commits January 28, 2026 08:42
…o stop hook

- Add outputToUser passthrough to stop hook schema
- Remove banned comments from tests
- Strengthen weak assertions in tests
- Fix lint violations

Co-Authored-By: Claude Opus 4.5 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@tools/dev-workflow/platform/domain/workflow-execution/error-handler.spec.ts`:
- Around line 21-32: The test suite for handleWorkflowError leaves spies on
process.exit and console.error active after each test; add an afterEach that
restores those mocks (either call vi.restoreAllMocks() or individually call
(process.exit as any).mockRestore() and (console.error as any).mockRestore()) to
undo the vi.spyOn calls and prevent cross-test pollution while keeping
TestExitSignal behavior isolated to each test.
- Around line 48-56: The test in error-handler.spec.ts redundantly re-spies on
process.exit inside the 'exits with code 1' case; remove the line that creates
the local spy (const mockExit = vi.spyOn(process, 'exit')) and rely on the
existing mock set up in beforeEach, leaving the expect(() =>
handleWorkflowError(testError)).toThrow(TestExitSignal) and
expect(process.exit).toHaveBeenCalledWith(1) (or the existing mocked reference)
intact so the test still verifies handleWorkflowError behavior.

In
`@tools/dev-workflow/platform/infra/external-clients/github-rest-client.spec.ts`:
- Around line 36-42: Split the single test in the describe('GitHubError') block
into three granular tests: one asserting error.name equals 'GitHubError', one
asserting error.message equals the provided message, and one asserting the
instance relationship (e.g., expect(error).toBeInstanceOf(Error) or expect(error
instanceof Error).toBe(true)). Locate the tests around the existing
describe('GitHubError') and replace the single combined it(...) with three
separate it(...) blocks that each construct new GitHubError('test message') and
perform one assertion each on GitHubError.
- Around line 45-57: The test sets process.env.GITHUB_TOKEN in the getOctokit
describe block but never restores it, causing cross-test leakage; modify the
suite to capture the original token (e.g., save process.env.GITHUB_TOKEN into a
local variable) and add an afterEach hook that restores process.env.GITHUB_TOKEN
to its original value (or deletes it if it was undefined) so the getOctokit
tests leave the environment unchanged for other specs.

NTCoding and others added 2 commits January 28, 2026 11:40
- Add comprehensive error path tests for hook-router.ts
- Add error path tests for respond-to-feedback/cli.ts
- Replace manual type guards with Zod validation
- Fix stop hook to use passthrough: false
- Remove v8 coverage ignore blocks
- All tests now pass with 100% coverage

Co-Authored-By: Claude Opus 4.5 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tools/dev-workflow/features/claude-hooks/entrypoint/hook-router.spec.ts`:
- Around line 57-75: The tests currently use a fixed 50ms setTimeout after
mocking process.stdin (mockStdin) and importing './hook-router', which is flaky;
replace the time-based wait with awaiting the stdin readable stream's 'end'
event for determinism: after calling mockStdin(...) and triggering the
import('./hook-router'), create and await a Promise that resolves when
process.stdin emits 'end' (or 'close') so the test proceeds only once the mocked
stdin has finished, and apply the same change to the other tests that use the
50ms timeout (referencing mockStdin and the test blocks that currently call new
Promise(resolve => setTimeout(resolve, 50))).

In `@tools/dev-workflow/features/respond-to-feedback/entrypoint/cli.spec.ts`:
- Around line 12-15: Reformat the vi.mock factory to improve readability by
expanding the returned object across multiple lines and aligning keys/values;
specifically update the mock defined with vi.mock that returns
executeRespondToFeedback to use the existing mockExecuteRespondToFeedback (and
include mockProcessExit if intended) in a multi-line object form so the factory
is clearer and consistent with codebase style (references:
mockExecuteRespondToFeedback, mockProcessExit, vi.mock,
executeRespondToFeedback).

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant