Runs the multi-agent refactoring-backlog pipeline in docs/project/ refactor-backlog-setup/ up to and including three of the seven Phase 1 agents. 00-baseline.md establishes the metrics every later agent must cite, using only tooling already in the repo (vitest lcov, coverlet cobertura, ESLint's core `complexity` rule at threshold 0 for a full distribution, depcruise --metrics). Duplication and C# complexity had no tooling, so tools/baseline-scan.mjs adds a deterministic ~200-line text scan rather than a new dependency; the approximations are labelled as such. Headline: FE 75.1% line coverage but only over the 98 of 220 source files a spec loads; BE 97.6% line / 79.6% branch; 0 layering violations; 7.1% duplication; 25 of 2085 TS functions over CC 10. Then 02-testability, 04-cqrs-light and 06-adr-conformance (27 findings). 01/03/05 were skipped deliberately — the baseline shows little for them to find; 07 (BIO2) and 08 (consolidation) are still open. Each agent corrected a baseline observation of mine, and in every case the error was in something derived rather than measured: - BL-007 counted ~13 adapter "mutations" from the `runSubmit` helper name; 5 of those call sites are reads. It also missed 3 real mutations that reach the raw ApiClient and never return a Result. - BL-002 diagnosed the 100%-duplicated auth folders as ADR-0002's divergence prediction failing. It never had a chance to fail: §3's `Principal` union was never built. - BL-004 named libs/shared/domain and libs/beheer/contracts as coverage gaps; both are pure type declarations where 0% is unimprovable. All three corrections are recorded inline in 00-baseline.md §10, so agent 08 does not inherit the bad numbers. .prettierignore excludes the agent prompt directories — reflowing their markdown would edit the prompt text itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
642 lines
41 KiB
Markdown
642 lines
41 KiB
Markdown
## Scope: apps/ssp (auth, registratie, herregistratie, brief, showcase+shell+root), apps/behandelportal (auth, behandeling, shell+root), libs/shared (domain, application, infrastructure, ui, layout, kernel, upload, testing), libs/beheer, backend (Program.cs, Domain, Data, Zgw, Contracts, Stamdata, tests)
|
||
|
||
## Status: complete
|
||
|
||
## Last updated: 2026-08-26
|
||
|
||
## Depends on: 00-baseline.md
|
||
|
||
## ---
|
||
|
||
# 02 — Testability
|
||
|
||
What blocks a **unit** test — static/singleton dependencies, hidden I/O, work in
|
||
constructors/field initializers, pure logic entangled with impure. Every finding cites a
|
||
`BL-###` or a metric row from `00-baseline.md`. Seams proposed are extractions, never
|
||
rewrites.
|
||
|
||
## How this file reads the baseline
|
||
|
||
Three filters were applied before anything was written down, and they killed more
|
||
candidates than they kept:
|
||
|
||
1. **BL-004's carve-out is honoured.** No finding is filed against a `ui/` (or
|
||
`layout/` component) file for lacking a Vitest spec. 66 Storybook stories + the a11y
|
||
addon are the house strategy (CLAUDE.md §5), not a gap.
|
||
2. **"Untested" ≠ "untestable".** Several of the worst-covered non-`ui/` files are
|
||
perfectly injectable and simply have no spec (`submit-besluit.ts`,
|
||
`Contracts/Mappers.cs`, `breadcrumb-trail.ts`). Those are noted in their module
|
||
section but not filed as testability findings — there is no seam to add. Whoever
|
||
owns coverage should pick them up.
|
||
3. **Already-covered code needs a positive argument.** Backend line coverage is 97.6%
|
||
(BL-005); a "this is untestable" claim there has to point at a branch the current
|
||
test shape genuinely cannot reach. Two do (TE-007, TE-008); one points at the cost
|
||
of how it is reached (TE-009).
|
||
|
||
**Two baseline items are closed as false gaps** — see `libs/shared/domain` and
|
||
`libs/beheer/contracts` below. BL-004 names both as "genuine gaps"; on inspection
|
||
neither contains an executable statement.
|
||
|
||
**Deliberate decisions engaged with, not overridden:** the 7 static backend stores
|
||
(documented in `Data/Db.cs`) are left alone — TE-009 extracts rules _out_ of one of
|
||
them without touching its shape. BL-002's auth duplication is respected — TE-004 lands
|
||
the same seam twice rather than proposing a shared extraction.
|
||
|
||
---
|
||
|
||
## apps/ssp — auth
|
||
|
||
**TE-001 — `SessionStore.restore()` reads `localStorage` inline, so its shape guard cannot be unit-tested**
|
||
|
||
- Module / file:line — `apps/ssp/src/app/auth/application/session.store.ts:12-21`
|
||
- **What blocks unit testing.** `restore()` is module-private and calls
|
||
`localStorage.getItem(STORAGE_KEY)` itself, then does the parse + shape validation in
|
||
the same function. It is invoked from a field initializer
|
||
(`private _session = signal<Session | null>(restore())`, L37), so the storage read
|
||
happens the instant the singleton is constructed. A spec cannot feed it a raw string;
|
||
it must stub the `localStorage` global before the injector builds the store. The
|
||
logic being guarded is not incidental — the comments mark it G1 (never persist the
|
||
BSN) and G2 (validate the shape before trusting it), i.e. a trust boundary, and
|
||
CLAUDE.md §5 mandates a spec for boundary `parse*` adapters.
|
||
- **Baseline citation.** §3a: `ssp/auth` 42.9% line / 46.2% branch — **jointly the worst
|
||
line coverage in the frontend table** (§8 ranking). Per-file lcov for this file:
|
||
**LH 2 / LF 20 (10.0% line), BRH 3 / BRF 13 (23.1% branch)** — 4 of the module's 6
|
||
files are spec-reached (§3b, 67%), yet this one barely executes.
|
||
- **Minimal seam.** Split the pure half out and move it next to the type it produces:
|
||
`export function parseStoredSession(raw: string | null): Session | null` in
|
||
`auth/domain/session.ts` — which **already has a spec file**
|
||
(`auth/domain/session.spec.ts`) and is pure TS, so no new test scaffolding is needed.
|
||
`restore()` collapses to `parseStoredSession(localStorage.getItem(STORAGE_KEY))`. Three
|
||
test cases (absent, non-JSON, wrong shape) cover the guard.
|
||
- **Effort S.** Independently shippable in one deploy — pure move, no call-site change
|
||
outside the file.
|
||
- **Note on BL-002.** `bhp/auth` carries the identical function; the seam lands **twice**,
|
||
once per app. That is correct, not duplication to fix — ADR-0002 / CLAUDE.md §1 make
|
||
`auth` deliberately unshared, and BL-002 flags any extract-to-shared here as
|
||
contradicting an accepted ADR. Agent 06 owns whether that prediction still holds.
|
||
|
||
## apps/ssp — registratie
|
||
|
||
**No findings.**
|
||
|
||
The module's shape is the reason. Every `parse*` in its six adapters is exported and
|
||
directly spec'd (`applications`, `big-register`, `brp`, `dashboard-view`, `duo` all have
|
||
`.spec.ts` files); the machines are pure `domain/` units with specs; the five value
|
||
objects each have one.
|
||
|
||
`createDraftSync` deserves an explicit acquittal: at 143 lines it is the longest function
|
||
in the repo (§4a, "Functions over 75 lines — the entire population") and it owns a
|
||
`setTimeout` debounce, a `Router` navigation and an in-flight-create race guard. It is
|
||
nevertheless **the best-seamed effectful unit in the frontend** — deps arrive through an
|
||
explicit `DraftSyncDeps` object (`draft-sync.ts:24-33`), `Router`/`ActivatedRoute` are
|
||
`inject(..., { optional: true })` so it is inert without them, and `enabled()` exists
|
||
specifically so stories and tests can neutralize it (L31-32). It has a spec. Its length
|
||
is agent 01's call, not a testability defect.
|
||
|
||
`BigProfileStore` creates two `resource()`s in field initializers (constructor-time I/O),
|
||
which is normally a blocker — but the store is pure glue over `parseDashboardView`
|
||
(exported, spec'd) and `map`/`fromResource` (spec'd), so there is no untested decision
|
||
hiding behind the construction. §3a: 80.0% line / 77.3% branch, §3b 51% reach with the
|
||
20 unreached files being 11 `ui/` components (BL-004) and 3 pure-type `contracts/` files.
|
||
|
||
## apps/ssp — herregistratie
|
||
|
||
**No findings.** §3a 70.9% / 67.8%, §3b 56% reach. The four unreached files are the
|
||
`ui/` pages and wizard organisms (BL-004) plus `intake-policy.store.ts`, a thin
|
||
`resource()` wrapper over the exported-and-spec'd `parseIntakePolicy`. Both machines are
|
||
pure, Angular-free and carry four spec files between them, including an acceptance spec.
|
||
`intake.testing.ts` gives the wizard specs a fixture builder — the seam already exists.
|
||
|
||
## apps/ssp — brief
|
||
|
||
**TE-002 — `RevealBigNummerAdapter` hides a trust boundary inside a global-`fetch` method**
|
||
|
||
- Module / file:line — `apps/ssp/src/app/brief/infrastructure/reveal-bignummer.adapter.ts:32-41`
|
||
- **What blocks unit testing.** The shape validation of the response body — the code's
|
||
own comment calls it a "Trust boundary" — is written inline inside `async reveal()`,
|
||
after an `await fetch(...)` on the **global** `fetch` (L24). There is no injected transport. To assert that a
|
||
`{ bigNummer: 42 }` response is rejected, a spec must stub `globalThis.fetch`; the
|
||
boundary itself is not callable. Every other `parse*` in the repo is exported (30 of
|
||
them, §7) — this one is the outlier, and it guards a PII reveal (PRD-0002 §5c). The
|
||
same shape recurs in the sibling hand-written-`fetch` adapters:
|
||
`letter-preview.adapter.ts:56-62` (`errorMessage`) and
|
||
`org-template.adapter.ts:82-89` (the proefbrief error mapping) — both un-exported,
|
||
both unreachable without a `fetch` stub.
|
||
- **Baseline citation.** §3b: `ssp/brief` **42% spec reach** (11 of 26 files) — the
|
||
lowest of any non-zero context outside `bhp/behandeling`; §3a 75.3% line / **68.8%
|
||
branch**. All three `fetch` adapters are among the 15 unreached files, and none of
|
||
them is a `ui/` component, so BL-004's Storybook carve-out does not cover them.
|
||
- **Minimal seam.** Export the pure half as a named boundary, matching the file's 30
|
||
siblings: `export function parseRevealed(body: unknown): Result<string, string>` —
|
||
five lines moved verbatim out of the method, which becomes
|
||
`return res.ok ? parseRevealed(await res.json().catch(() => null)) : err(...)`. Same
|
||
move for `errorMessage` in the other two adapters (already separate functions; they
|
||
only need `export` + a spec). No transport abstraction, no `HttpClient` migration —
|
||
the hand-written `fetch` stays, and the documented reasons for it
|
||
(`.ExcludeFromDescription()`, per-request headers) are untouched.
|
||
- **Effort S.** Independently shippable.
|
||
|
||
## apps/ssp — showcase, shell, root
|
||
|
||
**No findings.**
|
||
|
||
§3b lists `ssp/root` + `ssp/shell` (with the behandelportal equivalents) at **0% reach,
|
||
12 files**. That is the correct number for what these files are: `main.ts`,
|
||
`app.config.ts`, `app.routes.ts`, `app.ts` and `shell/nav.config.ts` are composition
|
||
roots and static data — a spec asserting a provider array restates it. `debug-state`
|
||
is a dev-only devtool with a Storybook story.
|
||
|
||
One honest note, no ticket: `shell/debug-state/mask.ts::redactProfile` is a pure,
|
||
Angular-free PII-redaction function (it maps a `BigProfile` to a redacted shape) with no
|
||
spec and **no blocker** — it is directly callable today. Its dependencies
|
||
(`maskTail`, `REDACTED`) are in `libs/shared/kernel/pii.ts`, which is spec'd at 96.4%.
|
||
Missing test, not blocked test.
|
||
|
||
`showcase` is 100% line coverage on its one reached file (§3a); `snippets.generated.ts`
|
||
is generated and `concepts.page.ts` is a teaching page.
|
||
|
||
## apps/behandelportal — auth
|
||
|
||
**TE-001 applies here identically** — `apps/behandelportal/src/app/auth/application/session.store.ts:12-21`,
|
||
same `restore()`, same `localStorage` read in a field initializer, same §3a row
|
||
(`bhp/auth` 42.9% / 46.2%). Fix it in `apps/behandelportal/src/app/auth/domain/session.ts`,
|
||
which also already has a spec. Counted once as TE-001; it is two commits, or one commit
|
||
touching two apps.
|
||
|
||
No additional findings. `medewerker.ts:17-22` reads `window.location.search` +
|
||
`sessionStorage` directly, but it is a five-line dev-only role stand-in with the same
|
||
shape as `libs/shared/infrastructure/role.ts` — whose twin `subject.ts` is spec'd, so
|
||
the pattern is demonstrably testable as written.
|
||
|
||
## apps/behandelportal — behandeling
|
||
|
||
**No findings.**
|
||
|
||
§3b 31% reach (5 of 16) looks alarming and is not: the 11 unreached are 5 `ui/` files
|
||
(BL-004), the three `resource()`-wrapper stores, and the two command factories. §3a
|
||
records **91.6% line / 81.5% branch** on what is reached — the highest line coverage of
|
||
any frontend module in the table.
|
||
|
||
Explicitly not a testability finding: `application/submit-besluit.ts` is
|
||
**structurally identical** to `apps/ssp/src/app/registratie/application/submit-change-request.ts`,
|
||
which has a spec (`submit-change-request.spec.ts`). Same `inject()` + `runSubmit`
|
||
factory, same signature shape. It is not blocked by anything; it is a missing spec whose
|
||
template already exists in the repo. Both adapters' `parse*` functions are exported and
|
||
spec'd (`beoordeling.adapter.spec.ts`, `werkvoorraad.adapter.spec.ts`).
|
||
|
||
## apps/behandelportal — shell, root
|
||
|
||
**No findings.** Same composition-root reasoning as `ssp/shell + root` above.
|
||
|
||
## libs/shared — domain
|
||
|
||
**No findings — BL-004's "genuine gap" is a false positive here, and can be closed.**
|
||
|
||
§3b lists `libs/shared/domain` at **0% reached, 3 files**, and BL-004 names it first
|
||
among "the genuine gaps are non-`ui/` files with no spec". Reading all three files
|
||
(30 lines total): `capability.ts` is a 9-member string-literal union, `role.ts` is a
|
||
3-member union, `feature-flag.ts` is one `interface` plus one exported string constant.
|
||
**There is no executable statement in the folder.** 0% is the correct and unimprovable
|
||
number; the types are checked by `tsc` and their runtime counterparts are validated in
|
||
`parseMe` (spec'd, 94.7% infrastructure coverage). No ticket should be written against
|
||
this row.
|
||
|
||
## libs/shared — application
|
||
|
||
**No findings.**
|
||
|
||
§3a 80.3% / 70.0%, §3b 73% reach (8 of 11). The three unreached are `session.port.ts`
|
||
(an `InjectionToken` + interface — a declaration, nothing to run), `feature-flags.store.ts`
|
||
and `access.store.ts`. The seam kit itself (`remote-data`, `store`, `submit`,
|
||
`history`, `pending-saves`, `machine-remote-data`, `debounced-save`) is fully spec'd —
|
||
this is the folder that makes the rest of the frontend testable.
|
||
|
||
Noted without a ticket: `AccessStore.can()` (`access.store.ts:34-37`) is a
|
||
deny-by-default security gate whose decision reduces to
|
||
`rd.tag === 'Success' && rd.value.includes(capability)` over a `resource()` created in a
|
||
field initializer. The decision is two lines; the substance it guards
|
||
(`parseMe`, where a real silent-deny bug shipped — see the WP-66 regression test in
|
||
`me.adapter.spec.ts:26-31`) is already exported and thoroughly spec'd. Extracting a pure
|
||
`canFrom(rd, cap)` would be honest but buys close to nothing. Filing it would be volume,
|
||
not quality.
|
||
|
||
## libs/shared — infrastructure
|
||
|
||
**No findings.**
|
||
|
||
§3a 94.7% line / 81.0% branch, §3b 82% reach — the second-best module in the repo.
|
||
BL-001 singles out `api-client.provider.ts:49 fetch` (CC 19) as one of only two CC>10
|
||
functions outside the mandated idioms, so it is worth stating why it is _not_ a
|
||
testability finding: `httpClientFetch(http: HttpClient)` takes its dependency as an
|
||
ordinary function parameter (L47) rather than injecting it, and it has a spec
|
||
(`api-client.provider.spec.ts`). Its complexity is agent 01's call. The module-level
|
||
mutable `pendingIdempotencyKey` (L21) is self-clearing in a `finally` (L25), so it does
|
||
not leak between tests.
|
||
|
||
## libs/shared — ui
|
||
|
||
**No findings — BL-004 governs.** 34 files, 13 reached; the unreached 21 are components
|
||
covered by the Storybook + a11y strategy CLAUDE.md §5 mandates. The one non-component
|
||
module in the folder, `rich-text-editor/rich-text-dom.ts` (home of `collect`, CC 11 —
|
||
the other non-idiom CC>10 function per BL-001), **is** spec'd
|
||
(`rich-text-dom.spec.ts`). The layer is doing what the house rules ask.
|
||
|
||
## libs/shared — layout
|
||
|
||
**No findings.**
|
||
|
||
§3b 18% reach (2 of 11) is the lowest non-zero row, but 8 of the 9 unreached are
|
||
components (`shell`, `page-shell`, `site-header`, `site-footer`, `breadcrumb`,
|
||
`language-switcher`, `wizard-shell`) — BL-004 applies to `layout/` exactly as to `ui/`,
|
||
since CLAUDE.md §5 titles both under `Design System/`.
|
||
|
||
Two non-component files, neither ticketed:
|
||
|
||
- `breadcrumb/breadcrumb-trail.ts::trailFor` is a pure exported function with a subtle
|
||
parent-walk and a `delete trail[last].link` mutation, and has no spec. **No blocker** —
|
||
it is directly callable, and its sibling `language-switcher/locale-links.ts` is the
|
||
spec'd proof. Missing test, not blocked test.
|
||
- `route-focus.ts` is a 20-line `ENVIRONMENT_INITIALIZER` wrapping a `Router` subscription
|
||
and `afterNextRender`. Genuinely awkward to unit-test, but it is a11y wiring with no
|
||
branch worth asserting; a seam here would cost more than it returns.
|
||
|
||
## libs/shared — kernel
|
||
|
||
**No findings.** §3a 96.4% line / 90.0% branch, §3b **100% reach** — the best module in
|
||
the repo, and (§6) the most-depended-on at I = 5% with Ca 71. Pure functions, all spec'd.
|
||
This is the reference standard the other findings point back at.
|
||
|
||
## libs/shared — upload
|
||
|
||
Three findings. This module carries the frontend's weakest testability profile, and
|
||
BL-010 already flags it as sitting outside the layer convention.
|
||
|
||
**TE-003 — `UploadShellService` declares a port, then injects the concrete class instead**
|
||
|
||
- Module / file:line — `libs/shared/src/upload/upload-shell.service.ts:12-24` and `:35`
|
||
- **What blocks unit testing.** The file defines `export interface UploadTransport` and
|
||
documents it as _the_ swap seam ("swapping it in touches only this interface", L10-11).
|
||
It then binds it as
|
||
`private transport: UploadTransport = inject(KeepaliveTransport)` (L35) — the
|
||
**concrete class**, which is `@Injectable` but **not exported** (L18). A spec that
|
||
wants a fake transport cannot reference the class to override its provider, and cannot
|
||
provide against the interface (interfaces are not DI tokens). Result: every one of
|
||
`upload()`, `delete()`, `cancel()` and `pollReturning()` — the code that translates
|
||
transport outcomes into `UploadMsg`s — is reachable only through a real
|
||
`XMLHttpRequest`. The port exists on paper and does nothing.
|
||
- **Baseline citation.** §3a: `libs/shared/upload` 52.0% line / 50.0% branch — the
|
||
worst line coverage of any module except the two `auth` rows. §3b: 50% reach, and
|
||
per the lcov file list the **two unreached files are `upload-shell.service.ts` and
|
||
`upload-controller.ts`** — neither is a `ui/` component, so this is exactly the
|
||
non-`ui/` gap BL-004 says is genuine.
|
||
- **Minimal seam.** Add the token the repo already uses elsewhere:
|
||
`export const UPLOAD_TRANSPORT = new InjectionToken<UploadTransport>('UPLOAD_TRANSPORT',
|
||
{ providedIn: 'root', factory: () => inject(KeepaliveTransport) })`, then
|
||
`inject(UPLOAD_TRANSPORT)` on L35. This **extends an existing pattern** — §7 records
|
||
exactly one explicit port in the frontend, `SessionPort` + `SESSION_PORT`
|
||
(`libs/shared/src/application/session.port.ts`), with the same interface-plus-token
|
||
shape. Runtime behaviour is byte-identical; the default factory returns the same
|
||
instance.
|
||
- **Effort S.** Independently shippable in one deploy.
|
||
|
||
**TE-004 — `createUploadController` performs injection, DOM subscription and an `effect()` at call time**
|
||
|
||
- Module / file:line — `libs/shared/src/upload/upload-controller.ts:23-46`, policy at `:62-74`
|
||
- **What blocks unit testing.** Calling the factory does four irreversible things before
|
||
returning: three `inject()` calls (L24-25, L46), an `effect()` registration (L31), and
|
||
`window.addEventListener('focus', onFocus)` (L45). It must therefore run inside a
|
||
`TestBed` injection context with `UploadAdapter`, `UploadShellService` and `DestroyRef`
|
||
all satisfied — and `UploadShellService` is itself un-fakeable per TE-003, so the
|
||
mocking cost compounds. What is trapped behind that cost is real policy:
|
||
`onFileSelected` (L62-74) decides per file whether to emit `FileRejected` with reason
|
||
`'multiple'`, `FileRejected` with a `rejectReason` result, or to start an upload — a
|
||
decision over `(categories, categoryId, files)` with no I/O in it.
|
||
- **Baseline citation.** §3a `libs/shared/upload` 52.0% / 50.0%; §3b 50% reach with this
|
||
file among the two unreached. §4a additionally records the module's `max CC 27` and the
|
||
repo's only two >75-line functions include `reduceUpload` (109 lines) — the reducer this
|
||
controller feeds. The reducer is spec'd (`upload.machine.spec.ts`); the code choosing
|
||
_which_ messages reach it is not.
|
||
- **Minimal seam.** Pure-function split into the file that is already the tested unit:
|
||
add `export function planFileSelection(state: UploadState, categoryId: string, files:
|
||
{ name: string; type: string; size: number }[]): UploadMsg[]` to `upload.machine.ts`,
|
||
moving L62-73 verbatim. `rejectReason` — the predicate it calls — is already exported
|
||
from that file and already spec'd, so the move is downhill. The controller keeps the
|
||
`crypto.randomUUID()` + `files.set()` + `shell.upload()` side effects and just executes
|
||
the plan. No change to the controller's public surface or to the organism that calls it.
|
||
- **Effort S.** Independently shippable.
|
||
|
||
**TE-005 — `UploadAdapter.xhrUpload` buries response interpretation inside an `XMLHttpRequest` closure**
|
||
|
||
- Module / file:line — `libs/shared/src/upload/upload.adapter.ts:113-157`, helpers at `:169-199`
|
||
- **What blocks unit testing.** The method constructs `new XMLHttpRequest()` directly
|
||
(L118) — no transport parameter, no injected factory — and attaches four listeners
|
||
whose bodies contain the actual decisions: 2xx-vs-not (L131), `JSON.parse` of the body
|
||
with a fallback (L132-136), ProblemDetails mapping via the un-exported `parseError`
|
||
(L193-199), and abort-vs-error disambiguation (L142-144). None of it can be reached
|
||
without stubbing the XHR global. Compounding it, the method also branches on
|
||
`currentScenario()` at L115 and returns a `setTimeout`-driven dev simulator
|
||
(`simulateUpload`, L169-192), so a dev-only fake and the production transport share
|
||
one entry point.
|
||
- **Baseline citation.** Per-file lcov: **LH 5 / LF 64 (7.8% line), BRH 3 / BRF 57
|
||
(5.3% branch)**. The file is counted as "reached" in §3b only because another spec
|
||
imports it — essentially nothing in it executes. It is the single largest contributor
|
||
to the module's 52.0% / 50.0% row in §3a.
|
||
- **Minimal seam.** Extract the interpretation, not the transport:
|
||
`export function uploadOutcome(status: number, responseText: string): Result<string,
|
||
{ documentId: string }>` containing L131-139's logic plus `parseError`. The listener
|
||
becomes a two-line dispatch into it. Optionally (same ticket, still small) move the
|
||
`currentScenario()` branch from L115 up into `KeepaliveTransport.send()` — the seam
|
||
TE-003 makes usable — so `xhrUpload` is transport only. Do **not** abstract
|
||
`XMLHttpRequest`: the file documents why XHR is required (progress events +
|
||
cancellation, which `fetch` cannot give) and that reason still holds.
|
||
- **Effort S** for `uploadOutcome` alone, **M** if the scenario branch moves too.
|
||
Independently shippable; sequence it after TE-003 if both are taken.
|
||
|
||
## libs/shared — testing
|
||
|
||
**No findings.** §3a 100% line coverage. `given()` (`machine.ts`) and the `RemoteData`
|
||
constructors (`remote-data.ts`) are the DSL the domain specs are built on, and the
|
||
`no-testing-in-production` dependency-cruiser rule (§6) keeps them out of shipped code.
|
||
The gap this folder does _not_ yet cover is a `resource()`-shaped fake — which is why
|
||
`AccessStore`/`BigProfileStore` stay unreached — but adding one is a test-infrastructure
|
||
task, not a source-code seam, and no metric row demands it.
|
||
|
||
## libs/beheer
|
||
|
||
**TE-006 — blob-to-browser handoff is inlined in three application-layer commands**
|
||
|
||
- Module / file:line — `libs/beheer/src/application/stamdata.store.ts:137-147`;
|
||
also `apps/ssp/src/app/brief/application/brief.store.ts:230` and
|
||
`apps/ssp/src/app/brief/application/org-template.store.ts:217`
|
||
- **What blocks unit testing.** Each of the three commands ends in raw DOM/browser API
|
||
calls that jsdom cannot meaningfully execute: `StamdataStore.download()` does
|
||
`URL.createObjectURL` → `document.createElement('a')` → `a.click()` →
|
||
`URL.revokeObjectURL`; `BriefStore.previewLetter()` and
|
||
`OrgTemplateStore.proefbrief()` both do `window.open(URL.createObjectURL(blob),
|
||
'_blank')`. Because the call is the **last statement**, the entire success path of each
|
||
command is unassertable — a spec can only exercise the early-return/failure branches.
|
||
`brief.store.spec.ts` demonstrates this exactly: it tests `previewLetter`'s failure
|
||
case (which returns at the `!r.ok` guard) and cannot test the success case. In
|
||
`download()` the untestable tail sits directly behind a two-clause guard
|
||
(`if (!s || !this.canDownload()) return;`, L139), so the guard's true-branch is
|
||
permanently dark.
|
||
- **Baseline citation.** §3a: `libs/beheer/application` **40.5% branch — the worst
|
||
branch coverage of any frontend module in the table**, and its 65.7% line figure is
|
||
third-worst. Per-file lcov confirms `stamdata.store.ts` _is_ that row: LH 46 / LF 70,
|
||
**BRH 15 / BRF 37**. On the brief side, §3a `ssp/brief` is 68.8% branch and
|
||
`brief.store.ts` measures **BRH 32 / BRF 64 — exactly 50%**.
|
||
- **Minimal seam.** One small injectable in `libs/shared/src/application`, mirroring the
|
||
`SESSION_PORT` token shape already in that folder:
|
||
`export const BLOB_PRESENTER = new InjectionToken<{ open(b: Blob): void; download(b:
|
||
Blob, filename: string): void }>('BLOB_PRESENTER', { providedIn: 'root', factory: () =>
|
||
realBlobPresenter })`. The three commands each lose 1-4 lines of DOM code and gain one
|
||
method call; specs provide a recording fake and finally assert the success paths
|
||
(including `toJson(...)`'s output actually reaching the file, which today is only
|
||
tested one level down in `beheer/domain`). The content-producing logic stays exactly
|
||
where it is.
|
||
- **Effort S** (one token + three one-line edits) — **M** including the specs the seam
|
||
unlocks. Independently shippable; the three call sites can also land separately.
|
||
|
||
**Other beheer layers — no findings.**
|
||
|
||
- `libs/beheer/contracts` — §3b lists it at **0% reached, 1 file**, and BL-004 names it
|
||
as a genuine gap. It is not: `stamdata.dto.ts` is 30 lines of `interface` and `type`
|
||
declarations with **zero executable statements** and, by design, zero imports (it is
|
||
the wire seam). Like `libs/shared/domain`, this row should be closed rather than
|
||
ticketed.
|
||
- `libs/beheer/ui` — 0% reach, 4 files, all components → BL-004 / Storybook.
|
||
- `libs/beheer/domain` — 98.1% line, spec'd machine and rules. Nothing blocked.
|
||
- `libs/beheer/infrastructure` — `parseStamdataTable`/`parseColumn`/`parseRows` all
|
||
exported and spec'd; the 60.5% branch figure is unexercised defensive arms in an
|
||
otherwise open unit.
|
||
|
||
---
|
||
|
||
## backend/Program.cs
|
||
|
||
**No findings.**
|
||
|
||
§3c: 97.4% line / 84.8% branch, the second-best branch figure on the backend. Ten
|
||
endpoint bodies read the wall clock inline (`DateTimeOffset.UtcNow` at L282, L286, L301,
|
||
L390, L394, L426, L435, L449, L482, L931; `DateOnly.FromDateTime(DateTime.Today)` at
|
||
L138), which would normally be a finding — but **every rule and mapper they hand it to
|
||
already takes the instant as a parameter**: `HerregistratieRule.Evaluate(reg, today)`,
|
||
`ToDetailDto(a, now)`, `ToStatusDto(a, now)`, `ListCases(now)`,
|
||
`ApplicationStore.RecordBesluit(..., now)`. The clock-dependent _decisions_ are all
|
||
testable at any date; only the endpoint wiring is pinned to now, and that wiring is what
|
||
`EndpointTests`/`AdminCasesTests` legitimately cover through the host. Injecting
|
||
`TimeProvider` into 48 minimal-API lambdas would be a rewrite, not a seam, and no metric
|
||
row asks for it.
|
||
|
||
BL-003 (940 lines, file CC 78, read/write split by comment banner) is a **structure**
|
||
finding, explicitly, and belongs to agents 03/04.
|
||
|
||
## backend/Domain
|
||
|
||
**TE-007 — `LetterHtml.ResolveAuto` reads the wall clock although `Render` is already given the instant**
|
||
|
||
- Module / file:line — `backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs:138`
|
||
(resolver) vs. `:27` (signature) and `:49` (the correct usage)
|
||
- **What blocks unit testing.** `Render(BriefEntity brief, OrgTemplateDto template,
|
||
string at, bool watermark)` already accepts the letter's instant, and uses it properly
|
||
for the letterhead: `sb.Append(Enc(FormatDatumNl(at)))` at L49. But the body's
|
||
`datum` placeholder resolves through `ResolveAuto`, which ignores `at` and calls
|
||
`FormatDatumNl(DateTimeOffset.UtcNow.ToString("o"))` (L138). `ResolveAuto` is
|
||
`private static`, reached only via `RenderNode` ← `RenderParagraphs` ← `Render`, so a
|
||
test has no way to pin the value: it can only assert "whatever today is". The gap is
|
||
visible in the existing test file — `LetterHtmlTests.cs:26` declares a
|
||
`new PlaceholderDefDto("datum", "Datum", true)` in the fixture and **no assertion
|
||
anywhere in the file checks what it renders to**. This is a pure `Domain/` rule class
|
||
reaching for ambient state, which is precisely the purity §7 credits the folder with.
|
||
- **Baseline citation.** §3c: `backend/Domain` **82.0% branch** — the third-weakest
|
||
branch axis (BL-005). §4b: `Domain/Letters/LetterHtml.cs` file **CC 21**, the
|
||
third-highest-CC file in the backend after `Program.cs` (78) and
|
||
`Data/ApplicationStore.cs` (27).
|
||
- **Minimal seam.** Thread the parameter that already exists: `ResolveAuto(string key,
|
||
string label, string at)` → `"datum" => FormatDatumNl(at)`, passing `at` down through
|
||
`RenderParagraphs`/`RenderNode` (both private, both already in `Render`'s call chain
|
||
with `at` in scope). Two signature changes, one expression change, zero public API
|
||
change, zero call-site change. Then assert the rendered `datum` against a fixed
|
||
expected string in `LetterHtmlTests`.
|
||
- **Secondary benefit, stated conservatively.** This is not a shipped bug today — every
|
||
caller (`Program.cs:697`, `:708`, `BriefStore.cs:120`) passes `Now()` at render time,
|
||
so the two dates coincide. It becomes one the moment `Render` is called with a
|
||
historical `at` (re-rendering an archive, back-dating a letter), at which point the
|
||
letterhead and the body would disagree within a single document.
|
||
- **Effort S.** Independently shippable.
|
||
|
||
No other Domain findings. §7's claim holds under inspection: `SubmissionRules`,
|
||
`DocumentRules`, `IntakePolicy`, `BeoordelingRules`, `DiplomaRules`,
|
||
`HerregistratieRule`, `OrgTemplateRules`, `Authz` and `FeatureFlags` are static classes of
|
||
pure functions with a matching file in `tests/Domain/`, and the clock-dependent ones take
|
||
their instant as an argument. That is the correct shape.
|
||
|
||
## backend/Data
|
||
|
||
**TE-008 — brief state-transition and authorization rules live inside DB-opening, lock-held store methods**
|
||
|
||
- Module / file:line — `backend/src/BigRegister.Api/Data/BriefStore.cs`, five guard
|
||
clusters: `:72-76` (Save), `:88-90` (Submit), `:111-112` (Send), `:162-164`
|
||
(Approve/Reject shared path), plus the `RequiredFilled(e)` predicate
|
||
- **What blocks unit testing.** Each guard is a pure decision over
|
||
`(status tag, actor role, entity completeness)` — e.g. `Save` returns `Forbidden` if
|
||
`!isDrafter`, `Conflict` unless the status is `draft` or `rejected`, and reopens a
|
||
`rejected` letter to `draft`; `Submit` additionally requires `RequiredFilled`. But each
|
||
sits **inside** a method that has already done `lock (_gate) { using var db =
|
||
Db.Create(); ... }`, so exercising any of them requires a booted host and a real SQLite
|
||
file. There is no `BriefRules` class: `Domain/Letters/` contains only `LetterHtml.cs`
|
||
and `OrgTemplateRules.cs`. The pattern is visibly **half-applied** — `Authz.CanActOn`
|
||
at L163 _is_ a pure `Domain/` call, sitting one line away from three guards that are not.
|
||
The `Save` guard's own comment says it "mirrors the FE reducer", i.e. it is business
|
||
logic with a known pure counterpart on the other side of the wire.
|
||
- **Baseline citation.** §3c: `backend/Data` **75.5% branch** — named in BL-005 as one
|
||
of the three weak branch axes, against 99.0% line coverage (the exact signature of
|
||
"every unit is entered, edge branches are not"). §4b: `Data/BriefStore.cs` file
|
||
**CC 17**, and its `ToDto` at **CC 16** is the highest-CC non-`Program.cs` method in
|
||
the backend. §5: `backend/Data` 7.7% duplication.
|
||
- **Minimal seam.** Add `Domain/Letters/BriefRules.cs` with pure statics —
|
||
`CanSave(BriefStatusDto status, bool isDrafter) → Outcome`,
|
||
`StatusAfterSave(BriefStatusDto) → BriefStatusDto`,
|
||
`CanSubmit(status, isDrafter, bool requiredFilled) → Outcome`, `CanSend(status)`,
|
||
`CanDecide(status, Principal, drafterId)` — and have each store method call one.
|
||
The store keeps its lock, its `Db.Create()`, its static shape and its signature; only
|
||
the `if` cascade moves. This **extends the pattern §7 already records** for
|
||
`SubmissionRules` / `BeoordelingRules` / `OrgTemplateRules` / `DocumentRules`, and adds
|
||
a `tests/Domain/BriefRuleTests.cs` alongside the seven that exist.
|
||
- **Explicitly NOT proposed: changing the static-store shape.** `Data/Db.cs:6-12`
|
||
documents the static, non-DI store decision, and
|
||
`tests/TestWebApplicationFactory.cs:1-12` states the position outright — "Serializing
|
||
test classes is the fix, not a redesign of the stores for a test-only concern."
|
||
TE-008 respects that completely: it is orthogonal, and works _because_ the rules never
|
||
needed the DbContext in the first place.
|
||
- **The cost this seam actually pays down.** Because `Db.ConnectionString` is one static
|
||
field, that same file carries
|
||
`[assembly: CollectionBehavior(DisableTestParallelization = true)]` — **all 241 backend
|
||
tests run serially, process-wide**, and every brief-rule assertion currently pays a
|
||
host boot + SQLite file for a decision that is a pure function of two enums. Each rule
|
||
moved out of `BriefStore` moves a test out of the serialized integration lane into the
|
||
free-running unit lane. That is the argument for the seam; it is not an argument for
|
||
touching the stores.
|
||
- **Effort M** (five extractions + one new test file). Independently shippable, and
|
||
splittable one method at a time if preferred.
|
||
|
||
Two smaller Data notes, neither ticketed: `IdempotencyStore` is the only store that is
|
||
purely in-memory with no `Reset()` and no TTL, so its dictionary survives
|
||
`TestWebApplicationFactory` disposal and is shared by every test class in the process —
|
||
harmless today only because `IdempotencyTests.cs:24` keys on `Guid.NewGuid()`. And
|
||
`DocumentStore.cs:54` / `AuthzAuditStore.cs:35` stamp `DateTimeOffset.UtcNow` inline
|
||
while `ApplicationStore.RecordBesluit` correctly takes `now` — an inconsistency, but
|
||
neither audit timestamp is asserted on, so no metric supports a ticket.
|
||
|
||
## backend/Zgw
|
||
|
||
**No findings.** §3c 98.1% line / **85.5% branch — the strongest branch figure on the
|
||
backend**, and §5 records 1.8% duplication. This is the module that was built as
|
||
ports-and-adapters from the start (ADR-0005): `IZaakSource`/`IDocumentSource` each have
|
||
two implementations (§7), `ZgwHttpClient` takes an injected `HttpClient`, and
|
||
`tests/ZgwStubHandler.cs` provides the transport fake — five test files ride on it. It
|
||
is the backend's worked example of the seam TE-003 asks the upload module for.
|
||
|
||
## backend/Contracts
|
||
|
||
**No testability findings — but state the gap accurately.**
|
||
|
||
§3c records `backend/Contracts` at **65.0% branch, the worst branch figure in the repo**
|
||
(BL-005 names it first). It is nonetheless not a testability finding: `Mappers.cs` is 79
|
||
lines of pure `static` extension methods over records, with the clock already injected
|
||
where it matters (`ToStatusDto(this Aanvraag a, DateTimeOffset now)` at `:52`,
|
||
`ToSummaryDto(..., now)` at `:68`, `ToDetailDto(..., now)` at `:76`), and `Dtos.cs` is
|
||
250 lines of `record` declarations. §4b confirms the shape: file CC 4, max method CC 3,
|
||
the lowest complexity of any backend folder. **Nothing blocks a unit test here.**
|
||
|
||
What is missing is a test _file_: `backend/tests/` has `Domain/`, `Acceptance/` and
|
||
`Builders/` folders but no `Contracts/`, so all 65% is incidental coverage picked up
|
||
through endpoint tests. That is a coverage ticket for whoever owns coverage, requiring
|
||
zero source change — and per BL-009 there is no ratchet, so it would have to be verified
|
||
against §3c's numbers by hand.
|
||
|
||
## backend/Stamdata
|
||
|
||
**TE-009 — `Professions.ByProgram` freezes its valid-time filter at type-load from `DateTime.Today`**
|
||
|
||
- Module / file:line — `backend/src/BigRegister.Api/Stamdata/Professions.cs:25-27`
|
||
- **What blocks unit testing.** `ByProgram` is a `static readonly IReadOnlyDictionary`
|
||
whose initializer runs `Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan,
|
||
m.GeldigTot, DateOnly.FromDateTime(DateTime.Today)))`. Two compounding problems: the
|
||
peildatum is the ambient wall clock, **and** the result is computed once per process at
|
||
type-load and then immutable. A test cannot ask "which mappings are active on
|
||
2030-01-01" — not by arranging state, not by ordering, not at all. The temporal
|
||
behaviour of the one business-tunable table that has a validity window is therefore
|
||
unreachable. The file's own comment concedes the consequence: it "preserves the
|
||
pre-valid-time behaviour exactly **while the file's rows are all current**" — i.e. the
|
||
`ActiveOn` call is presently a constant-true filter, so both of its interesting
|
||
branches (not-yet-valid, expired) are dead in every run.
|
||
- **Baseline citation.** §3c: `backend/Stamdata` 96.8% line but **71.7% branch** —
|
||
named in BL-005 as the second-weakest branch axis, an exact 25-point line/branch split.
|
||
§4b: `Stamdata/StamdataTable.cs` file CC 21, joint-third-highest in the backend.
|
||
This is the rare backend case where "untestable" is defensible against 97.6% line
|
||
coverage: the lines run, the branches provably cannot.
|
||
- **Minimal seam.** Add the parameterized overload and define the existing field in terms
|
||
of it:
|
||
`public static IReadOnlyDictionary<string,string> ByProgramOn(DateOnly on) => Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, on)).ToDictionary(...);`
|
||
then `public static readonly IReadOnlyDictionary<string,string> ByProgram = ByProgramOn(DateOnly.FromDateTime(DateTime.Today));`.
|
||
**Zero call-site changes** — `DiplomaRules.ProfessionFor` and `All()` keep using
|
||
`ByProgram`. `StamdataValidationTests` gains the ability to assert both validity-window
|
||
branches against authored future/expired rows.
|
||
- **This extends an existing pattern in the same folder.** `StamdataTable.cs:63` already
|
||
does exactly this — `Temporal ? Rows().Where(r => ActiveOn(r, on)).ToArray() : Rows()`,
|
||
with `on` as a parameter — and `StamdataFile.ActiveOn(van, tot, on)` (`:36`) is already
|
||
clock-free. `Professions.cs` is the one caller that swallows the parameter.
|
||
- **Effort S.** Independently shippable; additive only.
|
||
|
||
## backend/tests
|
||
|
||
**No findings.** 39 files, 4 253 lines, 241 green tests, every backend source file
|
||
reached (§3c). The suite already carries the fixtures a unit lane needs
|
||
(`Builders/AanvraagBuilder.cs`, `ZgwStubHandler.cs`, `TestWebApplicationFactory` with
|
||
per-class throwaway SQLite files).
|
||
|
||
The one structural observation is not a defect to fix here:
|
||
`[assembly: CollectionBehavior(DisableTestParallelization = true)]`
|
||
(`TestWebApplicationFactory.cs:12`) serializes the entire suite because
|
||
`Db.ConnectionString` is a process-global. The repo reached that decision deliberately
|
||
and documented the race it prevents. Rather than reopen it, TE-008 and TE-009 reduce how
|
||
much _needs_ to run in that serialized lane. Note also §4b's outlier in this folder: a
|
||
**293-line test method at CC 20**
|
||
(`CreateZaak_posts_zaak_status_and_rol_and_maps_the_result_back`,
|
||
`OpenZaakZaakSourceTests.cs`) — a test-readability item for agent 01, not a testability
|
||
seam.
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
| ID | Title | Module | Blocker | Baseline | Effort | 1 deploy |
|
||
| ------ | -------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ | ------ | -------- |
|
||
| TE-001 | `SessionStore.restore()` reads `localStorage` inline | ssp/auth + bhp/auth | hidden I/O in a field initializer; guard is module-private | §3a 42.9%/46.2% (worst line); file LH 2/20, BRH 3/13 | S ×2 | yes |
|
||
| TE-002 | Trust boundary hidden inside a global-`fetch` method | ssp/brief | un-exported shape validation behind `await fetch` | §3b 42% reach; §3a 68.8% branch | S | yes |
|
||
| TE-003 | `UploadTransport` port declared, concrete class injected | libs/shared/upload | `inject(KeepaliveTransport)`; class not exported | §3a 52.0%/50.0%; file unreached (§3b, non-`ui/`) | S | yes |
|
||
| TE-004 | `createUploadController` injects + binds `window` at call time | libs/shared/upload | 3× `inject()`, `effect()`, `addEventListener` before returning | §3a 52.0%/50.0%; file unreached (§3b, non-`ui/`) | S | yes |
|
||
| TE-005 | `xhrUpload` interprets responses inside an XHR closure | libs/shared/upload | `new XMLHttpRequest()` hard-coded; dev simulator shares the method | file LH 5/64 (7.8%), BRH 3/57 (5.3%) | S–M | yes |
|
||
| TE-006 | Blob-to-browser handoff inlined in 3 commands | libs/beheer + ssp/brief | `window.open` / `a.click()` as the last statement of each command | §3a beheer/application 40.5% branch (worst); brief.store 50% | S–M | yes |
|
||
| TE-007 | `LetterHtml` resolves `datum` from `UtcNow`, not from `at` | backend/Domain | ambient clock in a private resolver inside a pure rule class | §3c Domain 82.0% branch; §4b file CC 21 | S | yes |
|
||
| TE-008 | Brief transition rules live inside DB-opening store methods | backend/Data | 5 pure guards behind `lock` + `Db.Create()` | §3c Data 75.5% branch (BL-005); §4b CC 17, `ToDto` CC 16 | M | yes |
|
||
| TE-009 | `Professions.ByProgram` freezes valid-time at type-load | backend/Stamdata | `static readonly` + `DateTime.Today`; both branches unreachable | §3c Stamdata 71.7% branch (BL-005) | S | yes |
|
||
|
||
**Modules with no findings:** ssp/registratie · ssp/herregistratie · ssp/showcase+shell+root ·
|
||
bhp/behandeling · bhp/shell+root · libs/shared/{domain, application, infrastructure, ui,
|
||
layout, kernel, testing} · libs/beheer/{domain, infrastructure, ui, contracts} ·
|
||
backend/Program.cs · backend/Zgw · backend/Contracts · backend/tests.
|
||
|
||
**Baseline rows recommended for closure as false gaps:** `libs/shared/domain` (0% reach,
|
||
3 files) and `libs/beheer/contracts` (0% reach, 1 file) — both named in BL-004 as genuine
|
||
gaps; both contain only type declarations and no executable statement.
|
||
|
||
**Cross-references, not owned here:** BL-001 complexity (agent 01) · BL-002 auth
|
||
duplication (agent 06) · BL-003 `Program.cs` structure (agents 03/04) · BL-006 backend
|
||
architecture enforcement (agent 03) · BL-007 write-side placement (agent 04) · BL-008
|
||
`coverageExclude` · BL-009 no coverage ratchet — which means none of the findings above
|
||
can be verified as "improved" by CI alone; verify against `00-baseline.md`'s numbers ·
|
||
BL-010 `libs/shared/upload` layer placement (TE-003/004/005 all land inside that
|
||
carve-out and do not resolve it) · BL-011 suite flakiness under parallel load.
|