Story test gate (Vitest) — the coverage floor, the route→service→gate seams, and the guard that stops a second policy path
The story's per-repo coverage and integration gate, run AFTER every motir-core code card has merged so it measures their real, merged coverage. Modelled on the administrative split's gate; this one has more to prove, because its story removes capability rather than preserving it.
The three jobs
- Coverage floor. Run coverage over the story's changed surface — the services that gained a gate,
lib/permissions/*, and the routes that changed — and write the missing unit and branch tests wherever a file is below the project's ≥90% per-file floor. Each card's own tests are the floor; this tops up the seams between them. - The route→service→gate seams the unit tests mock. Drive a REAL request through a route to a real service to a real
assertPermissionagainst a real Postgres, for at least one operation per key, and assert on the HTTP status the client will actually see — the refusal shape (404for a non-browser,403for a browser without the key) is what every consumer branches on, and no unit test on a service method observes it. - The architecture guards coverage cannot see.
- No second policy path. Every gate in the story resolves through
projectAccessService; a test asserts that no service underlib/servicesdecides access by reading a membership row and branching on the role itself — the private-helper shape the name-whitelist bug found three of. - Every member-facing key has at least one production call site, asserted by grepping
libandappoutsidetests/— the inverse of the orphan guard, at story scope. - The cross-repo parameter name is pinned. The core client's job-read query parameter and the name the motir-ai card requires are the same string, asserted from a shared constant rather than from two literals — this is the one seam in the story that no single repo's tests can cover, and a typo in it fails closed and silently.
- No second policy path. Every gate in the story resolves through
Boundary
One gate card, one repo. motir-ai has a single card in this story and it carries its own tests; there is no second gate card for it, because a story-level gate exists to cover the seams BETWEEN sibling cards and that repo has no siblings here. The seam that does cross the boundary is covered by job 3's shared-constant assertion.
Acceptance criteria
- Coverage over the story's changed files meets the project's per-file floor, and the PR body names any file that was already above it and needed nothing.
- At least one end-to-end route→service→gate test exists per member-facing key, asserting the HTTP status and body shape of the refusal, against real Postgres.
- The no-second-policy-path guard fails when a service is edited to branch on a role directly — proved by writing that violation, watching it go red, and reverting it.
- The production-call-site assertion covers all eight keys and excludes
tests/. - The shared-constant test for the job-read parameter name exists and fails if either side's literal drifts.
pnpm testis green for the touched suites, and the story's owntests/permissions/suite is unchanged by this card except where it adds coverage.
Context refs
tests/permissions/— the four shipped suites this card tops up rather than duplicates.motir-core/CLAUDE.md§ coverage — the per-file floor.lib/services/projectAccessService.ts— the single policy path the guard asserts.- The administrative split's gate — the sibling card whose shape this follows.
- The name-whitelist bug — why the second-policy-path guard exists.