From 8f73cfdf9d1fb5fdc9574f51c79cf64d10d36aa3 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Fri, 13 Feb 2026 22:03:29 +0800 Subject: [PATCH] docs: add strict mock policy and testing guidelines to CLAUDE.md Establish "external-only mock" rule: only third-party dependencies may be mocked in tests. Internal modules must use real implementations with temp directories, reset functions, or dependency injection. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b08b1e2d..dc3fcc53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -201,6 +201,74 @@ git add README.md git commit -m "docs: update API documentation" ``` +## Testing Guidelines + +### Mock Policy: External Only + +**CRITICAL RULE**: Only mock third-party/external dependencies. NEVER mock internal modules. + +| Type | Examples | Can Mock? | +|------|----------|-----------| +| Internal modules | `./runner.js`, `../utils/format.js` | NO | +| Monorepo packages | `@multica/core`, `@multica/utils` | NO | +| Third-party packages | `openai`, `@anthropic-ai/sdk`, `@mariozechner/*` | YES | +| System/time APIs | `vi.useFakeTimers()`, `vi.setSystemTime()` | YES | +| Network calls | External HTTP requests, WebSocket connections | YES | + +When AI writes code, tests become more valuable than the code itself. Mocking internal modules creates brittle tests that don't verify real integration between modules, hides bugs, and requires maintaining parallel mock implementations. + +### Preferred Patterns + +**Temp directories for I/O tests** (no filesystem mocking): +```typescript +const testDir = join(tmpdir(), `multica-test-${Date.now()}`); +beforeEach(() => mkdirSync(testDir, { recursive: true })); +afterEach(() => rmSync(testDir, { recursive: true, force: true })); +``` + +**Test reset functions for stateful modules**: +```typescript +// In the module itself: +export function resetForTests() { /* clear in-memory state */ } + +// In tests: +beforeEach(() => resetForTests()); +``` + +**Pure function tests** — no mocking needed: +```typescript +const result = resolveContextWindowInfo({ modelContextWindow: 100_000 }); +expect(result.tokens).toBe(100_000); +``` + +**Constructor/parameter injection** over module mocking: +```typescript +// Good: pass baseDir as parameter +const session = new SessionManager({ sessionId: "test", baseDir: testDir }); + +// Bad: mock the paths module +vi.mock("../../shared/paths.js", () => ({ DATA_DIR: "/tmp/test" })); +``` + +### Anti-Patterns + +- `vi.mock("./internal-module.js")` — NEVER mock internal modules +- Mock objects with 10+ method stubs — sign you should use the real implementation +- `vi.mock("../context-window/index.js")` with simplified logic — hides real behavior +- Tests that pass but don't exercise any real code paths ("fake green") + +### Reference Tests + +Good patterns to follow: +- `packages/core/src/agent/session/session-manager.display.test.ts` — real SessionManager + temp dirs +- `packages/core/src/agent/skills/loader.test.ts` — real skill loading + temp filesystem +- `packages/core/src/agent/context-window/guard.test.ts` — pure function tests +- `packages/core/src/agent/subagent/registry.test.ts` — real registry + `resetSubagentRegistryForTests()` + +Known violations (to be migrated): +- `packages/core/src/agent/async-agent.test.ts` — mocks internal `./runner.js` +- `packages/core/src/agent/session/compaction.test.ts` — mocks internal `../context-window/index.js` + ## Pre-push Checks Before pushing, always run: