Ai-setup caliber-testing
Write Vitest tests following Caliber patterns: tests in __tests__/ directories, use vi.mock() for modules, leverage global LLM mock from src/test/setup.ts, save/restore env vars. Trigger: user says 'write tests', 'add tests', 'test this', or creates *.test.ts files. Do NOT use for non-test code or when user is debugging existing tests.
git clone https://github.com/caliber-ai-org/ai-setup
T=$(mktemp -d) && git clone --depth=1 https://github.com/caliber-ai-org/ai-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.agents/skills/caliber-testing" ~/.claude/skills/caliber-ai-org-ai-setup-caliber-testing && rm -rf "$T"
.agents/skills/caliber-testing/SKILL.mdCaliber Testing
Critical
- Test location: Always place tests in
subdirectory alongside source code (e.g.,__tests__/
forsrc/commands/__tests__/score.test.ts
)src/commands/score.ts - Global LLM mock: src/test/setup.ts provides
andmockLLM
globally — use these instead of creating new mocksmockLLMStream - Environment variables: Wrap tests in
andbeforeEach(() => { process.env.SAVED = ... })
to avoid test pollutionafterEach(() => { process.env.SAVED = null }) - Import extensions: Use
extensions for all relative imports (ESM convention).js - Test file naming:
(not[module].test.ts
).spec.ts
Instructions
-
Set up test file structure
- Create file at
src/[feature]/__tests__/[name].test.ts - Import testing utilities:
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' - Import source file under test
- Verify file location matches existing test patterns in project
- Create file at
-
Handle environment variables
- Save original values in
:beforeEachconst savedEnv = { ...process.env } - Restore in
:afterEachObject.assign(process.env, savedEnv); delete process.env.NEW_VAR - This prevents tests from affecting each other
- Verify cleanup runs with
or test will fail on subsequent runsafterEach
- Save original values in
-
Mock external modules with vi.mock()
- Place
calls at top of test file before importsvi.mock() - Use hoisted pattern:
vi.mock('../path/to/module.js', () => ({ default: { /* mock */ } })) - For functions:
vi.mock('../utils/fetch.js', () => ({ fetchData: vi.fn() })) - Verify mock is applied before any test runs by checking first test imports the mocked module
- Place
-
Use global LLM mock from setup.ts
- Global
andmockLLM
are auto-registered in vitest.config.ts setupFilesmockLLMStream - Import from
:src/test/setup.jsimport { mockLLM } from '../test/setup.js' - Configure per test:
mockLLM.mockResolvedValue({ /* response */ }) - Reset between tests:
inmockLLM.mockReset()afterEach - Verify mock was called:
expect(mockLLM).toHaveBeenCalledWith(...)
- Global
-
Structure test groups with describe blocks
- Group related tests:
describe('scoreCommand', () => { it('should...', ...) }) - Use nested describes for sub-features:
describe('error handling', () => { ... }) - Verify each describe has at least one test
- Group related tests:
-
Write assertions matching project style
- Use
withexpect()
,.toEqual()
,.toBeDefined().rejects.toThrow() - For async:
await expect(asyncFn()).resolves.toEqual(...) - For errors:
await expect(asyncFn()).rejects.toThrow('message') - Verify assertion matches the actual return type
- Use
Examples
User says: "Write tests for src/scoring/index.ts"
Actions:
- Create
src/scoring/__tests__/index.test.ts - Import:
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' - Import under test:
import { scoreConfig } from '../index.js' - Set up env var handling:
let savedEnv: Record<string, string | undefined> beforeEach(() => { savedEnv = { ...process.env } }) afterEach(() => { Object.assign(process.env, savedEnv) delete process.env.CUSTOM_VAR }) - Write test using global
:mockLLMdescribe('scoreConfig', () => { it('should score with default weights', async () => { const result = await scoreConfig({ /* ... */ }) expect(result.score).toBeDefined() expect(mockLLM).toHaveBeenCalled() }) })
Result: Test file at correct path, uses global mock, cleans up env vars, follows project conventions.
Common Issues
Error: "Cannot find module '../test/setup.js'"
- Verify import path uses
extension (ESM).js - If accessing
, ensure it's imported:mockLLMimport { mockLLM } from '../test/setup.js' - Check that setup.ts exports the mock in src/test/setup.ts
Error: "Test timeout" or "mock not called"
- Verify
is at top of file, before all importsvi.mock() - Check that mocked module path matches actual import in source file
- Ensure async test uses
or returns Promise:awaitit('name', async () => { ... })
Error: "process.env.VAR is not defined in next test"
- Verify
restores env vars:afterEachObject.assign(process.env, savedEnv) - Check that
saves original:beforeEachconst savedEnv = { ...process.env } - If adding new env vars, delete them:
in afterEachdelete process.env.NEW_VAR
Test passes locally but fails in CI
- Verify no hardcoded absolute paths (use relative imports)
- Check that mocks reset between tests: add
in afterEachmockLLM.mockReset() - Ensure test doesn't depend on file system state (use memfs for file operations)