Skip to content

fix(test): stop mock.module() leaks from poisoning the unit test suite - #21

Merged
Rwanbt merged 3 commits into
devfrom
fix/ci-mock-module-leak
Jul 30, 2026
Merged

fix(test): stop mock.module() leaks from poisoning the unit test suite#21
Rwanbt merged 3 commits into
devfrom
fix/ci-mock-module-leak

Conversation

@Rwanbt

@Rwanbt Rwanbt commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Issue for this PR

Closes #

Type of change

  • Bug fix

What does this PR do?

Root-causes and fixes the unit (linux)/unit (windows) full-suite cascade failure (600+ unrelated tests failing with TypeError: undefined is not an object (evaluating 'Auth.Service') / Provider.Service).

Three test files (provider-discovery.regression.test.ts, provider-discovery.integration.test.ts, provider-discovery.bench.test.ts) used mock.module() to replace the entire Provider/Auth/node:fs/promises/node:child_process modules instead of preserving their real exports. Two compounding effects:

  1. Provider/Auth lost their .Service Effect tag (not just .list/.all). Config.layer/ShareNext.layer memoize their construction process-wide via a shared MemoMap (src/effect/run-service.ts). If that first construction ran while the mock was live, the resulting undefined got permanently cached — cascading into hundreds of unrelated failures in later files for the rest of the process.
  2. The node:fs/promises mock's readFile ignored its path argument, so any unrelated file read elsewhere in the process during the mock window got this fixture's fake content back (observed corrupting test/session/llm.test.ts's JSON fixture loader).
  3. resetMocks() itself restored from the same live ESM namespace bindings it was trying to undo — since those bindings reflect whatever mock.module() currently has active, this could re-register already-mocked state instead of the pristine original.

Fixed by capturing the real exports as frozen plain-object snapshots before any test in each file mocks anything, and scoping the fs/promises override to the exact credential-file path it simulates (~/.claude/.credentials.json) instead of intercepting every read.

Also included (found while verifying CI end-to-end):

  • storybook build was OOMing on ubuntu-latest (less RAM than the Blacksmith runner it used to run on) — bumped NODE_OPTIONS=--max-old-space-size.
  • 3 Team run-control OpenAPI operations (team.pauseRun/resumeRun/cancelRun) were missing a description, flagged by openapi-compat.test.ts. Added descriptions and regenerated the SDK.

How did you verify your code works?

  • Reproduced the cascade locally (bun test in packages/opencode, full suite), bisected to the exact test files and lines.
  • After the fix: full local suite run clean of the cascade (0 occurrences of Auth.Service/Provider.Service/leaked-fixture errors across 3 consecutive full runs).
  • Confirmed the 2 remaining local-only failures are pre-existing, unrelated, and already pass on real CI (cross-checked against the actual unit (linux) GitHub Actions log: both pass in 14-17ms there — they're local-machine artifacts, not regressions).
  • bun turbo typecheck: 15/15 green.
  • Regenerated packages/sdk/openapi.json + sdk.gen.ts, diff is exactly the 3 added descriptions.

Screenshots / recordings

N/A (test infra + CI workflow fix, no UI change).

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Three test files replaced the entire Provider/Auth/fs-promises/child_process
modules via mock.module() instead of preserving their real exports. Since
ESM namespace imports are live bindings, this had two compounding effects for
the rest of the bun test process:

- Provider/Auth lost their .Service Effect tag (not just .list/.all), and
  Config.layer/ShareNext.layer memoize their build process-wide (a shared
  MemoMap in src/effect/run-service.ts) — so the first construction that ran
  while the mock was live got permanently cached as broken, cascading into
  600+ unrelated failures in later files.
- The fs/promises readFile mock ignored its path argument, so any unrelated
  file read elsewhere in the process during the mock window got this
  fixture's content back (observed corrupting test/session/llm.test.ts's
  JSON fixture loader).
- resetMocks() itself restored from the same live bindings it was trying to
  undo, which could re-register already-mocked state instead of the original.

Fixed by capturing real exports as frozen plain-object snapshots before any
mocking happens, and scoping the fs/promises override to the exact path it
simulates instead of intercepting every read.

Also: bump storybook build's V8 heap (ubuntu-latest has less RAM than the
Blacksmith runner this workflow used to run on) and add missing
summary/description to 3 Team run-control OpenAPI operations that an
unrelated compat test flags as undocumented (SDK regenerated to match).
@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • No issue referenced. Please add Closes #<number> linking to the relevant issue.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

MM2-B02-WORKER added 2 commits July 30, 2026 12:16
…ers mock

Masked until now by the storybook build OOMing before it got this far: the
mock only re-implemented useProviders(), not the standalone popularProviders
array that src/hooks/use-providers.ts also exports and that
team-model-selector.tsx imports directly.
"C:/nonexistent/..." is a Windows-style path, not absolute on POSIX, so on
Linux CI it hit PATH_NOT_ABSOLUTE (a different, already-covered validation
path) instead of the intended PATH_NOT_DIRECTORY/GIT_COMMAND_FAILED. Use
tmpdir(), which is absolute on every platform.
@Rwanbt
Rwanbt merged commit e21b738 into dev Jul 30, 2026
15 checks passed
@Rwanbt
Rwanbt deleted the fix/ci-mock-module-leak branch July 30, 2026 10:57
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