Skip to content

Testing

Jaloliddin Ismailov edited this page Oct 4, 2026 · 2 revisions

Two suites guard the Hub, and they are honest about what each is for: the integration suite proves the domain logic against a live server and database, and the end-to-end suite proves the browser experience. Neither mocks the access engine — the point of the tests is that permission rules, transitions and concurrency behave correctly against the real thing.

The suites

npm run test:int    # vitest — needs dev server + migrated DB on :3000
npm run test:e2e    # playwright — browser flows against the same
npm run test        # both

Both provision their own accounts and content per run, which is why they can safely run against a dev database — they build what they need rather than expecting fixtures. Keep npm run dev up while they run.

What the integration suite covers

The specs live under tests/ and are organised by domain — auth, decisions, elections, membership, negotiations, notifications, and the authority model. The authority spec (authority-records.int.spec.ts) is the best example of what "real" testing looks like here:

  • derivation: a record resolves to capabilities through deriveAuthority;
  • the write path: grantAuthority/revokeAuthority transition rows correctly;
  • substitute confinement: a substitute's grant can't exceed the seat it covers;
  • the advisory lock: concurrent grant attempts serialise through pg_locks, verified by inspecting the lock state itself;
  • the unique index: two racing grants of the same record produce one row, not two;
  • provenance: migrated_from is write-once.

That last cluster is the interesting bit — the tests exercise the concurrency machinery, not just the happy path, because the bugs that matter live in the gaps between requests.

CI's test surface

Every PR runs the matrix in .github/workflows/verify.yml:

flowchart LR
    PR --> Lint[Prettier + ESLint]
    PR --> Build[Build + types-drift check]
    PR --> Provision["Provision<br/>(fresh migration chain)"]
    PR --> Node22[Node 22 suite]
    PR --> Node24[Node 24 suite]
    PR --> MCP["Content MCP smoke<br/>(handshake → 18 tools)"]
    PR --> Deploy[Vercel preview]
Loading

The provision job deserves its own mention: it builds the entire migration chain on an empty database, so a broken migration fails CI rather than a deploy. The MCP smoke job boots mcp-content standalone and proves the agent surface still answers initialize and lists its tools — the contract the whole agent workflow depends on.

Traps we've already fallen into

  • The login rate limiter. Several test runs in quick succession will exhaust the 20-attempts-per-15-minutes budget and every subsequent login 429s. Restart npm run dev — the limiter is in-memory.
  • pagination: false still returns { docs }. Payload's find returns a PaginatedDocs shape either way; pagination: false just returns all of them. Tests asserting on the raw return value trip on this.
  • Migrations can't run inside a transaction. DROP DATABASE, CREATE DATABASE and friends fail in a BEGIN…COMMIT block — run them standalone.
  • The dev DB is stateful. A spec that asserts "no active records" will fail if earlier runs left some; assert the transition, not the emptiness.

Writing a new spec

Put it in the domain's spec file (or a new *.int.spec.ts if the domain is new). Provision what you need through the same endpoints a member would use — don't seed rows directly, because then the test proves the rows exist, not that the system creates them. And when the feature touches authority, assert on the derived effect (can the account do the thing), not just the row.

Clone this wiki locally