mock-to-internals-migration
This skill provides a migration protocol for replacing unreliable `mock.module()` and `vi.spyOn()` patterns with dependency injection through `_internals` exports in production code. Use this when test files mock Node.js modules like `child_process` or `fs`, the production code has an `_internals` export available, and tests need reliable mocking across Bun's shared test runner while avoiding Windows EBUSY conflicts from `mock.restore()` calls.
git clone --depth 1 https://github.com/ZaxbyHub/opencode-swarm /tmp/mock-to-internals-migration && cp -r /tmp/mock-to-internals-migration/.opencode/skills/generated/mock-to-internals-migration ~/.claude/skills/mock-to-internals-migrationSKILL.md
# mock.module / vi.spyOn → _internals DI Seam Migration Protocol
Follow every step in order. Do not skip steps.
## When to use this skill
- A test file uses `mock.module('node:child_process')` or `mock.module('node:fs')` or similar
- A test file uses `vi.spyOn(module, 'functionName')` on a module's exported functions (same anti-pattern as `mock.module` — unreliable across Bun's shared test runner when the module is split or functions move)
- The production code already has an `_internals` export (or needs one added)
- The goal is to eliminate `mock.module` usage per AGENTS.md invariant 7 and the writing-tests skill
**Benefit:** This migration also sidesteps the Windows EBUSY risk documented in the writing-tests skill — tests using `_internals` seams do not need `mock.restore()`, which on Windows can conflict with async child process handles.
## Step 0 — Identify the target module and its _internals seam
1. Find where the test imports from:
```typescript
import { _internals as engineInternals } from '../../mutation/engine.js';
```
2. Read the source module (e.g., `src/mutation/engine.ts`)
3. Check if `_internals` already exists:
```typescript
export const _internals = {
existingHelper,
// other entries...
};
```
4. Identify which function/method the mock.module was intercepting (usually `spawnSync`, `execFileSync`, `readFileSync`, etc.)
## Step 0b — Check ALL test files consuming the target module
**CRITICAL:** Before converting any test file, identify EVERY test file that imports or spies on the target module. Cross-file mock leakage in Bun's shared test runner means a migration in one file can silently break tests in another file.
```bash
# Find all test files that import the module
grep -rln "from.*<source-module>" src/ tests/ --include="*.test.ts"
# Find all test files that spy on the module
grep -rln "vi.spyOn.*<source-module>" src/ tests/ --include="*.test.ts"
grep -rln "spyOn.*<source-module>" src/ tests/ --include="*.test.ts"
# Find all test files that mock the module
grep -rln "mock.module.*<source-module>" src/ tests/ --include="*.test.ts"
```
Prefer the `imports` tool for comprehensive discovery — grep misses dynamic imports and re-exports.
Record every file found. ALL of them must pass after migration — not just the one being explicitly converted.
## Step 1 — Add the function to _internals in the source file
**CRITICAL:** Do NOT import type aliases from Node.js built-ins (e.g., `type SpawnSyncFn` from `node:child_process`). Use `typeof` instead. This applies to ALL Node built-in functions, not just spawnSync.
```typescript
// BAD — fails build
import { spawnSync, type SpawnSyncFn } from 'node:child_process';
// GOOD — works at build time
import { spawnSync } from 'node:child_process';
type SpawnSyncFn = typeof spawnSync;
```
Add the function to `_internals`:
```typescript
export const _internals: {
executeMutation: typeof executeMutation;
computeReport: typeof computeReport;
executeMutationSuite: typeof executeMutationSuite;
spawnSync: SpawnSyncFn; // ← ADD THIS
} = {
executeMutation,
computeReport,
executeMutationSuite,
spawnSync, // ← ADD THIS
} as const;
```
> **Note:** The explicit type annotation on `_internals` (the `{ ... }` shape on the left side of `=`) overrides `as const` readonly inference. If you omit the explicit type annotation, `as const` will make properties `readonly` and test injection (`engineInternals.spawnSync = mockSpawnSync`) will fail at TypeScript compile time. Always include the explicit type annotation.
Replace all direct calls in the module with `_internals.spawnSync(...)`:
```typescript
// BEFORE
const result = spawnSync('git', ['apply', patchFile], { cwd: workingDir });
// AFTER
const result = _internals.spawnSync('git', ['apply', patchFile], { cwd: workingDir });
```
**Verify:** Run `bun run build` to ensure the type alias compiles.
## Step 2 — Convert the test file
### 2a. Remove mock.module block
Delete the entire `mock.module(...)` block and any related mock setup.
### 2b. Add module-level variables for save/restore
```typescript
// Module-level reference to the original function (saved/restored in beforeEach/afterEach)
let originalSpawnSync: typeof import('node:child_process').spawnSync;
// Module-level mock that logs calls and delegates to original
const mockSpawnSync = mock(
(cmd: string, args: string[], opts: Record<string, unknown>) => {
spawnCallLog.push({ cmd, args, opts: { ...opts } });
if (originalSpawnSync) {
return originalSpawnSync(cmd, args, opts);
}
return {
pid: 12345,
output: Buffer.alloc(0),
stdout: Buffer.from('ok'),
stderr: Buffer.alloc(0),
status: 0,
signal: null,
error: undefined,
} as ReturnType<typeof import('node:child_process').spawnSync>;
},
);
```
### 2c. Update beforeEach
```typescript
beforeEach(() => {
// Save original and replace with mock
originalSpawnSync = engineInternals.spawnSync;
engineInternals.spawnSync = mockSpawnSync;
spawnCallLog.length = 0;
tempDir = makeTempDir();
});
```
### 2d. Update afterEach
**CRITICAL:** Must include ALL of these in order:
1. Restore original function
2. Call `mockReset()` to clear mockImplementation state
3. Clean up temp directories
4. Clear call logs
```typescript
afterEach(() => {
// 1. Restore original
engineInternals.spawnSync = originalSpawnSync;
// 2. Reset mock implementation to prevent leak between tests
mockSpawnSync.mockReset();
// 3. Clean up temp directory
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
}
// 4. Clear call log
spawnCallLog.length = 0;
});
```
**WARNING:** Omitting `mockReset()` causes `mockImplementation` state from one test to leak into the next test's active window.
### 2e. Update test assertions
Replace assertions that checked `mockSpawnSync.mock.calls` with assertions that check `spawnCallLog`:
```typescript
// BEFORE (with mock.module>
Run a rigorous, quote-grounded codebase review or security/QA/accessibility/performance/AI-slop/enhancement audit. Use for full-repo or large-subsystem review reports; not for normal implementation. Performs Phase 0 inventory, selected exhaustive tracks with non-diluting depth, coverage closure, reviewer/critic validation, and writes .swarm/review-v8 artifacts without modifying source files.
>
>
Use when asked to trace, investigate, root-cause, plan, fix, close, or prepare a PR for a GitHub issue or bug report. Runs an evidence-first issue workflow: GitHub intake, reproduction, reasoning-guided localization, no-gap fix planning, independent critic review, user approval gate, implementation, tests, and PR-ready closure.
>
>