Skip to content

Testing

Testing

Tests exist so an agent can close its own loop honestly. A test suite that can’t fail meaningfully provides confidence without coverage, which is worse than no suite at all.

Levels

LevelToolRuns against
UnitVitestPure logic. No I/O
DatabaseVitestReal Postgres — Docker locally, Neon branch in CI
APIVitest + @cloudflare/vitest-pool-workersThe real Workers runtime
WebPlaywrightBuilt Astro output
ContracttscClients against packages/shared

Non-negotiables

Real Postgres, never mocked. pg_trgm and FTS cannot be meaningfully faked, and a mocked query proves nothing about the SQL. Per ADR-0011: Docker locally, Neon branch in CI.

Real Workers runtime for the API. @cloudflare/vitest-pool-workers, not Node with mocked bindings. The runtimes differ in bindings, request lifecycle and available globals — the exact places this project’s traps live.

Write the test first. Step 3 of the loop commits the failing test. A test written after the implementation asserts what the code does; one written before asserts what it should do.

What must have a test

  • Every Definition of Done line that is behavioural
  • Every domain invariant from ADR-0006 and ADR-0007
  • Every bug fix — the regression test comes first, and it must fail before the fix
  • Every migration — that it applies, and that it is reversible or explicitly marked one-way

The invariant tests

These encode the decisions most likely to be broken by a well-meaning change. They live in packages/db and must never be weakened:

  1. A second session rating updates the shelf projection and leaves the first session’s rating intact.
  2. A session participant with a null user_id round-trips through every participant query.
  3. A session insert with only game_id and played_at succeeds.
  4. No table foreign-keys to clerk_user_id — schema assertion, per ADR-0005.

What not to test

  • Framework behaviour. Drizzle, Hono and Zod have their own suites.
  • Getters, pass-throughs, and generated code.
  • Implementation detail. A test asserting how something works blocks the refactor it should have survived.

Test data

Fixtures live beside the tests. Database tests get a transaction rolled back at teardown — no shared mutable state between tests, no ordering dependence.

Catalogue fixtures come from packages/catalogue, so the same data seeds tests and local development.

Anti-patterns

Each of these turns a red gate green while removing the coverage:

  • Deleting or loosening an assertion to pass
  • it.skip without a linked issue
  • Widening a type to silence the typechecker
  • Asserting toBeDefined() where the actual value matters
  • Snapshot tests over structures nobody reads — a diff nobody understands gets accepted blind

If a check is genuinely wrong, that is a work item with an ADR, not an inline edit.