The pyramid (4 layers + evals)
1. Unit — packages/schema, packages/core pure logic
Pure functions only: schema validation, supersession resolution, commitment FSM transitions, retrieval fusion scoring. No DB, no mocks of the DB — if a test wants to mock packages/db, it’s an integration test in disguise; move it down a layer.
2. Integration — the workhorse layer
- Runs against real Postgres + pgvector — never a mock, never SQLite. Locally: the same
docker-compose.ymlPostgres as self-host (test database). CI: each selectedintegration (...)shard gets its own disposablepgvector/pgvector:pg18service container, aggregated bytest, with the heavy DB package split intodb-structure,db-search,db-auth, anddb-memoryslices. This path requires no hosted-database credentials and works for fork PRs. - Two isolation strategies, used deliberately (
packages/db/test/integration/helpers.ts):- Savepoint pattern —
withTestTransaction()for tests that own their SQL: rolled back, fast, parallel-safe. - Truncate-based cleanup —
resetDomainTables()for suites exercising the realwithTenant()path:withTenantcommits on its own pool, so cross-connection visibility makes savepoints inapplicable there. Honest trade: those suites serialize on cleanup. Because these global TRUNCATEs are not isolated across packages on a shared database (--fileParallelism=falseonly serializes within a package), the roottest:integrationscript pins turbo--concurrency=1for local/single-database runs. CI keeps package-internal serialization and gives each selected shard its own Postgres service container. DB sub-shards therefore cannot collide with one another.
- Savepoint pattern —
- RLS tests run as the runtime role, not the owner role (owner bypasses RLS — a green isolation test as superuser proves nothing). The test harness provisions roles via the same
scripts/provision-roles.sqlas production. - Mandatory suites that exist from the start (
packages/db/test/integration/):- Tenant isolation: user A writes, user B queries — across every query path including edges, facts, commitments, proposals. Mirrors the eval slice but as fast deterministic tests.
withTenantdiscipline: concurrent pooled connections cannot leakapp.user_id.- Append-only enforcement: UPDATE/DELETE on
memory_events/audit_logfails at the grant level; no DELETE anywhere in the memory domain. - Migration round-trip: fresh DB → all migrations → structure parity (tables, policies+NULLIF, FSM trigger, HNSW index, SECURITY DEFINER resolvers).
- HNSW parity: pgvector vs the eval gate’s exact cosine on the real golden-set embeddings — the bridge between the eval gate’s upper bound and what production serves. Two-regime (see Data model & RLS): natural plan asserted index-driven (selective tenants get btree+exact — correct), plus a forced-HNSW arm (
enable_sort=off,hnsw.iterative_scan) with EXPLAIN-asserted index use; both ≥ 0.95 overlap.
3. Contract — MCP and REST surfaces
- Spin up the Express app in-process through the SDK v2 dual-era handler; drive both legacy and
2026-07-28clients through the official MCP client SDK (not hand-rolled JSON-RPC) so version negotiation, transport, auth bridging, and structured-output semantics are actually exercised. - Every tool: happy path + auth-missing + cross-tenant + output-schema validation (the response must parse against the tool’s declared
outputSchema— enforced as plain tests, not a bespoke CI job). - REST mirrors: generated OpenAPI is diffed in CI; a contract change shows up in the PR.
4. E2E — minimal by design
Playwright against the staging deployment afterstaging merges: login → remember via MCP → see it in dashboard → revise → search. One critical-path spec file, not a sprawling E2E suite. E2E failures page the author; they do not gate feature PRs.
LLM policy in tests
- Layers 1–4 never call a real model.
packages/llmships aFakeGateway(deterministic embeddings via seeded hash → vector; canned completions per operation key). Tests assert on our logic, not model behavior. - Model behavior is the eval harness’s job — that’s the separation: tests prove the machine is wired right; evals prove the memory is good. A PR can’t compensate for a golden-set regression with passing unit tests, and vice versa.
- Recorded-fixture mode (capture a real gateway response once, replay) is allowed for extraction-shaped logic, stored under
__fixtures__/, refreshed deliberately — never auto-recorded in CI.
Test health policy
- No skipped tests on
main.it.skip/todofails the lint step. A test that can’t pass is either fixed in the PR or deleted with a linked issue — a pile of dormant markers is how a suite rots. - No flake tolerance: a test that fails intermittently is quarantined by deletion + issue, not retry-until-green. CI has no retry-on-failure config.
- Coverage is observed, not gated. Vitest coverage reported on PRs for review judgment; no threshold ratchet (thresholds breed assertion-free tests). The mandatory-suite list above is the real floor.
- Speed budgets are enforced: unit
<30s, integration<3min, contract<2min, totalci.yml<10minwall. A PR that blows the budget fixes it or splits the suite — slow CI is how the job sprawl started. - Tests-with-the-change: every behavior PR carries its tests; there is no “test backfill” epic, ever.