docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# WP-70 — Test-data builders: illegal fixtures unrepresentable
|
||||
|
||||
Status: done (2eea860..a82332f)
|
||||
Phase: 12 — DDD hardening
|
||||
|
||||
## Why
|
||||
|
||||
Decision #3 in `CLAUDE.md` — "make illegal states unrepresentable" — is honoured in
|
||||
production code (`AanvraagStatus`'s private-ctor/factory shape, the FE's tagged-union
|
||||
machines, branded value objects behind `parse*`) but **not** in the test suites that exercise
|
||||
them. Every layer independently reinvented ad-hoc, hand-built fixtures that reach around the
|
||||
production construction path:
|
||||
|
||||
- Backend: `Aanvraag` is a mutable EF-backed bag with independent public setters. Its own
|
||||
`StatusAt` dereferences `Referentie!` three times on the unstated assumption
|
||||
"Submitted ⇒ Referentie != null" — a convention two test files (`RuleTests.cs`,
|
||||
`OpenZaakZaakSourceTests.cs`) kept consistent by hand across eight inline fixtures.
|
||||
- Frontend: no shared fixture helper existed anywhere. Every machine spec redefined its own
|
||||
throwaway literal helper (`editing1/2/3`, `editingWith`), each hardcoding fields like
|
||||
`errors: {}` — asserting against shapes the real reducer may never produce.
|
||||
- E2E: the seeded BSN and a diploma id were copy-pasted across all three specs, coupled to
|
||||
`SeedData.cs`'s exact shape by comment only.
|
||||
|
||||
## Read first
|
||||
|
||||
- ADR-0006 (`docs/reference/architecture/0006-test-data-builders.md`) — the principle and the
|
||||
full decision table this WP implements.
|
||||
- `CLAUDE.md` §"The decisions" #3, #5.
|
||||
- `backend/src/BigRegister.Api/Data/ApplicationStore.cs` (`Aanvraag`, `StatusAt`).
|
||||
- `backend/src/BigRegister.Api/Domain/Applications/AanvraagStatus.cs` — the exemplar this
|
||||
WP's backend builder mirrors.
|
||||
- `backend/src/BigRegister.Api/Domain/Beoordeling/BeoordelingRules.cs`.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
1. **No `With*`-per-field builders anywhere.** A builder that opens every field back up is an
|
||||
object literal with extra syntax — reject that shape on either side of the seam.
|
||||
2. **Backend: a type-state builder.** `Given.Concept()` → `ConceptAanvraag` (only `.AtStep`/
|
||||
`.Submitted`/`.Build` exist) → `SubmittedAanvraag` (only `.Decided`/`.Build` exist) →
|
||||
`DecidedAanvraag`. `Decided(...)` validates a toelichting by calling the real
|
||||
`BeoordelingRules.RequiresToelichting`, not by re-stating the rule.
|
||||
3. **`Aanvraag` itself stays mutable** — WP-68 deliberately kept it an EF-backed class; fixing
|
||||
that for real is an EF-mapping refactor, out of scope here (see Follow-ups).
|
||||
4. **Frontend: replay, don't fabricate.** One combinator, `given(reduce, initial)(...msgs)`
|
||||
(`libs/shared/src/testing/machine.ts`), replaces every hand-written state literal. Value
|
||||
objects: `unwrapOk(parseX(raw))`, never a cast. `RemoteData`: named constructors
|
||||
(`loading()`/`success(v)`/`failure(e)`), replacing duplicated per-file literals.
|
||||
5. **E2E stays a flat smoke suite** (WP-19's scope). Only extract shared `Actors`/`SeedRefs`/
|
||||
`loginAs` — no page-object layer, no Given/When/Then runner, no dev-only seeding API.
|
||||
The shared-mutable-backend isolation problem is a documented follow-up, not fixed here.
|
||||
6. **Convert worst offenders only**, not a full sweep: `RuleTests.cs`'s `Decided()` helper +
|
||||
`OpenZaakZaakSourceTests.cs`'s seven inline initializers (backend);
|
||||
`herregistratie.machine.spec.ts` + `change-request.machine.spec.ts` + both RemoteData
|
||||
specs (frontend); all three e2e specs (actors/seed-refs only).
|
||||
|
||||
## Files
|
||||
|
||||
| Area | Path |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| New (BE) | `backend/tests/BigRegister.Tests/Builders/AanvraagBuilder.cs`, `Acceptance/BesluitLifecycleTests.cs` |
|
||||
| Edit (BE) | `RuleTests.cs`, `OpenZaakZaakSourceTests.cs` |
|
||||
| New (FE) | `libs/shared/src/testing/{machine,remote-data,value-object}.ts`, `herregistratie/domain/intake.testing.ts`, `intake.acceptance.spec.ts` |
|
||||
| Edit (FE) | `herregistratie.machine.spec.ts`, `change-request.machine.spec.ts`, `remote-data.spec.ts`, `machine-remote-data.spec.ts`, both `tsconfig.app.json`, `angular.json` |
|
||||
| New (e2e) | `e2e/support/actors.ts` |
|
||||
| Edit (e2e) | `smoke.spec.ts`, `brief-v2.spec.ts`, `error-state.spec.ts` |
|
||||
| Docs | ADR-0006, `libs/shared/docs/testing.mdx`, `.claude/skills/test-strategy/SKILL.md`, this file + backlog README row |
|
||||
|
||||
## Steps
|
||||
|
||||
Executed as three file-disjoint parallel tracks (backend / frontend / e2e), each ending its
|
||||
own layer's tests green, then a combined gate, then docs written up against the interfaces as
|
||||
actually shipped.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `Given.Concept().Decided(...)` does not compile (proved live: temporarily inserted the
|
||||
call, confirmed `dotnet build` fails with `CS1061`, reverted).
|
||||
- [x] `Decided(Besluit.Afwijzen)`/`MeerInfoOpvragen` with no toelichting throws, via the real
|
||||
`BeoordelingRules.RequiresToelichting`.
|
||||
- [x] Backend tests: 220/220 passing (was 216 before; +4 from `BesluitLifecycleTests`).
|
||||
- [x] Frontend: `npm test` green across all four projects (ssp/behandelportal/shared/beheer);
|
||||
converted specs assert the same behaviour as before (diffed, not just re-passed) —
|
||||
one case (`editing3`'s hardcoded `errors: {}` at step 3 with invalid punten) was
|
||||
confirmed reachable via `SetField` after `Next`, not an unrepresentable state, so the
|
||||
assertion carried over unchanged.
|
||||
- [x] No fixture-only export (`givenIntake` etc.) leaks into a production bundle — confirmed
|
||||
via `grep -rl` on `dist/` after both a plain and a `--localize` build.
|
||||
- [x] `npm run ci` green (lint, format, tokens, both localized builds, audit, backend
|
||||
format+test, snippet-drift, api-client-drift).
|
||||
- [~] `npm run e2e` — refactor reviewed line-by-line (zero assertions changed), but not run to
|
||||
completion in this environment: port 4200 was occupied by an unrelated container
|
||||
(`team-monitor-web-1`), not this repo's stack. Confirm on a clean runner/CI before
|
||||
relying on it; not a regression introduced by this WP.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd backend && dotnet format --verify-no-changes && dotnet test # 220/220
|
||||
npm run ci # green (2026-08-18)
|
||||
npm run e2e # run on a clean port 4200
|
||||
```
|
||||
|
||||
## Out of scope
|
||||
|
||||
- E2E test isolation (a dev-only seed endpoint) — the real fix for the shared-mutable-backend
|
||||
problem; a new production-adjacent surface needing its own security review.
|
||||
- Making `Aanvraag` itself illegal-states-unrepresentable (EF-mapping refactor).
|
||||
- `RegistrationStatus`'s equivalent flat-record gap (`Domain/Registrations/`) — same class of
|
||||
defect, separate WP.
|
||||
- E2E coverage for `apps/behandelportal` (currently zero).
|
||||
|
||||
## Risks
|
||||
|
||||
- The backend type-state builder only guards the fields it models (`Submitted`, `Referentie`,
|
||||
`SubmittedAt`, `BesluitStatus`, `BesluitToelichting`); other `Aanvraag` fields (e.g.
|
||||
`ZaakUrl`) are still set post-`.Build()` directly, since `Aanvraag` remains mutable. A
|
||||
future field added to the lifecycle needs a deliberate builder update, or it silently
|
||||
reopens the same gap this WP closed.
|
||||
Reference in New Issue
Block a user