Write tests following project conventions. Use when adding new tests or modifying existing ones.
Write tests using Vitest. Tests are colocated next to source files.
src/lib/utils.ts
src/lib/utils.test.ts # colocated
src/app/api/stats/route.ts
src/app/api/stats/route.test.ts
src/test-utils/
├── setup.ts # global setup (PGlite, MSW, auth mock)
├── msw-handlers.ts # external API mocks
└── auth.ts # auth test helpers
Auth is globally mocked. Use helpers to control auth state:
import { mockAuthenticated, mockUnauthenticated } from '@/test-utils/auth';
it('returns 401 when unauthenticated', async () => {
await mockUnauthenticated();
const response = await GET(new Request('http://localhost/api/foo'));
expect(response.status).toBe(401);
});
it('returns data when authenticated', async () => {
await mockAuthenticated(); // uses default test user
// or with overrides:
await mockAuthenticated({ email: 'custom@example.com' });
const response = await GET(new Request('http://localhost/api/foo'));
expect(response.status).toBe(200);
});
Default test user: test@example.com / Test User
Uses PGlite (in-memory PostgreSQL). No Docker required.
insertUsageRecord from @/lib/queries to seed dataimport { insertUsageRecord, getOverallStats } from '@/lib/queries';
beforeEach(async () => {
await insertUsageRecord({
date: '2025-01-01',
email: 'user@example.com',
tool: 'claude_code',
model: 'sonnet-4',
rawModel: 'claude-sonnet-4-20250514',
inputTokens: 1000,
outputTokens: 500,
cacheWriteTokens: 0,
cacheReadTokens: 0,
cost: 0.01,
});
});
pnpm test # run all
pnpm test:watch # watch mode
foo.ts → foo.test.ts)mockAuthenticated()/mockUnauthenticated() for authinsertUsageRecord for seeding databasesrc/test-utils/msw-handlers.tsEvery protected route MUST have an auth test. This is non-negotiable.
Routes using getSession() must verify 401 on unauthenticated requests:
it('returns 401 for unauthenticated requests', async () => {
await mockUnauthenticated();
const response = await GET(new Request('http://localhost/api/your-route'));
expect(response.status).toBe(401);
});
Routes using CRON_SECRET must verify auth:
beforeEach(() => {
vi.stubEnv('CRON_SECRET', 'test-secret');
});
it('returns 401 without authorization header', async () => {
const response = await GET(new Request('http://localhost/api/cron/your-route'));
expect(response.status).toBe(401);
});
it('returns 401 with invalid authorization', async () => {
const response = await GET(
new Request('http://localhost/api/cron/your-route', {
headers: { Authorization: 'Bearer wrong-secret' },
})
);
expect(response.status).toBe(401);
});
Routes with signature verification must test invalid signatures:
it('returns 401 without signature', async () => {
const response = await POST(
new Request('http://localhost/api/webhooks/github', {
method: 'POST',
body: '{}',
})
);
expect(response.status).toBe(401);
});
it('returns 401 with invalid signature', async () => {
const response = await POST(
new Request('http://localhost/api/webhooks/github', {
method: 'POST',
body: '{}',
headers: { 'x-hub-signature-256': 'sha256=invalid' },
})
);
expect(response.status).toBe(401);
});
When adding a new route, always ask: "What auth does this route require?" and add the corresponding auth test.
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer