The doctrine says what the code does again

Eighteen leaves had drifted behind today's changes, and several taught the exact
mistakes that were just removed.

Corrected, among others: the identity and the profile were conflated, and
`knowledge_context-internals` still described the impersonation fallback and the
principal-to-username join as current mechanisms. `caveat_identity-ids-in-screens`
and `knowledge_data-modes` still had `joinEvent` logging and returning where it
now throws. The shape listings still carried the event host. And
`knowledge_screen-pattern`'s canonical sample taught a toast written beside the
call rather than after the write -- the very bug fixed this afternoon, sitting in
the file a new screen is copied from.

New leaves for what had no home: write rights read from the owned-document
listing, with its three states and its deliberate residual; the owner's ruling
that no "may I write this?" call is coming, so the list is the answer for good;
and the `@data` suite losing its fixtures now that the seed writes nothing into a
connected wallet.

Four doc-debt files settled, including one the hook opened mid-pass.

Worth recording how one leaf died: a caveat was written for the unguarded edit
screen exactly as briefed, then deleted on finding the fix had landed while the
pass ran. Doctrine tracks the tree, not the instructions it was given.
This commit is contained in:
Sylvain Duchesne
2026-08-16 15:25:34 +02:00
parent 13eb2c4a15
commit cebd54c978
29 changed files with 265 additions and 166 deletions
-16
View File
@@ -1,16 +0,0 @@
# Doc-debt — bdd-testing
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED src/shared/test-harness/harness-ng.tsx @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/shared/test-harness/harness.tsx @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/shared/support/hooks.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/workshop/steps/data/multibrowser.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/event/steps/e2e/evenement.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/event/steps/data/reconnexion.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/event/steps/data/reconnexion-froide-sans-local.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/auth/steps/e2e/connexion.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/event/steps/e2e/reconnexion-persistance.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
- TOUCHED src/modules/home/steps/e2e/accueil-connecte-rend.steps.ts @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
@@ -33,6 +33,7 @@ BDD tests written in **Cucumber/Gherkin in French** (`Etant donné`, `Quand`, `A
- [[knowledge_e2e-layer]] — the `@e2e` layer: the real app inside the iframe
- [[knowledge_multibrowser-harness]] — several isolated browsers on the shared wallet (storageState injection); the only way multi-user is exercised
- [[caveat_data-scenarios-share-one-wallet]] — a scenario cannot choose its identity, so all of them share one wallet that nothing empties: no per-scenario isolation
- [[caveat_data-suite-has-no-fixtures]] — **known, not fixed**: the fixture seed writes nothing into a connected wallet, so `@data` scenarios that assumed seeded data have none
- [[caveat_reconnexion-froide-local-vs-broker]] — a "fresh page" is not a cold start: which setup proves broker durability, and which one just re-reads local
- [[caveat_first-time-entry-untested]] — **open**: no test proves a first-time device can get into Festipod any more; the SDK's replacement barrier publishes nothing to test against
- [[decision_2026-03-12_headless-wallet-creation]] — why the test wallet is created through a headless UI
@@ -0,0 +1,29 @@
---
type: caveat
summary: Known, not fixed — the fixture seed is off for connected wallets, so every @data scenario that assumed seeded events or profiles now runs against whatever the shared wallet happens to hold; "load test data" is a no-op that reports success
last_checked: 2026-08-16
---
# Caveat: the `@data` suite lost its fixtures
## What changed under it
No fixture is written into a connected wallet any more, by any route — a product decision enforced in one place (`concept data-layer`, [[knowledge_seed-data]]). The `@data` layer did not ask for that and was not adapted to it.
## What that does to the suite
The bridge's `loadTestData()` still resolves, and it reports **`seeded: false`** with no documents created. So:
- Scenarios that **load test data and then assert on it** (`auth/connexion-nextgraph.feature`: loading the fixtures, the "not reloaded twice" idempotence check, "the events have NextGraph identifiers") no longer have anything to assert on. The call succeeds; nothing is written.
- Scenarios whose background **assumes a seeded wallet** ("le portefeuille contient des données de test", "un événement {string} existe" — which seeds on demand when the wallet reads empty) now depend entirely on what the shared wallet happens to already hold.
- Nothing raises. A no-op seed reports success, which is the failure mode to expect: a green step followed by an assertion that finds nothing.
## What NOT to do about it
**Do not re-enable the seed for the tests, and do not add a test-only bypass of the enforcement point.** The switch is enforced at `bootstrapWallet` precisely so no caller can walk around it, and a harness is a caller like any other. **Do not weaken the affected scenarios into something that passes** either.
The suite needs scenarios that **create what they need through the app's own path** (the same `createEvent` / `joinEvent` a user drives), rather than a background that assumes a pre-populated wallet. That is the direction; it is not done.
## Related
This compounds [[caveat_data-scenarios-share-one-wallet]] — scenarios already could not choose their identity or start from a clean slate, and now they cannot furnish that slate either. Both are open.
@@ -22,8 +22,15 @@ summary: How to add a BDD scenario/step — a tagged French .feature, steps per
```
Always `await` (forgetting it means asserting before the promise resolves).
6. **If you add a data operation**: expose the helper on `window.__testData` in **both** harnesses (`src/shared/test-harness/harness.tsx` AND `harness-ng.tsx`) — otherwise the mock fallback drifts away from the real broker.
6. **⚠️ `waitForFunction` timeout goes in the THIRD slot, not the second.** Playwright's signature is `waitForFunction(pageFunction, arg, options)`. Passing `{ timeout: N }` where `arg` belongs is **not an error**: it is accepted as the page function's *argument*, no options are supplied, and the wait silently uses the **30 s default** while the source reads 5, 10 or 60. When there is no argument to pass, the slot must be filled explicitly:
```ts
// ❌ await frame.waitForFunction(fn, { timeout: 10000 }) // waits 30 s
// ✅ await frame.waitForFunction(fn, undefined, { timeout: 10000 }) // waits 10 s
```
This had gone unnoticed on **seventeen** calls at once, nine of which meant to wait *less* than the default. It is worth honouring the written number: a wait that is too short fails loudly and names its step, whereas thirty seconds obtained by accident hides a real slowness and makes the source lie. Same family as the pitfall above — both are Playwright argument slots that accept the wrong thing without complaining.
7. **Wire up a screen under test**: if the French screen name does not resolve to its `id`, add an alias in `screenNameMap` (`src/shared/steps/ui/navigation.steps.ts`).
7. **If you add a data operation**: expose the helper on `window.__testData` in **both** harnesses (`src/shared/test-harness/harness.tsx` AND `harness-ng.tsx`) — otherwise the mock fallback drifts away from the real broker.
8. **Run**: `bun run test:cucumber` (everything) or `bun run test:data` (@data). Report: `reports/cucumber-report.html`. `@data`/`@e2e` require the test wallet (`bun run test:auth-setup` on the first go if needed, otherwise it is created automatically — see [[decision_2026-03-12_headless-wallet-creation]]).
8. **Wire up a screen under test**: if the French screen name does not resolve to its `id`, add an alias in `screenNameMap` (`src/shared/steps/ui/navigation.steps.ts`).
9. **Run**: `bun run test:cucumber` (everything) or `bun run test:data` (@data). Report: `reports/cucumber-report.html`. `@data`/`@e2e` require the test wallet (`bun run test:auth-setup` on the first go if needed, otherwise it is created automatically — see [[decision_2026-03-12_headless-wallet-creation]]).
@@ -1,7 +1,7 @@
---
type: knowledge
summary: The @data layer — Playwright drives Chromium (persistent profile) into the real broker, which loads harness-ng.tsx in an iframe; automated wallet lifecycle, window.__testData bridge, mock fallback; the harness signs in exactly as the app does, and per-scenario isolation is currently ABSENT
last_checked: 2026-08-10
summary: The @data layer — Playwright drives Chromium (persistent profile) into the real broker, which loads harness-ng.tsx in an iframe; automated wallet lifecycle, window.__testData bridge, mock fallback; the harness signs in as the app does, per-scenario isolation is ABSENT and the seed now writes nothing
last_checked: 2026-08-16
---
# The `@data` layer (real broker)
@@ -35,8 +35,8 @@ Cucumber → Playwright (Chromium, persistent profile)
- **HTTP server** started in `BeforeAll` (auto-assigned port), serving the HTML plus `/harness.js` (separate files — an inline script breaks because of special characters in the bundle).
- **The bridge is the real app path (per entity).** Since the move to *one document per entity* (concept `data-layer`, [[rule_document-per-entity]]), the `window.__testData` bridge (`events`/`users`/`participations`, `joinEvent`/`leaveEvent`/`isParticipating`/`getEventParticipants`, `loadTestData`) **delegates to the app's data context** (`appData` through `FestipodDataProvider`) — this is the real per-entity path the screens use, not a read at root-store level. It reads `appData` through a **live ref** (a captured snapshot goes stale after a seed re-render).
- **The harness signs in exactly as the app does.** It mounts `NextGraphProvider > FestipodDataProvider`**no identity provider, no default login** — and awaits the single `ensureIdentity()` before exposing the bridge, mirroring the order `AuthGate` imposes (concept `app-security`, [[decision_2026-08-10_the-barrier-names-no-identity]]). Nothing may read before it resolves. The low-level probes that used to reach past the app path are **gone**, along with the scenarios whose subject was the SDK rather than Festipod ([[rule_tests-validate-festipod-not-the-sdk]]).
- **Identity before writing.** A `Participation` has a mandatory `fp:user`; the current user is **the profile document read back in the protected scope**, so it lags behind the public events. Steps wait for `ensureCurrentUser()` before `joinEvent` (otherwise the mutation refuses, or writes a participation with no user → dropped on read) and then wait (`waitForFunction`) for the participation to be read back.
- **Identity before writing.** A `Participation` has a mandatory `fp:user`, and what goes in it is **the profile document this session owns** — resolved from the owned-document listing, and created at sign-in when there is none, so it lags behind the public events. Steps wait for `ensureCurrentUser()` before `joinEvent`, then wait (`waitForFunction`) for the participation to be read back. Waiting is no longer optional politeness: `joinEvent` and `leaveEvent` now **reject** when the profile is unresolved, so a step that fires too early fails loudly instead of passing over a write that never happened.
- **Per-scenario isolation is currently ABSENT — read [[caveat_data-scenarios-share-one-wallet]] before trusting a green run.** The `Before` hook still mints `this.freshIdentifier` and injects it into `localStorage['festipod.account.identifier']`, and several steps re-inject it, but **nothing reads that key any more**: no published call takes an identifier. Every scenario therefore runs as the same identity on one accumulating wallet. That machinery is inert, not load-bearing — do not build new setup on it, and do not "repair" it by making the app honour the key again.
- The old per-scenario reset (`resetDataState()`, a SPARQL DELETE on the anchor graph) was dropped for cost (up to 10 s of the `Before` hook's 60 s budget, already eaten by the broker login) and its helper is gone too.
- The **physical** growth of the shared wallet was never bounded by any of this — see [[caveat_wallet-bloat-hang]] (profile to be moved aside when reads start to hang).
- The connected seed stays **lightweight** (few docs): creating a document is a serial round trip, so the seed's cost is linear in the number of documents it writes.
- **The bridge's `loadTestData` no longer writes anything.** No fixture reaches a connected wallet by any route, and the enforcement point is deliberately un-bypassable — so the call resolves, reports nothing seeded, and every scenario that assumed seeded events or profiles is now running on whatever the shared wallet already holds. Read [[caveat_data-suite-has-no-fixtures]] before diagnosing an empty assertion, and do not re-enable the seed for the tests.
@@ -10,6 +10,7 @@ last_checked: 2026-08-10
- Helper: `src/shared/test-harness/renderHelper.tsx` (installs the happy-dom globals, wraps the screen). Invoked from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`.
- Deterministic fixtures (`src/shared/data/seedData.ts`, see concept `data-layer`): `Marie Dupont`/`@mariedupont` = currentUser, `Jean Durand`/`@jeandurand` exists, 5 events, and so on.
- **`@ui` is untouched by the connected-wallet seed switch.** No fixture may be written into a *wallet* any more, but `@ui` renders the fixtures straight into React state through `LocalDataProvider` and writes to nothing — so these fixtures are unchanged and stay the layer's ground ([[caveat_data-suite-has-no-fixtures]] is a `@data` problem only).
## Good assertion patterns