write-tests
Installation
SKILL.md
Write tests
Where tests live
- Next to the code:
src/lib/refs.ts→src/lib/refs.test.ts. A command group has one test file for the group or one per subcommand (src/commands/label/label.test.ts,src/commands/project/reorder.test.ts). Vitest, globals on,restoreMocks: true, so spies restore themselves: no manual.mockRestore(). - Command tests mock the SDK client:
vi.mock('../../lib/api/core.js', () => ({ getApi: vi.fn() }))at the top,mockApi = setupApiMock()inbeforeEach, a program fromcreateTestProgram(registerXxxCommand), thenprogram.parseAsync(['node', 'td', ...]). Capture output withcaptureConsole()orcaptureStream(). Readsrc/commands/label/label.test.tsfor the shape. src/index.integration.test.tsbuilds the CLI and runsdist/index.js, for argv handling, global flags and JSON errors end to end.vi.mock('chalk')uses the manual mock insrc/__mocks__/chalk.ts, which makes styling a passthrough so tests read plain text.- Agent tooling tests live in
.agents/scripts/and run copies of the tools in throwaway repos (.agents/scripts/lib/test-repo.ts).
Helpers
From src/test-support/: createMockApi, setupApiMock, fixtures, makeFilter, createProjectProgram. From @doist/cli-core/testing: createTestProgram, captureConsole, captureStream, describeEmptyMachineOutput. Build entities from fixtures instead of by hand; extend it when a test needs a new shape.
The quality bar
Read references/test-rules.md before writing or reviewing a test. In short: a test counts only if it can fail. It asserts outcomes, takes expected values from outside the code under test, mocks only real boundaries, and you saw it fail once.
Done when each new test has failed once for the right reason, and then passes.