Test Patterns
Patterns for unit tests, architecture fitness tests, RLS integration tests, and component tests -- with real examples from the codebase.
Unit Tests
Standard tests for pure functions and module exports. These form the bulk of the test suite and run instantly in Node.
Pattern: Testing Module Config and Constants
From packages/ai/src/__tests__/config.test.ts -- verifying that model configuration constants are correct:
import { describe, expect, it } from "vitest";
import { DEFAULT_MODEL, MODEL_MAX_TOKENS, MODEL_STREAM_MAX_TOKENS } from "../config";
describe("config", () => {
it("defaults to sonnet", () => {
expect(DEFAULT_MODEL).toBe("claude-sonnet-4-6");
});
it("has max tokens for all models", () => {
expect(MODEL_MAX_TOKENS["claude-opus-4-6"]).toBe(16_000);
expect(MODEL_MAX_TOKENS["claude-sonnet-4-6"]).toBe(16_000);
expect(MODEL_MAX_TOKENS["claude-haiku-4-5"]).toBe(8_192);
});
});
Pattern: Testing Error Mapping
From packages/ai/src/__tests__/errors.test.ts -- verifying that SDK errors are mapped to domain errors with correct codes and retryability:
import Anthropic from "@anthropic-ai/sdk";
import { describe, expect, it } from "vitest";
import { AIError, mapAnthropicError } from "../errors";
describe("mapAnthropicError", () => {
it("maps RateLimitError", () => {
const sdkError = new Anthropic.RateLimitError(
429,
{
type: "error",
error: { type: "rate_limit_error", message: "Rate limited" },
request_id: null,
},
"Rate limited",
new Headers(),
);
const result = mapAnthropicError(sdkError);
expect(result).toBeInstanceOf(AIError);
expect(result.code).toBe("rate_limited");
expect(result.statusCode).toBe(429);
expect(result.retryable).toBe(true);
});
it("maps generic Error", () => {
const result = mapAnthropicError(new Error("Something broke"));
expect(result.code).toBe("unknown");
expect(result.retryable).toBe(false);
});
});
Key qualities: tests verify specific error properties (code, status, retryability), not just that mapping "works." Each SDK error type has its own test case, plus a catch-all for unknown errors.
Pattern: Testing Algorithms with Factories
From packages/search/src/__tests__/fusion.test.ts -- a factory function creates test data, and tests verify scoring behavior:
function makeResult(id: string, rank: number, overrides?: Partial<RankedResult>): RankedResult {
return {
id,
sourceTable: "research_artifact",
sourceId: "src_" + id,
title: "Title " + id,
rank,
score: 1 / rank,
textSnippet: "Snippet for " + id,
...overrides,
};
}
describe("reciprocalRankFusion", () => {
it("boosts documents appearing in both result sets", () => {
const kw = [makeResult("shared", 1), makeResult("kw_only", 2)];
const sem = [makeResult("shared", 2), makeResult("sem_only", 1)];
const result = reciprocalRankFusion(kw, sem, 10);
expect(result[0]?.id).toBe("shared");
expect(result[0]?.inKeyword).toBe(true);
expect(result[0]?.inSemantic).toBe(true);
expect(result[0]?.rrfScore).toBeGreaterThan(result[1]?.rrfScore ?? 0);
});
it("uses k=60 by default for RRF scoring", () => {
const kw = [makeResult("a", 1)];
const result = reciprocalRankFusion(kw, [], 10);
expect(result[0]?.rrfScore).toBeCloseTo(1 / 61, 5);
});
});
Key qualities: factory function with overrides keeps tests focused on what varies. Mathematical assertions use toBeCloseTo for floating-point. Empty-input edge cases are tested separately.
Pattern: Testing Request Parameter Building
From packages/ai/src/__tests__/request.test.ts -- verifying that options are translated to SDK parameters:
describe("buildRequestParams", () => {
const baseOpts: ChatOptions = {
messages: [{ role: "user", content: "Hello" }],
feature: "test",
};
it("uses default model (sonnet) when none specified", () => {
const { params, model } = buildRequestParams(baseOpts, false);
expect(model).toBe("claude-sonnet-4-6");
expect(params.model).toBe("claude-sonnet-4-6");
});
it("uses streaming max_tokens when streaming", () => {
const { params } = buildRequestParams(baseOpts, true);
expect(params.max_tokens).toBe(64_000);
});
it("omits optional fields when not provided", () => {
const { params } = buildRequestParams(baseOpts, false);
expect(params.system).toBeUndefined();
expect(params.tools).toBeUndefined();
expect(params.temperature).toBeUndefined();
});
});
Key qualities: a shared baseOpts object keeps boilerplate minimal. Tests verify both presence and absence of fields. Streaming vs. non-streaming is a distinct test, not a parameter of the same test.
Architecture Fitness Tests
These tests enforce structural rules that are too important for advisory documentation alone. They read source files at test time and verify constraints.
Pattern: Procedure Enforcement (@repo/api)
From packages/api/src/__tests__/arch.test.ts -- ensures all feature routers use authorizedProcedure:
const AUTHORIZED_PROCEDURE_ALLOWLIST: Record<string, string> = {
"pat.ts": "PAT management is user-level, not org-scoped -- uses protectedProcedure",
"index.ts": "Barrel re-export file",
"ai-logs-helpers.ts": "Pure helper functions, no procedure definitions",
};
describe("authorizedProcedure enforcement", () => {
for (const file of routerFiles) {
if (AUTHORIZED_PROCEDURE_ALLOWLIST[file]) continue;
it(`${file} should only use authorizedProcedure`, () => {
const content = fs.readFileSync(path.join(ROUTERS_DIR, file), "utf-8");
expect(content).toContain("authorizedProcedure");
const usesPublic = /\bpublicProcedure\s*\./.test(content);
if (usesPublic) {
expect.fail(
`${file} uses publicProcedure -- feature routers must use authorizedProcedure.`,
);
}
});
}
});
The allowlist pattern is critical: every exception must have a documented justification. The test reads actual source files and uses regex to detect lower-privilege procedure usage. For full details on the authorization enforcement pattern, see Identity & Access -- Router Enforcement.
Pattern: Tool Registration Enforcement (@repo/mcp)
From packages/mcp/src/__tests__/arch.test.ts -- ensures all MCP tool files in the tools/ directory are imported and registered in server.ts:
describe("MCP tool registration", () => {
const serverContent = fs.readFileSync(SERVER_FILE, "utf-8");
const toolFiles = fs
.readdirSync(TOOLS_DIR)
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"));
for (const file of toolFiles) {
const toolName = file.replace(/\.ts$/, "");
it(`tools/${file} should be imported in server.ts`, () => {
const importPattern = `./tools/${toolName}`;
expect(serverContent).toContain(importPattern);
});
}
it("server.ts should not import non-existent tool files", () => {
// Reverse check: every import in server.ts has a corresponding file
});
});
This catches the common failure mode of creating a tool file but forgetting to register it. The bidirectional check (files exist for imports, imports exist for files) prevents drift in either direction.
RLS Integration Tests
These tests require a running PostgreSQL instance and verify that Row-Level Security policies enforce tenant isolation at the database level. They are the most critical tests in the codebase.
For the full RLS testing pattern and how tenant scoping works, see Data & Storage -- Tenant Scoping.
Pattern: Tenant Context Helper
From packages/db/src/__tests__/rls.test.ts -- a helper function that sets up an RLS-scoped session:
async function asAuthenticatedTenant<T>(
tenantId: string,
fn: (tx: ReturnType<typeof drizzle>) => Promise<T>,
): Promise<T> {
return db.transaction(async (tx) => {
await tx.execute(sql`SET ROLE authenticated`);
await tx.execute(sql`SELECT set_config('app.tenant_id', ${tenantId}, true)`);
const result = await fn(tx as unknown as ReturnType<typeof drizzle>);
await tx.execute(sql`RESET ROLE`);
return result;
});
}
This mirrors what withTenantContext() does in production: set the PostgreSQL role and session variable inside a transaction, then reset the role afterward.
Pattern: Four-Operation Isolation
RLS tests verify all four CRUD operations are isolated:
describe("RLS Tenant Isolation", () => {
describe("SELECT isolation", () => {
it("org A context sees only org A members", async () => {
const rows = await asAuthenticatedTenant(ORG_A, (tx) => tx.select().from(member));
expect(rows).toHaveLength(2);
expect(rows.every((r) => r.organizationId === ORG_A)).toBe(true);
});
it("no tenant context returns zero rows", async () => {
const rows = await asAuthenticatedNoTenant((tx) => tx.select().from(member));
expect(rows).toHaveLength(0);
});
});
describe("INSERT isolation", () => {
it("rejects insert with wrong organization_id", async () => {
await expect(
asAuthenticatedTenant(ORG_A, (tx) =>
tx.insert(member).values({
id: "test_member_reject_001",
organizationId: ORG_B, // wrong tenant
userId: USER_1,
role: "member",
createdAt: new Date(),
}),
),
).rejects.toThrow();
});
});
describe("UPDATE isolation", () => {
it("cannot update rows belonging to another org", async () => {
// Attempt cross-tenant update, then verify no change
});
});
describe("DELETE isolation", () => {
it("cannot delete rows belonging to another org", async () => {
// Attempt cross-tenant delete, then verify row still exists
});
});
});
Key qualities:
- Real database, real policies -- mocks are never used for RLS tests
- All four operations -- SELECT, INSERT, UPDATE, DELETE each get isolation tests
- No-context case -- tests that an empty tenant context returns zero rows (not an error)
- Cross-tenant rejection -- INSERT with the wrong
organization_idmust throw; UPDATE/DELETE with the wrong tenant must be silently no-ops (RLS filters rows, it does not throw on missing rows) - Targeted cleanup -- test data uses prefixed IDs (
test_org_a_001) and targeted DELETEs, notTRUNCATE CASCADE, to avoid interfering with parallel test files
Component Tests (Web App)
Component tests in apps/web use jsdom environment and @vitejs/plugin-react for JSX. They test React hooks, helper functions, and component behavior.
Pattern: Testing Helper Functions
From apps/web/src/app/api/admin/ai-playground/__tests__/route-helpers.test.ts:
describe("buildServerTools", () => {
it("returns empty array when no tools enabled", () => {
expect(buildServerTools({})).toEqual([]);
});
it("includes web search tool with allowed domains", () => {
const tools = buildServerTools({
webSearch: true,
webSearchAllowedDomains: ["example.com"],
});
expect(tools).toHaveLength(1);
expect(tools[0]).toMatchObject({
type: "web_search_20260209",
name: "web_search",
allowed_domains: ["example.com"],
});
});
});
toMatchObject is preferred over toEqual for partial assertions on objects with many fields.
Mocking Rules
Mocking is permitted only at system boundaries:
| Boundary | Mock Approach |
|---|---|
| Database queries | Use real DB for RLS tests; mock Drizzle client for unit tests |
| External API calls | Mock SDK responses (e.g., Anthropic SDK errors) |
| Loggers | Mock or suppress ctx.logger |
| CASL abilities | Construct real defineAbility with test permissions |
| Time | Use vi.useFakeTimers() |
Never mock functions within the same package. If a function is hard to test without mocking internal dependencies, that is a signal the function needs refactoring, not more mocks.
Over-mocking (more than 5 mocks per test) is flagged by the /test-review skill as an isolation concern.