Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Writes Vitest tests following project patterns: __tests__/ directories, vi.mock() for module mocking with vi.hoisted() for test-time factories, global LLM mock from src/test/setup.ts, environment variable save/restore in beforeEach/afterEach, vi.clearAllMocks() lifecycle, and test file organization. Use when user says 'write tests', 'add test coverage', 'test this', creates *.test.ts files, or when test failures appear in CI. Do NOT use for non-test code or for debugging without writing tests.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 188% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 119% | 0% |
__tests__/ directories parallel to source files: src/[module]/__tests__/[module].test.ts__tests__/ directories in vitest.config.ts's include glob (already configured: src/**/*.test.ts)src/test/setup.ts — it is the global LLM provider mock already applied to all testsprocess.env in beforeEach, restore in afterEach, and explicitly delete env vars to test absenceafterEach, not in individual test cleanup. Use fs.rmSync(dir, { recursive: true, force: true })vi.unmock('../module.js') BEFORE the import statementpnpm test locally and pnpm test:coverage before committing to verify coverage thresholds (lines: 50, functions: 50, branches: 50, statements: 50)Create src/[module]/__tests__/[module].test.ts. The parent source file is src/[module]/[module].ts.
Verify: The __tests__ directory exists at the same level as the source file being tested.
At the top of every test file, import from vitest:
typescriptimport { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Add additional imports based on what you're testing:
import fs from 'fs'; import path from 'path'; import os from 'os';fs.mkdtempSync(path.join(os.tmpdir(), 'caliber-prefix-'))import { execSync } from 'child_process';Verify: All required test utilities are imported before test definitions.
If testing a module that imports other modules you want to mock:
typescriptvi.mock('../config.js', () => ({ loadConfig: vi.fn(), writeConfigFile: vi.fn(), }));
For complex mocks with test-time factory functions (hoisted):
typescriptconst { mockLoadConfig } = vi.hoisted(() => ({ mockLoadConfig: vi.fn(), })); vi.mock('../config.js', () => ({ loadConfig: () => mockLoadConfig(), }));
For unmocking global setup mocks (e.g., to test llm/index.js itself):
typescriptvi.unmock('../index.js');
Place all vi.mock() and vi.unmock() calls BEFORE importing the module under test.
Verify: Mock declarations appear before the import of the module being tested.
Group related tests with describe():
typescriptdescribe('functionName', () => { it('returns X when Y', () => { // test body }); });
Verify: Each it() test has a clear, complete assertion.
For tests that modify process.env or process.argv:
typescriptdescribe('config tests', () => { const originalEnv = process.env; const originalArgv = process.argv; beforeEach(() => { process.env = { ...originalEnv }; // Copy, not reference process.argv = [...originalArgv]; delete process.env.SPECIFIC_VAR; // Explicitly remove vars to test absence }); afterEach(() => { process.env = originalEnv; process.argv = originalArgv; }); it('tests env var behavior', () => { process.env.MY_VAR = 'test'; // test code }); });
Verify: beforeEach creates a copy of env/argv; afterEach restores originals; unused env vars are explicitly deleted with delete process.env.VAR.
For file system tests, create temporary directories and clean them up:
typescriptdescribe('file tree', () => { const dirs: string[] = []; afterEach(() => { for (const d of dirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } dirs.length = 0; }); it('processes files', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'caliber-test-')); dirs.push(tmp); // use tmp directory }); });
Verify: All temporary directories are pushed to a cleanup array and removed in afterEach.
Before each test, clear mock call history to avoid pollution between tests:
typescriptbeforeEach(() => { vi.clearAllMocks(); // Reset all mock call counts and return values }); afterEach(() => { vi.restoreAllMocks(); // Restore original implementations vi.resetModules(); // Reset cached module imports (if you reload modules) });
Verify: beforeEach calls vi.clearAllMocks() for providers and afterEach calls vi.restoreAllMocks().
Write assertions using expect(). Match the patterns from existing tests:
typescript// Simple checks expect(value).toBe(expected); expect(array).toContain(item); expect(fn).toThrow('error message'); // Instance checks expect(obj).toBeInstanceOf(ClassName); // Mock checks expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith(arg); // File system checks expect(fs.existsSync(path)).toBe(true);
Verify: Each test has at least one assertion and uses appropriate expect() matchers.
Run tests locally before committing:
bashpnpm test # Run all tests in watch mode pnpm test:coverage # Check coverage thresholds pnpm test -- src/my/path/__tests__/my.test.ts # Run single test file
Verify: All tests pass and coverage thresholds are met (lines: 50%, functions: 50%, branches: 50%).
User asks: "Write tests for my config loader that reads from env vars and a config file."
Actions taken:
src/lib/__tests__/config.test.tsfs and osprocess.env in beforeEach/afterEachResult:
typescriptimport { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import fs from 'fs'; vi.mock('fs'); vi.mock('os', () => ({ default: { homedir: () => '/home/user' } })); import { loadConfig } from '../config.js'; describe('config', () => { const originalEnv = process.env; beforeEach(() => { vi.clearAllMocks(); process.env = { ...originalEnv }; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENAI_API_KEY; }); afterEach(() => { process.env = originalEnv; }); it('returns env config when ANTHROPIC_API_KEY is set', () => { process.env.ANTHROPIC_API_KEY = 'sk-ant-test'; const config = loadConfig(); expect(config?.provider).toBe('anthropic'); }); it('returns null when no env vars set', () => { expect(loadConfig()).toBeNull(); }); });
User asks: "Write tests for file tree analysis."
Actions taken:
src/fingerprint/__tests__/file-tree.test.tsResult:
typescriptimport { describe, it, expect, afterEach } from 'vitest'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { getFileTree } from '../file-tree.js'; const dirs: string[] = []; afterEach(() => { for (const d of dirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } dirs.length = 0; }); describe('getFileTree', () => { it('returns files sorted by mtime descending', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'caliber-ft-')); dirs.push(tmp); fs.writeFileSync(path.join(tmp, 'a.ts'), 'a'); fs.writeFileSync(path.join(tmp, 'b.ts'), 'b'); const tree = getFileTree(tmp); expect(tree).toHaveLength(2); }); });
User asks: "Write tests for the provider factory that instantiates different providers based on config."
Actions taken:
src/llm/__tests__/index.test.tsvi.unmock('../index.js') to test the real modulevi.hoisted()resetProvider() between tests to clear cached instancesResult:
typescriptimport { describe, it, expect, vi, beforeEach } from 'vitest'; vi.unmock('../index.js'); const { mockLoadConfig, MockAnthropicProvider } = vi.hoisted(() => { class MockAnthropicProvider { config: unknown; call = vi.fn(); constructor(c: unknown) { this.config = c; } } return { mockLoadConfig: vi.fn(), MockAnthropicProvider, }; }); vi.mock('../config.js', () => ({ loadConfig: () => mockLoadConfig(), })); vi.mock('../anthropic.js', () => ({ AnthropicProvider: MockAnthropicProvider, })); import { getProvider, resetProvider } from '../index.js'; describe('getProvider', () => { beforeEach(() => { vi.clearAllMocks(); resetProvider(); }); it('creates AnthropicProvider for anthropic config', () => { mockLoadConfig.mockReturnValue({ provider: 'anthropic', model: 'claude-sonnet-4-6', apiKey: 'sk-test', }); const provider = getProvider(); expect(provider).toBeInstanceOf(MockAnthropicProvider); }); });
"Cannot find module" when running tests
vi.mock() called after the import statementvi.mock() and vi.unmock() calls to the TOP of the file, before any import statementsEnvironment variable persists across tests
beforeEach assigns by reference instead of copying: process.env = originalEnvbeforeEach: process.env = { ...originalEnv }; delete process.env.VARTemporary files not cleaned up, filling disk
afterEach not called or temp paths not trackeddirs: string[] = [] pushed to in tests, cleaned in afterEach with fs.rmSync(..., { recursive: true, force: true })Mock return value from previous test bleeds into next test
vi.clearAllMocks() not called in beforeEachvi.clearAllMocks() as first statement in beforeEach"vi.mocked() is not a function" when accessing mock calls
vi.mocked() on a non-mocked modulevi.mock() before importing, then cast: const mockFn = vi.mocked(importedFn)Test passes locally but fails in CI
vi.mock() for fs/http/execCoverage threshold failures: "lines not covered", "statements not covered"
pnpm test:coverage locally to see untested lines. Add it() tests for error cases, edge conditions, and branches that return different valuesif (x) return 'a'; else return 'b'; has 0% branch coverage, add tests: one with x=true, one with x=false"expected 1 error but got 0" when testing error throws
expect(() => fn()).toThrow('message'). For async: expect(async () => { await fn() }).rejects.toThrow('message') or use await expect(promise).rejects.toThrow()Mock factory returns undefined
vi.hoisted() variables used before definition in the same blockvi.hoisted() block returns all factories, and vi.mock() blocks use them after the hoisted definitionTest flakes (sometimes passes, sometimes fails)
{ force: true } in rmSync. For timing, avoid setTimeout; use vi.useFakeTimers() and vi.runAllTimers() if needed. Check for leftover files from previous test runs in beforeEachOther measured skills in the registry, with their headline benchmark lift.