Skip to main content
ClaudeWave
Skill349 estrellas del repoactualizado today

mock-to-internals-migration

This skill automates migration of test files from Node.js module mocking to dependency injection seams. Use it when a test file contains `mock.module()` calls for built-in modules like `node:child_process` or `node:fs` and the production code has or needs an `_internals` export. The migration eliminates brittle mocking patterns and avoids Windows EBUSY errors that can occur with `mock.restore()` on asynchronous file operations. Follow the step-by-step protocol to expose target functions through the `_internals` object, replace all direct calls with `_internals` references, and update tests to inject stubs instead of mocking modules globally.

Instalar en Claude Code
Copiar
git clone --depth 1 https://github.com/zaxbysauce/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-migration
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.md

# mock.module → _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
- 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 = { ... };
   ```
4. Identify which function/method the mock.module was intercepting (usually `spawnSync`, `execFileSync`, `readFileSync`, etc.)

## 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.

```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 | undefined;

// 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)
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['apply', ...]);

// AFTER (with _internals seam)
const gitCalls = spawnCallLog.filter(c => c.cmd === 'git');
expect(gitCalls.length).toBeGreaterThan(0);
```

## Step 3 — Verify no mock.module remains

```bash
grep -n "mock.module" <your-test-file>
```
Must return no matches.

## Step 4 — Run tests

```bash
bun --smol test src/tools/__tests__/mutation-test.adversarial.test.ts --timeout 30000
```

## Step 5 — Check for helpers that bypass the seam

Some test helpers (like `initGitRepo`) may use `require('node:child_process')` directly to bypass the mock. This is INTENTIONAL and CORRECT — they need the real subprocess to set up test fixtures.

```typescript
function initGitRepo(dir: string): void {
  const { spawnSync: s } = require('node:child_process');
  s('git', ['init'], { cwd: dir });
}
```

Do NOT change these helpers to use the DI seam.

## Common mistakes

| Mistake | Why it fails |
|---------|-------------|
| Forgetting `mockReset()` in `afterEach` | `mockImplementation` state leaks between tests |
| Using `type SpawnSyncFn` import from `node:child_process` | Type export doesn't exist at runtime; build fails |
| Forgetting to restore `_internals.spawnSync` | Subsequent tests or other files get