4.1.5 Tests — sprint state machine, one-active guard, association + same-project guard, rank ordering, bounded backlog at scale
Estimate: 22m · Depends on: 4.1.3, 4.1.4
The closing test subtask — Vitest over the real Postgres (the project convention: no mocks except getSession; tests/helpers/db.ts truncation), proving the data model + rules the rest of Epic 4 depends on. No UI/E2E here (4.1 has no UI; the sprint E2E rides Story 4.5.4 once a board renders sprints).
State machine + guards: assertSprintTransition accepts planned→active + active→complete and rejects every skip/reopen/self transition; the sprint_one_active_per_project partial-unique index rejects a second active sprint in a project (DB-level, asserted via the repository) while allowing one active sprint per project across two projects; createSprint default-naming + window validation; deleteSprint drops a planned sprint and its issues fall to the backlog (SetNull), and refuses the active sprint.
Association + rank: assignToSprint sets sprint_id and removes the issue from the backlog read; the same-project guard rejects a cross-project assignment; moveToBacklog restores it in rank order; rankIssue lands an issue strictly between two neighbours with a SINGLE-row write (assert no other row's rank changed) and handles append/prepend; new issues get a backlog_rank at creation; each write records a 1.4.6 revision row.
Bounded reads (finding #57): getBacklog is cursor-paginated in backlog_rank order and returns the total count; seed a large backlog and assert the read returns ONE bounded page (not every row) and the cursor walks the full ordering deterministically; the rank writes stay O(1) on the large set.
Tenancy: a cross-workspace sprint/backlog read or write is denied by the finding-#26 gate.
Acceptance criteria
pnpm test(real Postgres) covers: the transition guard (all legal + illegal pairs), the one-active-per-project index (rejects 2nd active in a project; allows per-project), create-default-naming + window validation, delete-to-backlog + active-delete refusal, association + same-project guard,moveToBacklog, single-rowrankIssue(append/prepend + between), create-time rank, the 1.4.6 revision writes, and the cross-workspace denial.- A scale test (seeded large backlog) asserts
getBacklogreturns one bounded page + the total count (never load-all) and the cursor walks the whole rank order; rank writes are O(1). pnpm test:coveragekeeps the Story-4.1 service/repository files ≥90% branch/fn/line (the CI coverage gate); the suite uses the real-Postgres harness + the single allowedgetSessionmock.
Context refs
tests/helpers/db.ts— real-Postgres truncation harness + the large-seed fixture pattern- Story 4.1.3 (service + guard) + 4.1.4 (association + rank + bounded reads) — the units under test
motir-core-coverage-gate(≥90% per-file; empty-input guards need a direct test) +motir-core-local-postgres(the sandbox already has PG@5433) +motir-core/CLAUDE.md(real-Postgres, no mocks)- Story 4.5.4 — where the sprint-rendering E2E lives (4.1 ships no UI, so no E2E here)