11.3.10 Story vitest gate — coverage floor, the write→read seams, and the contract guards the planning endpoints owe
The story-level vitest gate for 11.3, run AFTER its feature subtasks merge — it measures their real, merged result, so it cannot run before they land. Real Postgres, not mocks.
It is a gate, not a list of cases. Three jobs, resolved at run time:
1. Coverage floor
Run coverage over the story's changed surface — lib/api/v1/pagination.ts's new surface, the project / sprint / ready schema modules, sprintsService.getById, and every new route file — and wherever it sits below the project's ≥90% per-file branch/fn/line gate, WRITE the missing unit and branch tests to reach it. Each subtask's own units are the floor; this tops up the seams between them. "Already covered by the per-subtask floor" is the EXPECTED normal here, not a reason the card has nothing to do.
2. Integration seams — the writer→consumer paths the units mock
Drive one subtask's REAL output back through the next's REAL consumer, which is where key drift hides:
- Create → list → read. A sprint created through
POST /api/v1/projects/{projectKey}/sprintsappears inGET .../sprintsand reads identically throughGET /api/v1/sprints/{sprintId}— the same sprint, the same field values, through three independently-written mappers. - Start → every read. After
POST .../start, the activation baseline (committedIssueCount/committedPoints) is non-null on BOTH sprint reads, andstateisactiveeverywhere it appears. - Move → the two ranked collections. Items moved into a sprint appear in
GET /api/v1/sprints/{sprintId}/work-itemsat the rank tail and are ABSENT fromGET .../backlog; moved back, the inverse — the round trip the membership card deliberately left to this level rather than depending on a sibling endpoint. - Complete → carry-over. After
POST .../complete, the unfinished members are in the destination and the done-category ones remain on the completed sprint. - Move → ready. Sprint membership does not change what
GET .../readyreturns (readiness is not sprint-scoped) — the seam that catches a route quietly filtering the ready set by sprint.
3. Architecture / contract guards — what coverage percentage cannot see
- No route re-derives readiness, rank, or sprint state. The story's central risk: a route that computes
blockedBy.every(done)instead of callinglistReadypasses every functional test on a shallow fixture and is wrong on a deep tree. Guard it structurally (no route may reachcollectReadyLeaves-equivalent logic, sort a ready page, or compare status categories), and back it with the deep-tree fixture where the parent-ready cascade excludes an item whose own blockers are all done. - No v1 route imports from
lib/mcp/. The two transports align through the service; an import is how they start sharing a shape neither owns. This is a NEW guard — 11.1's shipped tree-wide guard checks Prisma, transactions, the wrapper and the declared scope, not this. - Every v1 response is a schema's output. No route in this story's tree hand-shapes a body inline (Amendment 2), and no work-item cuid appears in any response — the same guard
tests/api/v1/story-gate.test.tsruns, re-run over the new routes. - Cursor isolation. A cursor issued by any one of this story's collections is refused by every other — asserted across the real endpoints, not just at the codec's unit level.
- The two closed vocabularies are TOTAL by compile-time guard (sprint
state, projectaccessLevel) — assert the guards exist rather than re-testing the values. - Cross-tenant isolation across the new tree: every endpoint taking a
projectKeyorsprintIdanswers 404, never 403, for another tenant's resource.
Do NOT enumerate at plan time which cases the feature subtasks will cover — that is unknowable until they are built.
Acceptance criteria
- The per-file coverage floor (≥90%) holds on every file this story added or changed, with the top-up tests written where it did not.
- Each integration seam above is covered by at least one test that drives REAL output through a REAL consumer against real Postgres.
- The re-derivation guard exists and FAILS when violated — proven by the shipped pattern of asserting the guard against a deliberately-violating fixture, as
tests/api/v1/story-gate.test.tsdoes for its own guards (a guard nobody has seen fail is not a guard). - The
lib/mcp/import guard finds the route tree at all before asserting over it (a guard over zero files proves nothing) and fails on a planted violation. - One gate card, one repo, ONE PR against
motir-core.
Context refs
tests/api/v1/story-gate.test.ts— 11.1's shipped gate: the tree-walking architecture guards, the guards-fail-when-violated pattern, and the cross-tenant sweep to extend.tests/api/v1/work-item-story-gate.test.ts— 11.2.11's resource-level gate, the closest precedent for this card's shape.motir-core/CLAUDE.md§ coverage — the ≥90% per-file gate and how it is run.- Blockers: every code subtask of this story — 11.3.2, 11.3.3, 11.3.4, 11.3.5, 11.3.6, 11.3.7, 11.3.8, 11.3.9. Parent story: 11.3.