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>
4.3 KiB
WP-74 — E2E isolation without a new backend endpoint
Status: done (42f7bd6)
Phase: 12 — DDD hardening
Why
The three Playwright specs shared one mutable backend and admitted it in their own comments
("Restart the backend between CI runs — a second run would see a leftover Concept"). A crashed
mid-wizard run poisoned every subsequent run via CreateConcept's 409, and both mutating specs
acted as the same identity (DocumentStore.DemoOwner), so any new state-touching spec would
collide immediately.
Decisions (pre-made)
WP-70 recorded the fix as a dev-only seed endpoint. That premise was wrong, and exploration established why:
- The DB path already routes through
IConfiguration(Program.cs,Db.ConnectionString = GetConnectionString("AppDb") ?? …), soConnectionStrings__AppDbas an env var gives a throwaway DB with zero backend change — the same trickTestWebApplicationFactoryalready uses per-test. StubIdentityProvideralready honours anX-Subjectheader; the only gap was that no FE interceptor sent one.- The backend has no
IsDevelopment()gate anywhere (grep: zero hits), so a seed endpoint would have had to invent the codebase's first environment gate — a new security posture for no gain.
So: throwaway DB + a dev-only X-Subject interceptor. No new endpoint, no environment gate.
Acceptance criteria
npm run e2epasses twice back-to-back with no backend restart — the actual acceptance test, and the thing that failed before this WP.X-Subjectobserved on a real request reaching the backend (X-Subject: 111222333onGET /api/v1/uploads/categories), not merely wired.- Each new BSN elfproef-verified by script against the weights
[9,8,7,6,5,4,3,2,-1]. - No backend change, no new endpoint, no
IsDevelopment()gate. - Committed port config still defaults to 4200 (verification used an override).
Notes on the two caveats
reuseExistingServerstays on. Flipping it tofalsewould hard-failnpm run e2efor anyone already running the docker stack on 4200/5000 — a real local-workflow regression. The consequence (the throwaway DB only applies when Playwright itself spawns the backend; always true in CI) is documented in a comment on thewebServerentry.- Unique DB filename per invocation, with
global-setup.tssweeping only prior runs' leftovers. A fixed name unlinked mid-run is only safe if SQLite's pool never reopens by path afterwards; underfullyParallelthat risks silently recreating an empty, unmigrated DB.
Deviation: interceptors alone were not enough
Two hand-written call sites bypass Angular's interceptor chain (as CLAUDE.md documents) and
needed X-Subject stamped explicitly:
libs/shared/src/upload/upload.adapter.ts's raw XHR upload — without this every uploaded document landed underDemoOwner, breaking submit for any other identity.apps/ssp/.../letter-preview.adapter.ts's preview fetch (pluscache: 'no-store', correct regardless since the endpoint sends noCache-Control).
Known gap (a real backend bug, not caused by this WP)
Under any BSN other than DemoOwner, GET /brief/preview returns a sent letter still
carrying the draft watermark — while curl against the same backend at the same instant returns
the correct frozen archive. Client caching was ruled out (no-store, then cache-busting query
strings), the dev proxy was ruled out, and it reproduced across two BSNs and never for
DemoOwner. This points at a staleness/race in BriefStore's SQLite read path.
brief-v2.spec.ts therefore keeps the shared zorgverlener identity — it still gains
throwaway-DB repeatability, just not per-spec identity isolation. actors.ts reserves a
briefOpsteller actor for whoever fixes the backend. Tracked as a follow-up below.
Verification
npm run e2e # twice consecutively, no backend restart
npm run lint && npm run typecheck && npm test && npm run build
Note: port 4200 was held by an unrelated container on the dev machine, so verification ran with
E2E_BASE_URL pointed at an alternate port. The committed default is unchanged.
Follow-ups
/brief/previewstaleness for non-DemoOwneridentities (above) — the blocker for givingbrief-v2.spec.tsits own identity.