一键导入
testing
Vitest mock patterns (vi.mock, vi.hoisted, vi.fn reset), TDD planning rules, and general test strategy. Load when writing or debugging tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Vitest mock patterns (vi.mock, vi.hoisted, vi.fn reset), TDD planning rules, and general test strategy. Load when writing or debugging tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Pre-completion protocol for implementation agents — gather context, dispatch the pre-completion-reviewer subagent, and handle its report before writing stage notes and recommending /ship-issue. Load at the end of /tdd-plan and /build-plan after all implementation steps are complete.
Package-specific context for @gotgenes/pi-permission-system. Load when working on code, tests, or docs in packages/pi-permission-system/.
TypeScript conventions, code design principles (SOLID, self-documenting code, file organization), structural design heuristics (dependency width, LoD, output arguments), pnpm rules, ES2024 target, Pi SDK patterns, and Biome/ESLint conflict workarounds. Load during implementation, refactoring, or code review.
Review a module's dependency and structural patterns for code smells. Use when adding a parameter to a shared interface, when a dependency bag grows past 5 fields, or when planning a refactoring that touches wiring between layers.
Heuristics and process for discovering structural improvements in a package. Load when planning a new improvement round — contains the smell taxonomy, analysis workflow, and prioritization framework distilled from 10 phases of pi-subagents refactoring.
Project-specific markdown rules (one-sentence-per-line, compact tables, sequential numbering) and YAML frontmatter schema for plans/retros. Load when writing or editing markdown — contains rules that differ from standard markdownlint defaults.
| name | testing |
| description | Vitest mock patterns (vi.mock, vi.hoisted, vi.fn reset), TDD planning rules, and general test strategy. Load when writing or debugging tests. |
Load this skill when writing, debugging, or planning tests.
vi.mock(), extract each vi.fn() stub to a module-scope variable and reset it in beforeEach — vi.restoreAllMocks() only operates on vi.spyOn() spies, not on vi.fn() instances.
Use .mockReset() when the stub has no default implementation.
Use .mockClear() when the vi.mock() factory provides a default implementation that tests must preserve.vi.mock() factory references a module-scope vi.fn() stub, wrap the stub declaration in vi.hoisted() — Vitest hoists vi.mock() above normal declarations, so unhoisted variables are undefined when the factory runs.vi.mock(), use vi.fn() with no implementation — not vi.fn(() => ({})).
Arrow-function implementations are not constructable; new MockClass() throws "is not a constructor".node:* built-in modules with vi.mock(), include a default key mirroring the named exports — omitting it causes "No default export defined on the mock" errors.import, not a per-test await import(...) — Vitest hoists vi.mock()/vi.hoisted() above static imports, so the mock still applies.
A per-test dynamic import of a module that transitively pulls heavy deps pays the transform/resolve cost inside each test's testTimeout window and can flake CI (Refs #554).vi.fn() factory returns an empty array or narrow literal, annotate its return type explicitly — vi.fn((): string[] => []), not vi.fn(() => []).
Without the annotation TypeScript infers never[], and subsequent mockReturnValueOnce([...]) calls fail with “not assignable to never”.
Use import type to pull domain types (e.g., AgentConfig, PreloadedSkill) for the annotation.Mock<specific-signature> — e.g., Mock<() => void>, Mock<(arg: string) => Promise<void>>.
Do not use ReturnType<typeof vi.fn> — in Vitest v4 it expands to Mock<Procedure | Constructable>, a union that TypeScript cannot call.RunnerIO, AssemblerIO), do not annotate the return type with that interface — the annotation erases Mock<...> methods (mockResolvedValue, mock.calls, etc.) from the inferred type.
Leave the return type unannotated so callers retain full mock access.createSubagentSession(params, deps)), add typed implementations to every vi.fn() stub — vi.fn((_param: Type): ReturnType => default), not vi.fn().mockReturnValue(default).
Bare vi.fn() and chained .mockReturnValue() produce Mock<Procedure> which is not assignable to specific function signatures.Partial<ProductionInterface>, the spread { ...defaults, ...overrides } creates a union type that also erases mock methods.
Either remove the Partial<ProductionInterface> annotation (let TypeScript infer from the spread) or drop the overrides parameter and configure mocks on the returned object directly.?? to supply defaults from an overrides object, explicit undefined values are swallowed.
Use "key" in overrides presence checks or Object.hasOwn(overrides, "key") for fields where undefined is a meaningful test value.as unknown as X cast from a mock, the type checker starts verifying mockReturnValue payloads too, not just method presence.
Incomplete return-value literals the cast used to mask (e.g. { state: "allow" } for a full PermissionCheckResult) fail pnpm run check; build them with the shared make* fixture builder instead.setInterval, never use vi.runAllTimersAsync() — it loops infinitely.
Use vi.advanceTimersByTimeAsync(ms) with a specific duration instead.process.env inside functions rather than capturing it as a module-level constant — vi.stubEnv() alone cannot change a constant already evaluated at import time.
If a module-level constant is unavoidable, test it with vi.resetModules() + await import(...) inside the test body, and call vi.unstubAllEnvs() + vi.resetModules() in afterEach.toBe, toEqual) over subset matchers (toContain, toMatchObject, expect.objectContaining).
Weak assertions hide unexpected values and make tests less useful as documentation.
When a weak assertion is necessary (third-party output, non-deterministic ordering), add a comment explaining why.denied expectation both yield { approved: false, state: "denied" }), a broken fixture false-greens the negative test.
Assert the positive (non-fallback) path against the same fixture builder first — a malformed fixture then fails loudly there — or assert a discriminating field the fallback cannot produce.test.todo.
A real assertion documents the limitation and lets a future fix flip the expectation.test.fails to document the expected behavior and file a GitHub issue.void 0;, unused locals) in tests just to make an Edit tool's oldText unique — widen oldText with surrounding context instead.async method declared Promise<T> must signal a precondition failure, return Promise.reject(new Error(...)), not throw — a synchronous throw escapes expect(...).rejects.toThrow(...), and switching to async to fix that trips @typescript-eslint/require-await when the body has no await.expect(fn).toHaveBeenCalledWith(...), not fn.mock.calls[0]![0].
A typed vi.fn<(a: string) => void>() makes the call tuple non-optional, so the ! trips @typescript-eslint/no-unnecessary-type-assertion.Group tests by the behavior or concern they exercise — open a nested describe("<concern>", () => { ... }) per concern rather than appending it blocks to a flat list.
When adding tests for a new concern (e.g. a details field alongside existing content assertions), start a new describe block instead of extending the existing one.
When consolidating duplicated test arrangements, group the shared setup in a describe-scoped beforeEach and keep the act (the call under test) explicit in each test.
Do not wrap the system-under-test call in a helper to eliminate a duplication-metric clone — the repeated act is the test subject, not duplication to remove.
Vitest uses esbuild and does not typecheck.
Run pnpm run check (tsc --noEmit) for type-only changes.
Confirm any claim about what a module exports with tsc, not a runtime symptom.
A missing export throws is not a function at runtime but surfaces as TS2305 under tsc (e.g. #446, a runtime error misread as a types/runtime mismatch).
pnpm --filter @gotgenes/<pkg> exec vitest run <test-path> — plain pnpm vitest run fails at the repo root (Command "vitest" not found).pnpm --filter @gotgenes/<pkg> exec vitest runprefer-nullish-coalescing flags ||, check whether the left side could be a falsy non-null value ("", 0, false) that the code intentionally converts to the fallback.
If so, keep || and add // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts falsy values to fallback.
The rule also flags the equivalent x ? x : y ternary, so do not reach for a ternary to dodge it — use x || y with the disable.
Do not mechanically replace || with ?? without verifying test expectations.process.platform), it bans the text everywhere — including = process.platform default parameters.
Every such default must be removed in the guard's commit, which makes the param required and cascades to all callers, so enumerate every occurrence and caller at plan time rather than a representative subset (Refs #510).pnpm run check immediately after that step's commit.index.ts), the step must include updating that call site — the type checker will not allow the interface change and the call-site update to land in separate commits.pnpm run check immediately after that step to catch config issues before subsequent steps depend on the infrastructure.checkTool(), not callee(arg, "literal" — a single-line pattern misses multi-line invocations where args span continuation lines, undercounting scope (Refs #504).src/ files that reference the removed field — every file that reads or passes the field must update in the same step.
This is the inverse of the excess-property rule: TypeScript rejects reading a property that no longer exists on the type.extends or intersection chain, grep for types that compose it (extends <Interface>, <Interface> &) — intersection mock supertypes (e.g. MockGateHandlerSession) silently lose the removed members and break at the construction site, not the type definition."win32" string was expected takes the non-win32 branch).
Confirm the red lives in a test that exercises the new behavior, not just the new signature.session_start), grep all test files for consumers that resolve it — not just the tests you already plan to touch.
A timing change breaks them at runtime (the full suite), not at typecheck, so pnpm run check will not flag them.tsc passes either way; only a cross-consumer runtime test exercising both the producer and the consumer catches the mismatch.await (if (x) await f()) into an always-async helper, the no-op path gains a microtask boundary it did not have.
Tests asserting synchronous ordering (e.g. a factory called in the same tick as spawn()) break at runtime, not typecheck.
Keep a synchronous guard at the call site (if (bracket.hasProvider()) await bracket.prepare(…)) to preserve the fast path.Config into { identity, execution }), grep test factories for Partial<OldInterface> spread patterns.
Top-level ...overrides does not deep-merge — flat-key overrides like { description: "my task" } silently become no-ops when the field moves into a nested sub-object.
Either replace each call site with the full nested sub-object or switch to a deep-merge helper.{ ...variable spread patterns in tests — spreading a class instance produces a plain object that lacks the class's methods and private fields.
Replace with createTestX({ ...overrides }) factory calls or direct field mutation.noUnusedImports is warning-level (exit 0), so pnpm run lint stays green and the pre-completion reviewer is the only backstop.@deprecated — @typescript-eslint/no-deprecated fires on every surviving call site at commit time; use a prose comment instead.