Skip to content

test: fix unit test isolation via dependency injection#96

Open
twaldin wants to merge 2 commits intomainfrom
fix/unit-test-isolation
Open

test: fix unit test isolation via dependency injection#96
twaldin wants to merge 2 commits intomainfrom
fix/unit-test-isolation

Conversation

@twaldin
Copy link
Copy Markdown
Owner

@twaldin twaldin commented May 8, 2026

Resolves module mock leakage causing test failures when run after ssh.test.ts or remote-cmd.test.ts.

Problem

Bun's mock.module() creates global mocks that persist beyond the test file's lifecycle, even with mock.restore() in afterAll. When ssh.test.ts mocked 'fs' and 'child_process', subsequent tests needing real filesystem operations would fail (gates.test.ts, skills.test.ts, etc.).

Solution

Replaced global module mocking with dependency injection pattern:

Source changes:

  • src/ssh.ts: Export _setSshDepsForTest() to inject test doubles for execFileSync/statSync
  • src/commands/remote.ts: Export _setRemoteCommandDepsForTest() for fs/os/fetch/Bun.write
  • src/instructions.ts: Add droid .factory/skills directory mapping
  • src/skills.ts: Install droid skills to .factory/skills (native discovery)

Test changes:

  • tests/unit/ssh.test.ts: Use DI instead of mock.module()
  • tests/unit/remote-cmd.test.ts: Use DI instead of mock.module('fs')
  • tests/unit/skills.test.ts: Added droid adapter test case

Validation

bun test tests/unit/{ssh,remote-cmd,skills,gates,qna,adapters/registry}.test.ts
# All 64 tests pass (previously 9 failures)

Full unit suite now shows 98/102 files passing (4 pre-existing failures unrelated to isolation issue).

Summary by CodeRabbit

  • New Features

    • Added support for the Droid CLI with skills managed in a CLI-specific directory structure.
  • Documentation

    • Updated OAuth proxy configuration documentation with additional local proxy patch procedures.

twaldin and others added 2 commits May 7, 2026 23:26
Resolves module mock leakage that was causing test failures in gates.test.ts, skills.test.ts, and other tests when run after ssh.test.ts or remote-cmd.test.ts.

Root cause: Bun's mock.module() creates global mocks that persist beyond the test file's lifecycle, even with mock.restore() in afterAll. When ssh.test.ts mocked 'fs' and 'child_process', subsequent tests that needed real filesystem operations would fail.

Solution: Replace global module mocking with dependency injection:
- src/ssh.ts: Export _setSshDepsForTest() to inject test doubles for execFileSync/statSync
- src/commands/remote.ts: Export _setRemoteCommandDepsForTest() for fs/os/fetch/Bun.write
- tests/unit/ssh.test.ts: Use DI instead of mock.module()
- tests/unit/remote-cmd.test.ts: Use DI instead of mock.module('fs')
- tests/unit/skills.test.ts: Added droid adapter test case for .factory/skills projection

Fixes:
- src/instructions.ts: Add droid .factory/skills directory mapping
- src/skills.ts: Install droid skills to .factory/skills (native discovery)

Validates with:
  bun test tests/unit/ssh.test.ts tests/unit/remote-cmd.test.ts tests/unit/skills.test.ts tests/unit/gates.test.ts
  All 64 tests pass (previously 9 failures)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Droid as a new CLI adapter with skills installed under .factory/skills, introduces dependency injection hooks for testable command modules (remote and SSH), updates OAuth proxy documentation for string input handling, and refactors all related tests to use the new injection pattern.

Changes

Droid CLI Support and Testability Refactor

Layer / File(s) Summary
CLI Configuration and Skill Routing
src/instructions.ts, src/skills.ts
skillsDir() recognizes droid CLI and returns .factory/skills; projectSkills() installs Droid skills under .factory/skills instead of .flt/skills.
Remote Command Testable Dependencies
src/commands/remote.ts
Adds injectable _setRemoteCommandDepsForTest hook; replaces direct calls to fs.existsSync, fs.mkdtempSync, os.tmpdir, fetch, and Bun.write with injected implementations that default to real functions.
SSH Command Testable Dependencies
src/ssh.ts
Adds injectable _setSshDepsForTest hook; routes execFileSync and statSync calls through injectable implementations for sshExec, sshExecCheck, rsyncTo, and directory detection.
OAuth Proxy Configuration
DROID-OAUTH-PROXY.md
Adds proxy patch step that converts Responses API string input to list format required by Codex; renumbers subsequent steps.
Remote Command Test Refactoring
tests/unit/remote-cmd.test.ts
Refactors to use _setRemoteCommandDepsForTest for mocking tmpdir, fetch, and Bun.write instead of global module overwrites; updates assertions to validate mock calls.
SSH Test Refactoring
tests/unit/ssh.test.ts
Refactors to use _setSshDepsForTest injection instead of Bun's mock.module; clears dependencies in afterAll.
Skills Test for Droid
tests/unit/skills.test.ts
Adds droidAdapter fixture and test case verifying projectSkills installs under .factory/skills and updates AGENTS.md correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A droid arrived with skills to share,
Now living in a .factory lair,
With deps injected, tests run clean,
The finest refactor I've ever seen!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: fixing unit test isolation through dependency injection. It directly summarizes the primary change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/unit-test-isolation

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
tests/unit/ssh.test.ts (1)

133-136: 💤 Low value

mock.restore() is vestigial after the DI refactor

There are no mock.module() calls left in this file, so mock.restore() is a no-op. It's harmless but could mislead future readers into thinking there are module-level mocks in play.

🧹 Proposed cleanup
  afterAll(() => {
    _setSshDepsForTest(null)
-   mock.restore()
  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/ssh.test.ts` around lines 133 - 136, Remove the vestigial
mock.restore() call in the afterAll block because there are no mock.module()
usages in this test file; update the afterAll to only call
_setSshDepsForTest(null) (leave _setSshDepsForTest as-is) and remove any
reference to mock.restore to avoid confusion about module-level mocks (locate
the afterAll in tests/unit/ssh.test.ts and edit the afterAll that currently
calls mock.restore()).
src/commands/remote.ts (1)

22-34: 💤 Low value

_setRemoteCommandDepsForTest lacks JSDoc, unlike its peer in src/ssh.ts

Both _setSshDepsForTest and _setRemoteCommandDepsForTest are exported public symbols. Adding a brief JSDoc noting that this is a test-only hook helps prevent accidental production use and satisfies the project's documentation guideline.

As per coding guidelines: "Document all public APIs and functions with JSDoc comments".

📝 Proposed JSDoc
+/**
+ * Test-only hook: injects or resets the filesystem/network dependencies used
+ * by remote commands. Pass `null` to restore all implementations to their
+ * real defaults.
+ */
 export function _setRemoteCommandDepsForTest(deps: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/remote.ts` around lines 22 - 34, Add a JSDoc comment above the
exported function _setRemoteCommandDepsForTest stating it is a test-only
dependency-injection hook for replacing filesystem/network/write implementations
in tests, mirroring the style and wording used for _setSshDepsForTest in
src/ssh.ts; include a brief description, list the parameter (deps) and mark the
function as internal/test-only (e.g., `@internal` or `@deprecated` for production
use) so it's clear this is not for production consumption.
tests/unit/skills.test.ts (1)

190-204: ⚡ Quick win

Droid test is missing marker assertions in AGENTS.md

The Droid test only checks content.toContain('- my-skill: A test skill') but omits the <!-- flt:skills:start -->/<!-- flt:skills:end --> marker assertions that the equivalent codex test validates (lines 174–176). Without the markers, cleanupSkills won't be able to strip the block from AGENTS.md, making the test blind to a regression in that path.

✏️ Proposed addition
      const content = readFileSync(join(workDir, 'AGENTS.md'), 'utf-8')
+     expect(content).toContain('<!-- flt:skills:start -->')
+     expect(content).toContain('<!-- flt:skills:end -->')
      expect(content).toContain('- my-skill: A test skill')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/skills.test.ts` around lines 190 - 204, The Droid unit test for
projectSkills is missing assertions that AGENTS.md contains the skill markers,
so update the test (the one calling projectSkills(workDir, droidAdapter, {
requested: ['my-skill'] })) to also assert that the file includes the marker
comments "<!-- flt:skills:start -->" and "<!-- flt:skills:end -->" (the same
markers checked in the codex test) so cleanupSkills can locate and remove the
block; reference the AGENTS.md read via readFileSync and add
expect(content).toContain for both markers around the existing '- my-skill: A
test skill' assertion.
src/skills.ts (1)

261-286: ⚡ Quick win

Missing cleanupSkills test for the Droid adapter

cleanupSkills exercises the same cliName !== 'claude-code' && cliName !== 'opencode' branch for Droid (removing .factory/skills manifest entries and stripping the AGENTS.md marker block), but there is no test that validates this path. The existing cleanup tests cover claude-code and codex only.

Would you like me to draft the missing cleanupSkills test for the Droid adapter?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/skills.ts` around lines 261 - 286, Tests are missing to exercise the
Droid-specific branch in cleanupSkills; add a unit test that sets
adapter.cliName = 'droid' (or constructs an adapter with cliName 'droid') and
verifies cleanupSkills removes .factory/skills manifest entries and strips the
SKILLS_MARKER_START/END block from the instruction file (same behavior covered
for claude-code/codex). Specifically, create fixtures that install skills under
join('.factory','skills') (or emulate install via installAt/selected) and an
instruction file containing the SKILLS_MARKER_START/END block, then call
cleanupSkills and assert that manifests no longer reference the skills and that
the file no longer contains the marker block; reference the cleanupSkills
function, adapter.cliName, SKILLS_MARKER_START/SKILLS_MARKER_END, and installAt
to locate where to hook the test.
src/ssh.ts (1)

14-20: 💤 Low value

_setSshDepsForTest is missing a JSDoc comment

Consistent with the project's documentation guideline and the analogous function in src/commands/remote.ts, this exported test hook should carry a brief JSDoc explaining its test-only purpose.

As per coding guidelines: "Document all public APIs and functions with JSDoc comments".

📝 Proposed JSDoc
+/**
+ * Test-only hook: injects or resets the child-process/filesystem dependencies
+ * used by SSH helpers. Pass `null` to restore all implementations to their
+ * real defaults.
+ */
 export function _setSshDepsForTest(deps: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ssh.ts` around lines 14 - 20, Add a JSDoc comment above the exported test
hook _setSshDepsForTest describing that it is a test-only API to inject
dependencies (execFileSync and statSync implementations) for unit tests, noting
parameters and side effects (it assigns execFileSyncImpl and statSyncImpl) and
that it should not be used in production code; mirror the style and briefness of
the analogous function in src/commands/remote.ts so the project documentation
guideline is satisfied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/commands/remote.ts`:
- Around line 22-34: Add a JSDoc comment above the exported function
_setRemoteCommandDepsForTest stating it is a test-only dependency-injection hook
for replacing filesystem/network/write implementations in tests, mirroring the
style and wording used for _setSshDepsForTest in src/ssh.ts; include a brief
description, list the parameter (deps) and mark the function as
internal/test-only (e.g., `@internal` or `@deprecated` for production use) so it's
clear this is not for production consumption.

In `@src/skills.ts`:
- Around line 261-286: Tests are missing to exercise the Droid-specific branch
in cleanupSkills; add a unit test that sets adapter.cliName = 'droid' (or
constructs an adapter with cliName 'droid') and verifies cleanupSkills removes
.factory/skills manifest entries and strips the SKILLS_MARKER_START/END block
from the instruction file (same behavior covered for claude-code/codex).
Specifically, create fixtures that install skills under
join('.factory','skills') (or emulate install via installAt/selected) and an
instruction file containing the SKILLS_MARKER_START/END block, then call
cleanupSkills and assert that manifests no longer reference the skills and that
the file no longer contains the marker block; reference the cleanupSkills
function, adapter.cliName, SKILLS_MARKER_START/SKILLS_MARKER_END, and installAt
to locate where to hook the test.

In `@src/ssh.ts`:
- Around line 14-20: Add a JSDoc comment above the exported test hook
_setSshDepsForTest describing that it is a test-only API to inject dependencies
(execFileSync and statSync implementations) for unit tests, noting parameters
and side effects (it assigns execFileSyncImpl and statSyncImpl) and that it
should not be used in production code; mirror the style and briefness of the
analogous function in src/commands/remote.ts so the project documentation
guideline is satisfied.

In `@tests/unit/skills.test.ts`:
- Around line 190-204: The Droid unit test for projectSkills is missing
assertions that AGENTS.md contains the skill markers, so update the test (the
one calling projectSkills(workDir, droidAdapter, { requested: ['my-skill'] }))
to also assert that the file includes the marker comments "<!-- flt:skills:start
-->" and "<!-- flt:skills:end -->" (the same markers checked in the codex test)
so cleanupSkills can locate and remove the block; reference the AGENTS.md read
via readFileSync and add expect(content).toContain for both markers around the
existing '- my-skill: A test skill' assertion.

In `@tests/unit/ssh.test.ts`:
- Around line 133-136: Remove the vestigial mock.restore() call in the afterAll
block because there are no mock.module() usages in this test file; update the
afterAll to only call _setSshDepsForTest(null) (leave _setSshDepsForTest as-is)
and remove any reference to mock.restore to avoid confusion about module-level
mocks (locate the afterAll in tests/unit/ssh.test.ts and edit the afterAll that
currently calls mock.restore()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e189c4fa-4c78-4f4b-9dbf-2afd5ab18ed6

📥 Commits

Reviewing files that changed from the base of the PR and between 9e3c5c4 and ce9d184.

📒 Files selected for processing (8)
  • DROID-OAUTH-PROXY.md
  • src/commands/remote.ts
  • src/instructions.ts
  • src/skills.ts
  • src/ssh.ts
  • tests/unit/remote-cmd.test.ts
  • tests/unit/skills.test.ts
  • tests/unit/ssh.test.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant