Story test gate (Vitest) — the coverage floor, the write→resolve seams, and the guards that stop a second role-writing path
The story-level gate, run after every code subtask has merged, because it measures their real combined result. It is not a list of cases decided in advance — each card ships its own units, and this card does the three things no individual card can.
1 · Coverage floor
Run coverage over this story's changed surface — lib/permissions/*, lib/services/projectRoleDefinitionService.ts, the touched part of projectAccessService, the two repositories, the route handlers, the editor and the roles components — and, wherever a file sits under the project's per-file gate, write the missing unit and branch tests to reach it. "Already covered by the per-subtask floor" is the expected normal outcome for most files, not a redundancy to subtract in advance; the job is the gap between them.
2 · The seams the units mock
Each of these drives one card's REAL output through the next card's REAL consumer. They are the tests that catch key drift, which every unit masks by construction:
- Author → resolve. Create a role through the service, assign it through the members service, then resolve that actor with
projectAccessService.getPermissionsand assert the exact set. This is the story's central seam and the one thing no card owns end to end. - Store → read → screen. A stored
ProjectRoleDefinitionthroughgetRoleCataloginto the widenedRoleDTOand intoRoleList/RoleDetail, asserting the rendered name, theBased on … · ±Nchip and the counts — the DTO's keys read back through the component that consumes them, not compared to a fixture. - Editor → API → store. The editor's submitted payload through the real route into a stored row, and back out through the read.
- Delete → reassign → resolve. Delete a role with a destination and assert every mover's resolved permissions changed to the destination's — not merely that a column moved.
- Access level × custom role. The resolution's own parity table extended across all four access levels for a membership on a custom role, against real Postgres rather than the pure unit's inputs.
3 · The guards a coverage number cannot see
- One write path. No second writer of
role_definition_idexists outsideprojectMembershipRepository's three methods — a static guard overlib/, so a future card cannot add a parallel path and pass its own tests. - The catalog is the source of truth over stored data. A stored role holding a key no longer in
ROLE_GATED_PERMISSIONS, or one that is notenforced, resolves without it — stale rows may never widen access. - Purity.
lib/permissions/resolve.ts,catalog.tsandlimits.tsimport no Prisma and no React, so they load identically in a server component, a client bundle and a bare test. - Tenancy. Under a foreign workspace GUC, a project's role definitions are invisible to the read, uncountable by the catalog and unwritable by the service — asserted at the non-bypass app role, not only at the service.
- Nothing regressed.
tests/permissions/accessParity.test.ts,catalog.test.ts,noUngovernedOperation.test.tsandinventoryCoverage.test.tsare green unedited; a change forced on any of them is a behaviour change this story did not ask for, and is a finding rather than a fix.
Real Postgres throughout — no mocked database.
Scope boundary
In: the coverage top-up, the five seam tests, the structural guards, and any fixture they need. Out: re-deriving unit coverage a card already shipped; the browser flow, which the E2E owns; any production-code change — if a seam test fails because the product is wrong, that is a bug card, not an edit to the assertion.
Acceptance criteria
- Every file this story changed meets the repo's per-file coverage gate, with the tests written here for whatever fell short; the run is reported in the PR.
- All five seams above have a test that drives real code on both sides and reads the consumer's own output, never a hand-built fixture standing in for the producer.
- Each structural guard exists and fails when deliberately broken — a temporary second write path, a stale permission key, a Prisma import into
resolve.ts, each shown to turn the guard red before being reverted. - The four named existing suites pass with no edit; if any needed one, the PR says which and why rather than quietly changing it.
- The suite runs against real Postgres and is deterministic across repeated runs and a warm connection pool.
Context refs
tests/permissions/—accessParity.test.ts,catalog.test.ts,getPermissions.integration.test.ts,roleCatalogMappers.test.ts,rolesStoryGate.integration.test.ts,storyGate.integration.test.ts,noUngovernedOperation.test.ts,inventoryCoverage.test.ts— the shipped suites this extends and must not break.- The previous story's gate — the shape this follows.
motir-core/CLAUDE.md§ coverage — the per-file gate this measures against.- The nine code cards this runs over: Checkbox · schema · resolution · service · API · read · delete · editor · assignment.