4.3.7 Story tests — estimate write + config + bounded sprint/epic roll-ups (incl. statistic switch + at-scale) + badge/settings components + estimate-a-story E2E
Estimate: 24m · Depends on: 4.3.3, 4.3.4, 4.3.5, 4.3.6
The closing test subtask — the same split Stories 4.1.5 / 4.2.6 used: service/component tests + a focused E2E, against the real Postgres (the project convention: no mocks except getSession; tests/helpers/db.ts truncation).
Service (vitest, real Postgres). 4.3.3: setEstimate writes/clears storyPoints in one transaction (value validation: non-negative, in-range, null clears), records a 1.4.6 revision, and is denied cross-workspace by the finding-#26 gate; getEstimationConfig/updateEstimationConfig round-trips the statistic + scale + custom values, validates custom scale (InvalidScaleConfigError), and rejects a non-admin (EstimationConfigForbiddenError). Roll-ups: rollupForSprint returns committed/completed/remaining where completed counts only category = 'done' issues, remaining = committed − completed floored at 0, an unestimated sprint → {0,0,0}, and the aggregate is bounded (NOT a sum over a loaded page); rollupForParent sums the recursive SUBTREE (a grandchild's points roll into the epic); the statistic switch changes which field both roll-ups sum (SUM(storyPoints) vs SUM(estimateMinutes) vs COUNT(*)); empty-sprint / empty-subtree / empty-custom-scale guards.
Component. The EstimateBadge display (estimated / unestimated —) + picker (deck chips + free numeric + clear) + optimistic write + snap-back, on the board card (configured statistic, not raw minutes) + a backlog row; the sprint committed-points + epic roll-up displays (incl. unestimated —); the Estimation settings panel (statistic/scale selection, custom-scale validation, admin gate).
E2E (Playwright) tests/e2e/estimation.spec.ts. A real estimation session against a seeded project with a sprint + an epic/story tree:
- Estimate a story — open the backlog, click a story's estimate badge, pick a Fibonacci value; the badge updates and survives reload.
- Sprint roll-up — the story's sprint container committed-points figure increases by the estimate.
- Epic roll-up — the parent epic's rolled-up subtree total reflects the new estimate (on the detail + list/tree).
- Detail field — the issue detail shows the story-points field (distinct from the time Estimate) with the same value.
- Config — an admin switches the scale to custom in
settings/project/estimation; the picker deck reflects the custom values. - Scale (finding #57) — against
pnpm db:seed:large(a large sprint + deep epic subtree), the roll-ups come back from one bounded aggregate each and the DOM stays bounded.
Acceptance criteria
pnpm test(real Postgres) covers 4.3.3 (estimate write + validation + revision + workspace gate; config round-trip + custom-scale validation + admin gate;rollupForSprintcommitted/completed/remaining with thedone-category predicate + unestimated 0s + bounded;rollupForParentrecursive subtree sum; the statistic switch; empty-input guards) and the components (badge display/picker/optimistic-snap-back, the roll-up displays, the settings panel).pnpm test:e2e --grep estimationruns green over the real stack: estimate a backlog story (survives reload), the sprint committed-points + epic subtree roll-ups update, the detail story-points field shows the value, an admin switches the scale, and the at-scale roll-ups stay bounded (DOM bounded) ondb:seed:large.pnpm test:coveragekeeps the Story-4.3 service/route + component files ≥90% branch/fn/line (the CI coverage gate); the suite uses the real-Postgres harness + the single allowedgetSessionmock.
Context refs
tests/e2e/backlog.spec.ts(4.2.6) +tests/e2e/board-scrum.spec.ts(4.5.4) — the backlog/board E2E patterns to build the estimation E2E on;tests/helpers/db.ts(real-Postgres truncation + large-seed fixture)- Story 4.3.3 (service + roll-ups) + 4.3.4/4.3.5/4.3.6 (the UI under test); Story 4.1.5 / 4.2.6 — the sibling test-subtask split this mirrors (no duplication of their association/rank/grooming tests)
prodect-core-coverage-gate(≥90% per-file; empty-input guards need a direct test) +prodect-core-local-postgres(sandbox PG@5433 + Playwright) +motir-core/CLAUDE.md(real-Postgres, no mocks, singlegetSessionmock) + theprodect-e2e-selector-gotchas/prodect-e2e-run-harness-oomlessons