refactor(specs): replay real messages in 4 machine specs (RB-31)

Four machine specs built their starting state with a hand-rolled object
literal instead of replaying real Msgs through the real reduce, the
exact anti-pattern ADR-0006 section 2 forbids. Three of the four also
hardcoded errors: {}, a shape the reducer might never actually produce.

intake.machine.spec.ts now imports the existing givenIntake from
intake.testing.ts (previously used only by intake.acceptance.spec.ts).
Three new one-line *.testing.ts files export the same given(reduce,
initial) wrapper for registratie-wizard, besluit, and brief. Every old
literal helper (answering, invullen, editingWith, loaded) is replaced
by a message replay that reaches the same state.

Two tests in registratie-wizard.machine.spec.ts asserted a cursor value
the real reducer cannot reach (cursor 2 with no diploma chosen yet,
which requires a diploma to already be set). Both are re-pointed at the
reachable cursor-1 equivalent; submit() validates the whole draft
regardless of cursor, so no assertion changed. Recorded in
implementation/rb-31.md, not worked around.

No *.machine.ts reducer was touched. All four specs pass; npm run ci
is green (lint, typecheck, dep:check, format, tokens, seam, all four
test suites, both app builds, backend 293/293, api-client drift).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-28 13:26:12 +02:00
co-authored by Claude Opus 5
parent 03c6e09306
commit dfc6c419f4
9 changed files with 435 additions and 208 deletions
@@ -100,41 +100,41 @@ deployed first_, not _must ship together_.
Every ticket tracing to a `BIO-` finding, plus every row on agent 07's authoritative
16-row "Compliance review required" list, carries it — regardless of priority.
| ID | Module | Category | Description | Baseline metric improved | Effort | Risk | Priority | CD batch # | Depends on | Compliance | Status |
| --------- | -------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- | ---------- | ---------- | ------------ | -------- |
| **RB-01** | backend/Program.cs + Data | security | Add an owner/capability check to `GET /uploads/{id}/content` and `/uploads/status`; 404 not 403 | §3c Data 75.5% branch vs 99.0% line (BL-005) | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-02** | backend/Program.cs + Data | privacy | Stop concatenating the BSN into `AuthzAudit.Resource`; assert on **values** in the test | §3c Data 75.5% branch (BL-005) | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-03** | backend/Contracts | privacy | `MaskTail(a.Owner, 3)` in `ToAdminSummaryDto` — both cross-owner lists inherit it | §3a bhp/behandeling 91.6%/81.5%; §7 Mapping row | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-04** | backend/Data | privacy | Mask the BSN used as `AuditEntry.Actor` on document audit rows (ownership column untouched) | §3c Data 99.0% line / 75.5% branch | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-05** | backend/Zgw | privacy | Drop the BSN-bearing query + body snippet from the `ZgwHttpClient` exception message | §3c Zgw 98.1%/85.5% (best backend branch) — a design gap, not a test gap | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-06** | backend/Program.cs | security | Delete the dead `POST /registrations` (no FE caller) — or add the `ForeignIds` guard | BL-003 (48 mappings in 940 lines, file CC 78) | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-07** | backend/Program.cs | audit | Audit the **allow** path in all five authz gates + the 3 brief transitions and the besluit | §3c Program.cs 84.8% branch; BL-003 | SM | Med | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-08** | backend/Program.cs | security | Route `DELETE /admin/uploads/{id}` through `CasesAdmin`; delete the orphaned `IsAdmin` gate | BL-003; §7 CQRS-light wrappers row | S | Low | **P1** | 2 | RB-07 | **SIGN-OFF** | **done** |
| **RB-09** | backend/Domain + Program.cs | security | `IIdentityProvider` can express "no identity"; stub Development-only; fail fast in Production | §7 "Single-impl interface `IIdentityProvider`"; BL-006 | S | Med | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-10** | ssp/auth + bhp/auth + ssp/shell | testability | Extract `parseStoredSession` (×2 apps) + spec `redactProfile`; assert a stored BSN yields `''` | §3a auth 42.9%/46.2% (worst FE line, §8); file LH 2/LF 20, BRH 3/BRF 13 | S | Low | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-11** | ssp/brief + libs/shared/infra | security | Dev hatches out of prod on the 3 hand-written `fetch` paths; export their parse boundaries; fix the doc | §3b ssp/brief 42% reach (11/26, none `ui/`); §3a 68.8% branch | M | Med | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-12** | backend/tests (CI) | security gate | One test enumerating the route table; every route hits an authz wrapper or an explicit allow-list | BL-006 (zero backend architecture enforcement) | M | Low | **P1** | 3 | — | **SIGN-OFF** | **done** |
| **RB-13** | ssp/auth + bhp/auth | ADR execution | Land `Session → Principal`; `MedewerkerAdapter`; backoffice login stops being a DigiD/BSN form | BL-002 (211→151 dup after ADR-C-006; expected <40 after this) | M | Med | **P1** | 3 | RB-09 | **SIGN-OFF** | **done** |
| **RB-14** | repo (CI) | security gate | `dotnet list package --vulnerable --include-transitive` as a failing step | BL-006; §7 (the .NET tree is entirely unscanned today) | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-15** | backend/Program.cs | security | Wrap Swagger + the OpenAPI document in `if (app.Environment.IsDevelopment())` | BL-003; §3c Program.cs 97.4%/84.8% | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-16** | backend/Stamdata | input valid. | `DateOnly.TryParse` on `?peildatum=` → 400 instead of an unhandled 500 | §3c Stamdata 96.8% line / **71.7% branch** (BL-005) | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-17** | libs/shared/app + brief + beheer | CQRS-light | Split `runResult` (fold) from `runSubmit` (fold + idempotency mint); point the 5 reads at it | BL-007; §7 "read adapters 20 / mutations inline ~13" | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-18** | backend/Data | security | Key `IdempotencyStore` on `{SubjectId}:{idemKey}` | §7 stores "Not behind any port"; agent 02's Data note (no TTL, no reset) | S | Low | P2 | 3 | RB-17 | **SIGN-OFF** | **done** |
| **RB-19** | backend/Program.cs | structure | Reorder all 48 endpoints under read/write sub-banners; regroup admin-cases + org-template preview | BL-003 (940 lines, file CC 78 vs next-highest 27) | S | **High** | P2 | 4 | RB-12 | **SIGN-OFF** | **done** |
| **RB-20** | ssp/registratie | CQRS-light | `ApplicationsStore.cancel` / `AdminCasesStore.delete` through `runSubmit`; surface the error | BL-007; §7 "Command factories 3" | S | Low | P2 | 4 | — | **SIGN-OFF** | **done** |
| **RB-21** | ssp/registratie | CQRS-light | Extract the read half of `createDraftSync` into `application/find-concept.ts` | §4a `createDraftSync` 143 lines — longest fn in the repo; §9 (>40) | M | Med | P2 | 4 | — | — | **done** |
| **RB-22** | ssp/brief | CQRS-light | _(expand)_ `BriefStore.load()` tolerates a 404 by calling the existing `reset()` once | BL-003; §7 Backend CQRS-light row | S | Low | P2 | 4 | — | **SIGN-OFF** | **done** |
| **RB-23** | backend/Program.cs + Data | CQRS-light | _(contract)_ `GET /brief` 404s when absent; `GetOrCreate``Get` | BL-003; §7 Backend CQRS-light row | S | Med | P2 | 4 | RB-22 | **SIGN-OFF** | **done** |
| **RB-24** | libs/shared/upload | ADR conform. | Move `upload/` into `infrastructure`/`domain`/`application`; **delete** the depcruise carve-out | BL-010; §7 "+1 adapter outside `infrastructure/`", "8 of 9 machines in `domain/`"; §3b shared/domain 0% reach | M | Med | P2 | 5 | — | **SIGN-OFF** | **done** |
| **RB-25** | libs/shared/upload | testability | `UPLOAD_TRANSPORT` injection token (the `SESSION_PORT` shape) instead of `inject(KeepaliveTransport)` | §3a upload 52.0%/50.0%; §3b file unreached, non-`ui/` | S | Low | P2 | 5 | RB-24 | **SIGN-OFF** | **done** |
| **RB-26** | libs/shared/upload | testability | Move the accept/reject decision to `planFileSelection` in `upload.machine.ts` | §3a upload 52.0%/50.0%; §4a module max CC 27 | S | Low | P2 | 5 | RB-24 | **SIGN-OFF** | **done** |
| **RB-27** | libs/shared/upload | testability | Extract `uploadOutcome(status, responseText)` out of the XHR closure | file LH 5/64 (**7.8% line**), BRH 3/57 (**5.3% branch**) | SM | Low | P2 | 5 | RB-25 | **SIGN-OFF** | **done** |
| **RB-28** | libs/beheer + ssp/brief | testability | `BLOB_PRESENTER` token; the 3 commands' success paths become assertable | §3a beheer/application **40.5% branch — worst FE**; brief.store BRH 32/64 | SM | Low | P2 | 5 | — | **SIGN-OFF** | **done** |
| **RB-29** | backend/Domain | testability | Thread the existing `at` through `LetterHtml.ResolveAuto` instead of reading `UtcNow` | §3c Domain 82.0% branch; §4b `LetterHtml.cs` CC 21 | S | Low | P2 | 5 | — | — | **done** |
| **RB-30** | backend/Data + Domain | testability | Extract 5 brief guards into `Domain/Letters/BriefRules.cs`; add `tests/Domain/BriefRuleTests.cs` | §3c Data **75.5% branch** (BL-005); §4b `BriefStore.cs` CC 17, `ToDto` CC 16 | M | Med | P2 | 5 | — | **SIGN-OFF** | **done** |
| **RB-31** | 4 app contexts (specs only) | ADR conform. | Replace hand-rolled state literals with `given(reduce, initial)` replays in 4 machine specs | §7 Elm machines 9 (1 has a `*.testing.ts`); §3a herreg 67.8% / brief 68.8% branch | M | Low | P2 | 6 | — | — | open |
| **RB-32** | libs/shared/docs | ADR conform. | Add the missing `language-switcher` row to the CIBG gap register (9 markers vs 8 rows) | §2 libs/shared 86 files / 5 194 lines; §6 layout Ca 22 | S | Low | P3 | 6 | — | — | open |
| **RB-33** | libs/shared/testing | ADR conform. | Adopt `unwrapOk` at its one call site — **or delete it**; both satisfy ADR-0006 §3 | BL-004; §3a libs/shared/testing 3 files, 100% line | S | Low | P3 | 6 | — | — | open |
| ID | Module | Category | Description | Baseline metric improved | Effort | Risk | Priority | CD batch # | Depends on | Compliance | Status |
| --------- | -------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- | ---------- | ---------- | ------------ | --------------- |
| **RB-01** | backend/Program.cs + Data | security | Add an owner/capability check to `GET /uploads/{id}/content` and `/uploads/status`; 404 not 403 | §3c Data 75.5% branch vs 99.0% line (BL-005) | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-02** | backend/Program.cs + Data | privacy | Stop concatenating the BSN into `AuthzAudit.Resource`; assert on **values** in the test | §3c Data 75.5% branch (BL-005) | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-03** | backend/Contracts | privacy | `MaskTail(a.Owner, 3)` in `ToAdminSummaryDto` — both cross-owner lists inherit it | §3a bhp/behandeling 91.6%/81.5%; §7 Mapping row | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-04** | backend/Data | privacy | Mask the BSN used as `AuditEntry.Actor` on document audit rows (ownership column untouched) | §3c Data 99.0% line / 75.5% branch | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-05** | backend/Zgw | privacy | Drop the BSN-bearing query + body snippet from the `ZgwHttpClient` exception message | §3c Zgw 98.1%/85.5% (best backend branch) — a design gap, not a test gap | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-06** | backend/Program.cs | security | Delete the dead `POST /registrations` (no FE caller) — or add the `ForeignIds` guard | BL-003 (48 mappings in 940 lines, file CC 78) | S | Low | **P1** | 1 | — | **SIGN-OFF** | **done** |
| **RB-07** | backend/Program.cs | audit | Audit the **allow** path in all five authz gates + the 3 brief transitions and the besluit | §3c Program.cs 84.8% branch; BL-003 | SM | Med | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-08** | backend/Program.cs | security | Route `DELETE /admin/uploads/{id}` through `CasesAdmin`; delete the orphaned `IsAdmin` gate | BL-003; §7 CQRS-light wrappers row | S | Low | **P1** | 2 | RB-07 | **SIGN-OFF** | **done** |
| **RB-09** | backend/Domain + Program.cs | security | `IIdentityProvider` can express "no identity"; stub Development-only; fail fast in Production | §7 "Single-impl interface `IIdentityProvider`"; BL-006 | S | Med | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-10** | ssp/auth + bhp/auth + ssp/shell | testability | Extract `parseStoredSession` (×2 apps) + spec `redactProfile`; assert a stored BSN yields `''` | §3a auth 42.9%/46.2% (worst FE line, §8); file LH 2/LF 20, BRH 3/BRF 13 | S | Low | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-11** | ssp/brief + libs/shared/infra | security | Dev hatches out of prod on the 3 hand-written `fetch` paths; export their parse boundaries; fix the doc | §3b ssp/brief 42% reach (11/26, none `ui/`); §3a 68.8% branch | M | Med | **P1** | 2 | — | **SIGN-OFF** | **done** |
| **RB-12** | backend/tests (CI) | security gate | One test enumerating the route table; every route hits an authz wrapper or an explicit allow-list | BL-006 (zero backend architecture enforcement) | M | Low | **P1** | 3 | — | **SIGN-OFF** | **done** |
| **RB-13** | ssp/auth + bhp/auth | ADR execution | Land `Session → Principal`; `MedewerkerAdapter`; backoffice login stops being a DigiD/BSN form | BL-002 (211→151 dup after ADR-C-006; expected <40 after this) | M | Med | **P1** | 3 | RB-09 | **SIGN-OFF** | **done** |
| **RB-14** | repo (CI) | security gate | `dotnet list package --vulnerable --include-transitive` as a failing step | BL-006; §7 (the .NET tree is entirely unscanned today) | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-15** | backend/Program.cs | security | Wrap Swagger + the OpenAPI document in `if (app.Environment.IsDevelopment())` | BL-003; §3c Program.cs 97.4%/84.8% | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-16** | backend/Stamdata | input valid. | `DateOnly.TryParse` on `?peildatum=` → 400 instead of an unhandled 500 | §3c Stamdata 96.8% line / **71.7% branch** (BL-005) | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-17** | libs/shared/app + brief + beheer | CQRS-light | Split `runResult` (fold) from `runSubmit` (fold + idempotency mint); point the 5 reads at it | BL-007; §7 "read adapters 20 / mutations inline ~13" | S | Low | P2 | 3 | — | **SIGN-OFF** | **done** |
| **RB-18** | backend/Data | security | Key `IdempotencyStore` on `{SubjectId}:{idemKey}` | §7 stores "Not behind any port"; agent 02's Data note (no TTL, no reset) | S | Low | P2 | 3 | RB-17 | **SIGN-OFF** | **done** |
| **RB-19** | backend/Program.cs | structure | Reorder all 48 endpoints under read/write sub-banners; regroup admin-cases + org-template preview | BL-003 (940 lines, file CC 78 vs next-highest 27) | S | **High** | P2 | 4 | RB-12 | **SIGN-OFF** | **done** |
| **RB-20** | ssp/registratie | CQRS-light | `ApplicationsStore.cancel` / `AdminCasesStore.delete` through `runSubmit`; surface the error | BL-007; §7 "Command factories 3" | S | Low | P2 | 4 | — | **SIGN-OFF** | **done** |
| **RB-21** | ssp/registratie | CQRS-light | Extract the read half of `createDraftSync` into `application/find-concept.ts` | §4a `createDraftSync` 143 lines — longest fn in the repo; §9 (>40) | M | Med | P2 | 4 | — | — | **done** |
| **RB-22** | ssp/brief | CQRS-light | _(expand)_ `BriefStore.load()` tolerates a 404 by calling the existing `reset()` once | BL-003; §7 Backend CQRS-light row | S | Low | P2 | 4 | — | **SIGN-OFF** | **done** |
| **RB-23** | backend/Program.cs + Data | CQRS-light | _(contract)_ `GET /brief` 404s when absent; `GetOrCreate``Get` | BL-003; §7 Backend CQRS-light row | S | Med | P2 | 4 | RB-22 | **SIGN-OFF** | **done** |
| **RB-24** | libs/shared/upload | ADR conform. | Move `upload/` into `infrastructure`/`domain`/`application`; **delete** the depcruise carve-out | BL-010; §7 "+1 adapter outside `infrastructure/`", "8 of 9 machines in `domain/`"; §3b shared/domain 0% reach | M | Med | P2 | 5 | — | **SIGN-OFF** | **done** |
| **RB-25** | libs/shared/upload | testability | `UPLOAD_TRANSPORT` injection token (the `SESSION_PORT` shape) instead of `inject(KeepaliveTransport)` | §3a upload 52.0%/50.0%; §3b file unreached, non-`ui/` | S | Low | P2 | 5 | RB-24 | **SIGN-OFF** | **done** |
| **RB-26** | libs/shared/upload | testability | Move the accept/reject decision to `planFileSelection` in `upload.machine.ts` | §3a upload 52.0%/50.0%; §4a module max CC 27 | S | Low | P2 | 5 | RB-24 | **SIGN-OFF** | **done** |
| **RB-27** | libs/shared/upload | testability | Extract `uploadOutcome(status, responseText)` out of the XHR closure | file LH 5/64 (**7.8% line**), BRH 3/57 (**5.3% branch**) | SM | Low | P2 | 5 | RB-25 | **SIGN-OFF** | **done** |
| **RB-28** | libs/beheer + ssp/brief | testability | `BLOB_PRESENTER` token; the 3 commands' success paths become assertable | §3a beheer/application **40.5% branch — worst FE**; brief.store BRH 32/64 | SM | Low | P2 | 5 | — | **SIGN-OFF** | **done** |
| **RB-29** | backend/Domain | testability | Thread the existing `at` through `LetterHtml.ResolveAuto` instead of reading `UtcNow` | §3c Domain 82.0% branch; §4b `LetterHtml.cs` CC 21 | S | Low | P2 | 5 | — | — | **done** |
| **RB-30** | backend/Data + Domain | testability | Extract 5 brief guards into `Domain/Letters/BriefRules.cs`; add `tests/Domain/BriefRuleTests.cs` | §3c Data **75.5% branch** (BL-005); §4b `BriefStore.cs` CC 17, `ToDto` CC 16 | M | Med | P2 | 5 | — | **SIGN-OFF** | **done** |
| **RB-31** | 4 app contexts (specs only) | ADR conform. | Replace hand-rolled state literals with `given(reduce, initial)` replays in 4 machine specs | §7 Elm machines 9 (1 has a `*.testing.ts`); §3a herreg 67.8% / brief 68.8% branch | M | Low | P2 | 6 | — | — | **implemented** |
| **RB-32** | libs/shared/docs | ADR conform. | Add the missing `language-switcher` row to the CIBG gap register (9 markers vs 8 rows) | §2 libs/shared 86 files / 5 194 lines; §6 layout Ca 22 | S | Low | P3 | 6 | — | — | open |
| **RB-33** | libs/shared/testing | ADR conform. | Adopt `unwrapOk` at its one call site — **or delete it**; both satisfy ADR-0006 §3 | BL-004; §3a libs/shared/testing 3 files, 100% line | S | Low | P3 | 6 | — | — | open |
---
@@ -0,0 +1,155 @@
# RB-31 — replay real messages in the four hand-rolling machine specs
Status: **implemented** · 2026-08-28 · Source finding: `06-adr-conformance.md` ADR-C-010 ·
`99-backlog.md` RB-31
RB-31 replaces four hand-rolled state-literal fixtures with `given(reduce, initial)`
replays, per ADR-0006 §2 ("no object is built directly; a fixture is the result of
running real `Msg`s through the real `reduce`"). This is a fixture-construction change
only. No `*.machine.ts` production file was touched.
## What was wrong
Four machine specs built their starting `Answering`/`Invullen`/`Editing`/`loaded` state
with a local object-literal helper instead of replaying messages:
- `intake.machine.spec.ts``answering(answers, cursor, scholingThreshold)` hardcoded
`errors: {}`. `intake.testing.ts` (exporting `givenIntake`) already existed next to
it and was already correct, but was imported only by `intake.acceptance.spec.ts`.
- `registratie-wizard.machine.spec.ts``invullen(draft, cursor)` hardcoded `errors: {}`
and `upload: initialUpload`.
- `besluit.machine.spec.ts``editingWith(besluit, toelichting)` hardcoded `errors: {}`.
- `brief.machine.spec.ts``loaded(status, sections)` built the `'loaded'` tag object
directly (no `errors` field on this union, so this one did not hardcode `errors: {}`,
but it still skipped the reducer).
## What changed
| File | Change |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/ssp/src/app/herregistratie/domain/intake.machine.spec.ts` | Removed the local `answering(...)` helper. Every fixture is now built with the existing `givenIntake` (imported from `intake.testing.ts`), matching `intake.acceptance.spec.ts`'s own style. |
| `apps/ssp/src/app/registratie/domain/registratie-wizard.testing.ts` | New. One-liner: `export const givenRegistratieWizard = given(reduce, initial)`. |
| `apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts` | Removed the local `invullen(...)` helper and the now-unused `validAdres`/`validDraft`/`Draft`/`initialUpload` fixtures. Added module-level replay helpers (`toAdresValid`, `toBeroepStep`, `toBeroepStepWithDiploma`, `toControleStep`, `toIndienen`, `toFullDraftAtCursor0`) built from `givenRegistratieWizard` + `reduce`, reused across every `describe` block (the file's pre-existing `reduce (message-driven happy path)` block already had three of these, scoped locally; they are now module-level and shared, removing the duplication). |
| `apps/behandelportal/src/app/behandeling/domain/besluit.testing.ts` | New. One-liner: `export const givenBesluit = given(reduce, initial)`. |
| `apps/behandelportal/src/app/behandeling/domain/besluit.machine.spec.ts` | Removed the local `editingWith(...)` helper. Every fixture is now built with `givenBesluit` (or, for the empty-draft case, the machine's own `initial` — see below). |
| `apps/ssp/src/app/brief/domain/brief.testing.ts` | New. One-liner: `export const givenBrief = given(reduce, initial)`. |
| `apps/ssp/src/app/brief/domain/brief.machine.spec.ts` | Rewrote the `loaded(...)` helper to replay a real `BriefLoaded` message through `givenBrief` instead of building the `'loaded'` tag object directly. Also converted one further inline `BriefState` literal in the "deep-copies content" test to the same replay (same anti-pattern, same file, not named individually by the finding's evidence list but visibly the same shape — see "Beyond the letter of the finding" below). |
| `docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md` | RB-31's status cell: `open``implemented`. |
## Message sequence used per machine
### `intake.machine.spec.ts`
Every fixture in this file has `cursor` 0, 1, or 2 and the default or an overridden
`scholingThreshold`. All are built as direct sequences of `SetAnswer`/`Next`/`SetPolicy`
through `givenIntake`, mirroring `intake.acceptance.spec.ts`'s own explicit style (no new
generic wrapper was added — the finding's own resolution is "wire the spec to
`givenIntake`", not "invent a second helper"). Representative sequences:
- Cursor 0, plain answers (most tests): `givenIntake({SetAnswer buitenlandGewerkt}, {SetAnswer uren}, ...)`.
- Cursor 0 with an overridden threshold: adds a trailing `{tag:'SetPolicy', scholingThreshold: N}`.
- Cursor 1 ("editing an answer leaves the cursor fixed"): `SetAnswer buitenlandGewerkt=ja`,
`SetAnswer land`, `SetAnswer buitenlandseUren`, `Next` (buitenland step now valid ->
cursor 1), then the edit under test.
- Cursor 2 ("gaNaarStap jumps back..."): the above sequence continued with
`SetAnswer uren=4160`, `Next` (werk step valid -> cursor 2).
No drift found here: `intake.testing.ts` already existed correctly, and every one of the
nine cursor/threshold combinations the old literal used turned out to be reachable by a
real message sequence.
### `registratie-wizard.machine.spec.ts`
- `toAdresValid()` = `PrefillAdres(straat, postcode, woonplaats)`, `SetCorrespondentie('post')`
— cursor 0, matches the old `invullen(validAdres)`.
- `toBeroepStep()` = `reduce(toAdresValid(), Next)` — cursor 0 -> 1, no diploma. Matches
`invullen(validAdres, 1)`.
- `toBeroepStepWithDiploma()` = `reduce(toBeroepStep(), KiesDiploma('d1','Arts',[]))`
cursor 1, diploma set. Matches `invullen(validDraft, 1)`.
- `toControleStep()` = `reduce(toBeroepStepWithDiploma(), Next)` — cursor 1 -> 2. Matches
`invullen(validDraft, 2)`.
- `toFullDraftAtCursor0()` = `PrefillAdres`, `SetCorrespondentie('post')`, `KiesDiploma(...)`,
never advancing the cursor — matches `invullen(validDraft)` (cursor 0). `SetField`/
`SetCorrespondentie`/`KiesDiploma` carry no cursor gate, so setting every field before
ever pressing `Next` is a genuinely reachable cursor-0 state with a complete draft.
- `invullen({})` (five call sites) is exactly the machine's own `initial` value
(`{tag:'Invullen', draft:{antwoorden:{}}, cursor:0, errors:{}, upload:initialUpload}`)
— replaced with `initial` directly, no message needed.
### `besluit.machine.spec.ts`
- `editingWith('')` is exactly `initial` (`draft:{besluit:'',toelichting:''}`) — replaced
with `initial` directly.
- `editingWith('Afwijzen')` / `editingWith('Goedkeuren')` = `givenBesluit({SetField besluit})`.
- `editingWith('Afwijzen', ' niet erkend ')` = `givenBesluit({SetField besluit=Afwijzen}, {SetField toelichting=' niet erkend '})`.
- Every `Submitting`/`Failed` fixture is now `givenBesluit({SetField besluit}, {Submit})`
composed further with `reduce(..., {SubmitFailed}/{Retry}/{Reset})`.
### `brief.machine.spec.ts`
- `loaded(status, sections)` = `givenBrief({tag:'BriefLoaded', brief: briefWith(status, sections), availablePassages: lib, decisions})`.
This is a 1:1 replacement: the `'BriefLoaded'` reducer case sets exactly
`{tag:'loaded', brief: m.brief, availablePassages: m.availablePassages, decisions: m.decisions}`
— the same three fields the old literal built by hand, with the same values. No drift.
## Drift found
Two tests in `registratie-wizard.machine.spec.ts` asserted against a cursor value the
real reducer cannot reach:
- `'validateAll keeps only the answers to the questions that applied'` built
`invullen(validAdres, 2)` then called `kiesDiploma(...)` on it — i.e. a wizard already
at cursor 2 (`controle`) with **no diploma chosen yet**. That is impossible by replay:
advancing past `beroep` (cursor 1 -> 2) requires `validateStep('beroep', ...)` to pass,
which requires `diplomaId` and `beroep` to already be set. The literal encoded a state
the reducer can never produce.
- `'requires a declared beroep + all maximal questions before submit'` had the same
problem: `invullen(validAdres, 2)` then `kiesHandmatig(...)`, which leaves `beroep`
`undefined` — again a cursor-2 state that could never have been reached via `Next`.
In both cases the cursor value is not actually load-bearing for the test: `submit()`
calls `validateAll(s.draft, s.upload)`, which validates every step regardless of
`s.cursor`. Both tests were re-pointed at the reachable **cursor-1** equivalent
(`toBeroepStep()` then `kiesDiploma`/`kiesHandmatig`), with an inline `// DRIFT (see
rb-31.md)` comment at each site. No assertion changed — both tests still check the same
`submit(...)` outcome on the same field values; only the now-irrelevant cursor number
in the starting fixture moved from an unreachable 2 to a reachable 1.
No other named state, across any of the four machines, turned out to be unreachable.
## Beyond the letter of the finding
`brief.machine.spec.ts`'s `'BesluitSelected deep-copies content...'` test built a second,
separate `BriefState` literal inline (not through the `loaded(...)` helper the finding
cited) — same anti-pattern, same file, not itself named in ADR-C-010's evidence list.
Since it sits inside one of the four files already being brought into line, and the fix
is the identical one-line `BriefLoaded` replay, it was converted too rather than left as
a residual violation in a file this ticket otherwise fixed. No other spec, in any other
file, was touched.
## `intake.acceptance.spec.ts` — confirmed unaffected
`intake.testing.ts` and its `givenIntake` export were not modified. The acceptance spec
still imports and uses `givenIntake` exactly as before; it was not read or edited by
this ticket beyond confirming (by running it) that it still passes.
## Verification
- `npx ng test ssp`: 44 test files, 276 tests, all passing (includes
`intake.machine.spec.ts`, `intake.acceptance.spec.ts`,
`registratie-wizard.machine.spec.ts`, `brief.machine.spec.ts`, and every other ssp
spec, unmodified ones included).
- `npx ng test behandelportal`: 6 test files, 37 tests, all passing (includes
`besluit.machine.spec.ts`).
- `npx eslint` on all seven touched/added files: clean.
- `npx prettier --check` on all seven touched/added files: clean (one file needed
`--write` once, then verified clean).
- `npm run ci`: see the commit message / session report for the exit code and step
count.
## What this ticket did not touch
No `*.machine.ts` reducer or production domain file was changed — every fixture change
is confined to the four `*.spec.ts` files and the three new `*.testing.ts` files listed
above. No other machine spec (including `change-request.machine.spec.ts`, which the
finding notes already honours the idiom inline) was touched.