caliber-testing
Writes Vitest tests following project patterns: __tests__/ directories, vi.mock() for module mocking with vi.hoisted() for test-time factories, glob…
它会碰到什么
这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。
技能内容
Caliber Testing
Critical
- All test files MUST be placed in
__tests__/directories parallel to source files:src/[module]/__tests__/[module].test.ts - Register any new
__tests__/directories invitest.config.ts'sincludeglob (already configured:src/**/*.test.ts) - NEVER mock
src/test/setup.ts— it is the global LLM provider mock already applied to all tests - Environment variable tests MUST save
process.envinbeforeEach, restore inafterEach, and explicitly delete env vars to test absence - Temporary file/directory cleanup MUST happen in
afterEach, not in individual test cleanup. Usefs.rmSync(dir, { recursive: true, force: true }) - When a test requires unmocking modules mocked in global setup, call
vi.unmock('../module.js')BEFORE the import statement - Run
pnpm testlocally andpnpm test:coveragebefore committing to verify coverage thresholds (lines: 50, functions: 50, branches: 50, statements: 50)
Instructions
Step 1: Create the test file in the correct directory
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.
Step 2: Import test framework
At the top of every test file, import from vitest:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Add additional imports based on what you're testing:
- File system:
import fs from 'fs'; import path from 'path'; import os from 'os'; - Temporary files: Use
fs.mkdtempSync(path.join(os.tmpdir(), 'caliber-prefix-')) - Exec:
import { execSync } from 'child_process';
Verify: All required test utilities are imported before test definitions.
Step 3: Set up module mocking (if needed)
If testing a module that imports other modules you want to mock:
vi.mock('../config.js', () => ({
loadConfig: vi.fn(),
writeConfigFile: vi.fn(),
}));
For complex mocks with test-time factory functions (hoisted):
const { 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):
vi.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.
Step 4: Organize tests in describe blocks
Group related tests with describe():
describe('functionName', () => {
it('returns X when Y', () => {
// test body
});
});
Verify: Each it() test has a clear, complete assertion.
Step 5: Manage environment and process state
For tests that modify process.env or process.argv:
describe('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.
Step 6: Manage temporary files and directories
For file system tests, create temporary directories and clean them up:
describe('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.
Step 7: Handle mock state cleanup
Before each test, clear mock call history to avoid pollution between tests:
beforeEach(() => {
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().
Step 8: Test assertions
Write assertions using expect(). Match the patterns from existing tests:
// 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.
Step 9: Run tests
Run tests locally before committing:
pnpm 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%).
Examples
Example 1: Testing a module with environment variables (config.test.ts pattern)
User asks: "Write tests for my config loader that reads from env vars and a config file."
Actions taken:
- Create
src/lib/__tests__/config.test.ts - Import test framework and fs module
- Mock
fsandos - Save/restore
process.envin beforeEach/afterEach - Delete specific env vars to test absence
- Test each branch: env vars set, file exists, both, neither
Result:
import { 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();
});
});
Example 2: Testing file system operations with temp cleanup (file-tree.test.ts pattern)
User asks: "Write tests for file tree analysis."
Actions taken:
- Create
src/fingerprint/__tests__/file-tree.test.ts - Set up a cleanup array for temp directories
- Create temp dirs in each test and push to cleanup array
- Clean up all temp dirs in afterEach
- Test with various file mtimes and directory structures
Result:
import { 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);
});
});
Example 3: Testing with module mocking and factory functions (index.test.ts pattern)
User asks: "Write tests for the provider factory that instantiates different providers based on config."
Actions taken:
- Create
src/llm/__tests__/index.test.ts - Use
vi.unmock('../index.js')to test the real module - Create mock classes in
vi.hoisted() - Mock dependencies (config, provider classes)
- Test provider selection logic
- Use
resetProvider()between tests to clear cached instances
Result:
import { 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);
});
});
Common Issues
"Cannot find module" when running tests
- Cause:
vi.mock()called after the import statement - Fix: Move all
vi.mock()andvi.unmock()calls to the TOP of the file, before anyimportstatements
Environment variable persists across tests
- Cause:
beforeEachassigns by reference instead of copying:process.env = originalEnv - Fix: Create a copy in
beforeEach:process.env = { ...originalEnv }; delete process.env.VAR
Temporary files not cleaned up, filling disk
- Cause:
afterEachnot called or temp paths not tracked - Fix: Use centralized cleanup array:
dirs: string[] = []pushed to in tests, cleaned inafterEachwithfs.rmSync(..., { recursive: true, force: true })
Mock return value from previous test bleeds into next test
- Cause:
vi.clearAllMocks()not called inbeforeEach - Fix: Add
vi.clearAllMocks()as first statement inbeforeEach
"vi.mocked() is not a function" when accessing mock calls
- Cause: Trying to use
vi.mocked()on a non-mocked module - Fix: Ensure the module is mocked with
vi.mock()before importing, then cast:const mockFn = vi.mocked(importedFn)
Test passes locally but fails in CI
- Cause: Tests rely on real file system or network (not mocked)
- Fix: Check if temp files are being created in real directories instead of mocked ones. Verify all external calls use
vi.mock()for fs/http/exec
Coverage threshold failures: "lines not covered", "statements not covered"
- Cause: Branches or error paths not tested
- Fix: Run
pnpm test:coveragelocally to see untested lines. Addit()tests for error cases, edge conditions, and branches that return different values - Example: If
if (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
- Cause: Error not actually thrown, or thrown asynchronously
- Fix: For sync functions:
expect(() => fn()).toThrow('message'). For async:expect(async () => { await fn() }).rejects.toThrow('message')or useawait expect(promise).rejects.toThrow()
Mock factory returns undefined
- Cause:
vi.hoisted()variables used before definition in the same block - Fix: Ensure
vi.hoisted()block returns all factories, andvi.mock()blocks use them after the hoisted definition
Test flakes (sometimes passes, sometimes fails)
- Cause: Timing issues or file system race conditions in cleanup
- Fix: For file cleanup, use
{ force: true }inrmSync. For timing, avoidsetTimeout; usevi.useFakeTimers()andvi.runAllTimers()if needed. Check for leftover files from previous test runs inbeforeEach
想直接用这个技能?
本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。
同名技能的其他版本
有 4 个不同仓库或目录里都有叫 caliber-testing 的技能。它们内容并不相同,别混用:
- caliber-ai-org/ai-setup — Writes Vitest tests following project patterns: __tests__/ directories, vi.mock() for modu
- caliber-ai-org/ai-setup — Writes Vitest tests following project patterns: __tests__/ directories, vi.mock() for modu
- caliber-ai-org/ai-setup — Writes Vitest tests following project patterns: __tests__/ directories, vi.mock() for modu