Skip to main content

vitest-testing

Project-specific Vitest testing reference for the **server** package (apps/server). Use when writing or modifying server-side vitest tests, fixing failing tests, testing DB / socket.io server-side, choosing mocking strategies, or working with async patterns and fake timers. For frontend tests (apps/web) see `frontend-testing`.

Zur Installation springen

Quellinformationen

Repository
recca0120/code-quest
Letzte Quellaktivität
14. Mai 2026 um 03:53
Erkannte Sprache von SKILL.md
Mehrsprachig
Sterne
11
Forks
2

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
vitest-testing
description
Project-specific Vitest testing reference for the **server** package (apps/server). Use when writing or modifying server-side vitest tests, fixing failing tests, testing DB / socket.io server-side, choosing mocking strategies, or working with async patterns and fake timers. For frontend tests (apps/web) see `frontend-testing`.
# Vitest Testing — Server Reference > ⚠️ **This skill is server-focused.** Frontend tests follow `frontend-testing` (Testing Library + > six core principles). This file covers DB / socket.io server-side / CLI process patterns only. Stack: vitest 4.x, TypeScript, ESM, better-sqlite3, socket.io-client --- ## Test Organization - One test file per source file: `foo.ts` → `src/__tests__/foo.test.ts` - Use `describe` to group by class or feature; `it` for individual behaviors - Name tests as behavior assertions: `'creates a session with a unique id'` ```ts describe('MyService', () => { it('returns null when not found', () => { ... }); it('throws when input is invalid', () => { ... }); }); ``` --- ## Setup / Teardown - **`beforeEach`** — use for state that must be fresh per test (DB, server, sockets) - **`beforeAll`** — use only for truly shared, read-only fixtures (e.g. static config) - Always clean up servers and connections in `afterEach` to prevent port leaks **In-memory SQLite pattern (DB tests):** ```ts beforeEach(() => { db = createDatabase(':memory:'); migrate(db, { migrationsFolder }); store = new DrizzleSessionStore(db, sessions); }); ``` **Socket.io integration pattern:** ```ts beforeEach(async () => { httpServer = createServer(); io = new Server(httpServer); handler.register(io); await new Promise<void>((r) => httpServer.listen(0, r)); const { port } = httpServer.address() as AddressInfo; clientSocket = ioc(`http://localhost:${port}`, { transports: ['websocket'] }); await new Promise<void>((r) => clientSocket.on('connect', r)); }); afterEach(async () => { clientSocket.disconnect(); io.close(); await new Promise<void>((r) => httpServer.close(r)); }); ``` --- ## Test Double 選擇(server) 優先選低層級 double(真實實作接近度高 → 測試信心高): | 層級 | 方式 | 用途 | 範例 | |---|---|---|---| | 1 | **真實實作** | 可用 in-memory / 真的 server/socket 時優先 | `:memory:` SQLite、真的 `http.createServer` + `socket.io` server、real repositories | | 2 | **Spy** | 觀察真實方法呼叫但不改行為 | `vi.spyOn(logger, 'warn')` — 方法照跑,可驗證被呼叫 | | 3 | **Fake** | 保留行為的簡化實作(共用 harness) | `createFakeSummoner().claude()` / `FakeSummoner` — 見 `fake-summoner-server` / `fake-summoner-client` skill;in-memory `DrizzleSessionStore` | | 4 | **Stub** | 固定回傳值,無邏輯 | `vi.fn().mockResolvedValue(fixedResult)` — 窄介面、純 query | | 5 | **vi.mock** | 整個模組替換 | 無法 inject 的依賴(`child_process`、`node-pty`、`fs/promises`) | **原則:** - **DB clients / session stores / socket.io** 用真實 in-memory 實例(層級 1),不 mock - **CLI process** 用 `createFakeSummoner().claude()`(層級 3),不自行拼湊 `{ on: vi.fn(), emit: vi.fn() }` - **Protocol 事件** 用 `segments.*()` 產生真實 JSON,不手構 `{ type: 'assistant' } as any` - 用 `vi.spyOn` 務必 `afterEach` 呼叫 `vi.restoreAllMocks()` Client 端 test double 選擇見 `frontend-testing` skill;五型定義見 `test-doubles` skill。 ## Three Test Patterns (choose by scope) ### 1. Global Fake Socket — client→server request/response `setup.ts` mocks `socket.io-client` globally. `createSocket()` returns an EventEmitter-based fake socket with `addHandler`/`setJoinResult` (declared via module augmentation on `Socket`). ```ts import { createSocket } from '../../socket/client'; const socket = createSocket(); socket.addHandler('check_git_status', () => ({ branch: 'main', isClean: true })); // socket.emit('check_git_status', callback) → callback({ branch: 'main', isClean: true }) ``` Built-in handlers: `init`, `session:launch`, `session:join`, `get_session_request`. Override with `addHandler`. Configure join response with `setJoinResult`. **Use for:** hooks/components that call `socket.emit(event, payload, callback)`. **Do NOT use for:** pipeline tests (those need `vi.unmock('socket.io-client')` + real socket.io). ### 2. FakeClaude + Pipeline — CLI→server→client event flow ```ts vi.unmock('socket.io-client'); import { segments as s } from '@code-quest/summoner/test'; import { createFakeSummoner } from '@code-quest/summoner/test'; import { setupComponentPipeline } from '../../test/pipeline-component-harness'; const p = await setupComponentPipeline( createFakeSummoner().claude().initialize(), ); await p.sendMessage('go'); ``` **Use for:** testing CLI stdout → adapter → processRunner → channel → client handler flow. ### 3. Real JSON Fixtures — adapter/protocol parsing ```ts import { segments as s } from '@code-quest/summoner/test'; const line = s.assistant('hello'); const result = adapter.parseLine(line); ``` **NEVER** hand-construct protocol events (`{ type: 'assistant', ... } as any`). Use `segments.*()`. ### Decision Table | Testing what | Pattern | |-------------|---------| | Client calls socket.emit with ack callback | Global fake socket | | CLI event flows through pipeline to client | FakeClaude + pipeline | | Adapter/protocol JSON parsing | Real JSON fixtures | | Pure React component render (no socket) | render / renderWithProviders | | Pure function (no socket, no React) | Direct unit test | ``` --- ## Async Patterns - Wrap event-based APIs in `new Promise<T>((resolve) => emitter.on('event', resolve))` - For timers, use `vi.useFakeTimers()` in `beforeEach` and `vi.useRealTimers()` in `afterEach` - Prefer `vi.advanceTimersByTimeAsync()` over the sync variant to flush microtasks and avoid deadlocks ```ts await vi.advanceTimersByTimeAsync(5000); ``` ### 用 `vi.waitFor` 同步 pipeline,避免 fixed-sleep 測試跑完一個動作(socket send / claude.emit / abort …)需要等 async 副作用落地(event 廣播 / store 寫入 / downstream push),常見陷阱是寫 `await new Promise(r => setTimeout(r, 50))`。這種做法脆弱且慢: - 50ms 只是經驗值,排程抖動時不夠 → flaky - 實際效果早在 <10ms 完成,固定等 50ms 就是浪費 用 `vi.waitFor` 指定**可觀察的後置條件**,事件到就繼續: ```ts // 取代這個: await claude.send('session:fork', { ..., newChannelId: 'fork-verify' }); await new Promise<void>((r) => setTimeout(r, 50)); const row = await sessionStore.getByChannelId('fork-verify'); expect(row).toBeDefined(); // 改成: await claude.send('session:fork', { ..., newChannelId: 'fork-verify' }); await vi.waitFor(async () => { const row = await sessionStore.getByChannelId('fork-verify'); expect(row).toBeDefined(); }); ``` **absence 斷言(「不該發生」)**:用 waitFor 等一個**正向**事件證明 pipeline 已 flush,再 assert 目標事件不存在: ```ts await vi.waitFor(() => { expect(windowB.events('message:assistant').length).toBeGreaterThan(0); }); expect(windowB.events('chat:cancel_request')).toHaveLength(0); ``` **不適用情境** — 沒 observable signal 的 cleanup(例:`handle.abort()` 後只有 internal `for await` 結束,對 socket 無推播;或「N 維持不變」的 absence over a window)— 只能保留 fixed wait + 註解說明為何。 ### 避免 `setTimeout(fn, 0)` / 雙 `queueMicrotask` 等待 要把「目前所有 pending microtask + I/O 回呼」flush 完畢,用 `setImmediate`: ```ts // 不要: await new Promise((r) => setTimeout(r, 0)); await new Promise<void>((r) => queueMicrotask(() => queueMicrotask(r))); // 改成: await new Promise<void>((r) => setImmediate(r)); ``` `setImmediate` 排在「下一個 I/O 階段」,自然在所有 pending microtask / promise rejection handler 之後執行。語意比「猜要兩個 microtask」清楚。 ### Fake timers + `userEvent` 的衝突 `vi.useFakeTimers()` 連 microtask scheduling 都換掉時,`userEvent` 內部的 promise/microtask wait 會 deadlock。對策: - 限制 fake 範圍:`vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })` 只接管 setTimeout,userEvent 仍有實 microtask 可用 - 仍 deadlock → 改用 `fireEvent`(同步、不依賴 timer): ```ts // user.click → fireEvent.click fireEvent.click(button); // user.type 'hi{Enter}' → fireEvent.change + keyDown fireEvent.change(input, { target: { value: 'hi' } }); fireEvent.keyDown(input, { key: 'Enter' }); ``` --- ## Isolation and Parallel Execution - Vitest runs files concurrently by default; each file has its own module scope - `:memory:` SQLite databases are isolated per file naturally - Use random ports (`listen(0)`) for HTTP servers — never hardcode a port - Avoid shared mutable state at module level; initialize everything in `beforeEach` --- ## Performance Tips - Use `beforeAll` for expensive read-only setup (migrations on a shared DB snapshot) - Prefer `beforeEach` with `:memory:` DBs — SQLite in-memory creation is fast enough - Use `test.concurrent` only when tests are provably independent and stateless - Keep individual test files focused; split large files rather than using `.skip` --- ## Common Pitfalls - `vi.mock()` is hoisted — it runs before imports, making it hard to control per-test; prefer `vi.spyOn` - Forgetting `afterEach` cleanup on HTTP servers causes `EADDRINUSE` in subsequent test runs - Using `advanceTimersByTime` (sync) with promises causes hanging tests — use the `Async` variant - Hooks inside `describe` only apply to that block; global `beforeEach` applies to the entire file - ESM + `vi.mock`: factory functions must not reference variables defined outside the factory (temporal dead zone) --- ## Vitest v4 Breaking Changes > Reference: https://vitest.dev/guide/migration.html ### poolOptions 移除(v4 breaking change) `poolOptions` 已移除,改為頂層選項: ```ts // v3 poolOptions: { threads: { isolate: false, maxThreads: 2, minThreads: 2 }, } poolOptions: { forks: { singleFork: true }, } // v4 isolate: false, maxWorkers: 2, minWorkers: 2, // singleFork: true → maxWorkers: 1 ``` ### Arrow Function 不能作為 Constructor Mock v4 中用 `new` 呼叫的 mock,其實作不能是 arrow function: ```ts // v3(會 warn 但能跑) vi.fn(() => ({ id: 'mock' })) // v4(TypeError: () => ({}) is not a constructor) vi.fn(() => ({ id: 'mock' })) // ❌ // v4 正確寫法 vi.fn(function () { return { id: 'mock' }; }) // ✅ // 或 vi.fn(function () { this.id = 'mock'; }) // ✅ ``` ### Type Signature Changes | v1/v3 | v4 | |-------|----| | `vi.fn<TArgs extends unknown[], TReturn>()` | `vi.fn<T extends (...args: any[]) => any>()` — pass the **full function type** | | `Mock<TArgs, TReturn>` | `Mock<T>` — same shape, one type param | ```ts // v3 const fn = vi.fn<[string, number], boolean>(); // v4 const fn = vi.fn<(s: string, n: number) => boolean>(); ``` ### Spy / Mock Behaviour - **`getMockName()`** now returns `'vi.fn()'` instead of `'spy'` when no name was set — update any snapshot / assertion that matches the old string. - **`vi.restoreAllMocks()`** only restores **manually created spies** (`vi.spyOn`). Automock replacements are **not** restored by this call; use `vi.resetModules()` or re-import if needed. - **`mock.invocationCallOrder`** now starts at **1** (was 0). Assertions comparing the raw index need +1 adjustment. ### New API - **`mock.settledResults`** — array of `{ type: 'fulfilled' | 'rejected', value }` entries for each call, mirroring `mock.results` but for async return values after settlement.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen