From 02cda056b84d202e2bd3da7340726c5cc7ac4a79 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Mon, 6 Jul 2026 10:15:06 +0200 Subject: [PATCH] test+seed: fresh virtual wallet per @data scenario + seed events reach the index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @data Before hook sets a UNIQUE virtual-wallet id (username) per scenario so each scenario starts on a fresh, empty virtual wallet — isolation without touching the physical wallet; "le portefeuille est vide" is now a fast check, not a full scan. resetDataState / clearWallet fan-out dropped. - bootstrapWallet now submits each seeded PUBLIC event to the discovery index (mirrors the product createEvent), so a fresh virtual wallet can see seeded events through discovery rather than as its own docs. Note: @data still red — seeded/published events do not surface in the discovery read (submit→readIndex round-trip against the real broker), and some publish steps time out. The 75s ORM hang is gone; this is a distinct discovery-index integration issue, still under diagnosis. --- .project/concepts/app-security/_debt.md | 7 +++ .project/concepts/bdd-testing/_debt.md | 2 + .project/concepts/data-layer/_debt.md | 1 + .../auth/steps/data/connexion.steps.ts | 25 +++----- src/shared/support/hooks.ts | 57 ++++++++++++------- src/shared/utils/ngBootstrap.ts | 7 +++ 6 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 .project/concepts/app-security/_debt.md diff --git a/.project/concepts/app-security/_debt.md b/.project/concepts/app-security/_debt.md new file mode 100644 index 0000000..d7b87b1 --- /dev/null +++ b/.project/concepts/app-security/_debt.md @@ -0,0 +1,7 @@ +# Doc-debt — app-security + +> 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/modules/auth/steps/data/connexion.steps.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) diff --git a/.project/concepts/bdd-testing/_debt.md b/.project/concepts/bdd-testing/_debt.md index 6e978b9..c71f855 100644 --- a/.project/concepts/bdd-testing/_debt.md +++ b/.project/concepts/bdd-testing/_debt.md @@ -8,3 +8,5 @@ - TOUCHED src/modules/workshop/features/read-model-probe.feature @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) - TOUCHED src/modules/workshop/steps/data/read-model-probe.steps.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) - TOUCHED src/modules/event/steps/data/inscription.steps.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) +- TOUCHED src/shared/support/hooks.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) +- TOUCHED src/modules/auth/steps/data/connexion.steps.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) diff --git a/.project/concepts/data-layer/_debt.md b/.project/concepts/data-layer/_debt.md index 46eb004..e22af63 100644 --- a/.project/concepts/data-layer/_debt.md +++ b/.project/concepts/data-layer/_debt.md @@ -6,3 +6,4 @@ ## Raw markers (consolidate into blocks, then delete) - TOUCHED src/shared/data/readEntities.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) - TOUCHED src/shared/context/FestipodDataContext.tsx @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) +- TOUCHED src/shared/utils/ngBootstrap.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) diff --git a/src/modules/auth/steps/data/connexion.steps.ts b/src/modules/auth/steps/data/connexion.steps.ts index b156556..c37976c 100644 --- a/src/modules/auth/steps/data/connexion.steps.ts +++ b/src/modules/auth/steps/data/connexion.steps.ts @@ -5,27 +5,18 @@ import type { FestipodWorld } from '../../../../shared/support/world'; // --- Setup --- Given('le portefeuille est vide', async function (this: FestipodWorld) { - // Empty the wallet for real: with one-document-per-entity + a persistent broker, - // deleting entities means clearing the per-entity documents' CONTENT (the SDK - // clearWallet), not just flipping a store-root set. Then poll until the reactive - // read reflects the empty state. - await this.appFrame!.evaluate(async () => { - const td = (window as any).__testData; - await td.clearWallet(); - }); - await this.appFrame!.waitForFunction( - () => { - const td = (window as any).__testData; - return td.events.size === 0 && td.users.size === 0; - }, - { timeout: 30000 }, - ); + // Each @data scenario runs under a UNIQUE username (see hooks.ts + // freshScenarioUsername), so the shim hands it a FRESH, EMPTY virtual wallet: + // "le portefeuille est vide" is trivially true on entry. So this is a fast + // INSTANT CHECK — assert the reactive read already shows nothing — NOT the old + // `clearWallet` per-entity-doc fan-out (a full physical-wallet enumeration that + // was itself slow). No mutation, no polling: a fresh wallet has no docs to scan. const counts = await this.appFrame!.evaluate(() => { const td = (window as any).__testData; return { events: td.events.size, users: td.users.size }; }); - expect(counts.events, 'Events should be empty').to.equal(0); - expect(counts.users, 'Users should be empty').to.equal(0); + expect(counts.events, 'Fresh virtual wallet should have no events').to.equal(0); + expect(counts.users, 'Fresh virtual wallet should have no users').to.equal(0); }); Given('le portefeuille contient déjà des événements', async function (this: FestipodWorld) { diff --git a/src/shared/support/hooks.ts b/src/shared/support/hooks.ts index d226bc9..fcdc103 100644 --- a/src/shared/support/hooks.ts +++ b/src/shared/support/hooks.ts @@ -9,6 +9,24 @@ import { pool } from './browserPool'; setDefaultTimeout(90000); +// PER-SCENARIO FRESH VIRTUAL WALLET (T03.k). The shim keys each emulated account +// (its own private virtual wallet) by the NORMALIZED app-level username read from +// localStorage['festipod.account.username'] on the harness origin. When every +// @data scenario logs in as the SAME fixed user, that ONE virtual wallet +// accumulates every doc any prior scenario/run ever wrote → per-doc anchored +// reads fan out over hundreds of docs → 90s timeouts. Giving each scenario a +// UNIQUE username hands it a FRESH, EMPTY virtual wallet, so reads stay O(what +// THIS scenario provisions) and are fast + independent. A monotonic counter + +// per-run nonce guarantees uniqueness within and across runs; it normalizes to +// itself (lowercase, `@`-free) and is disjoint from the reserved `@index` +// account (whose shim key uses a sentinel prefix `normalizeUsername` can't emit). +const RUN_NONCE = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); +let scenarioSeq = 0; +function freshScenarioUsername(): string { + scenarioSeq += 1; + return `test-${RUN_NONCE}-${scenarioSeq}`; +} + let browser: Browser; let browserContext: BrowserContext; // Non-persistent launcher for fresh, isolated contexts (multi-browser scenarios). @@ -561,6 +579,20 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) { // the run self-heals instead of cascading failures across the rest. this.page = await newWalletPageResilient(); + // FRESH VIRTUAL WALLET per scenario (see freshScenarioUsername above). Set a + // UNIQUE app-level username into localStorage['festipod.account.username'] on + // EVERY origin (the init script runs in each frame before its scripts do — + // including the harness iframe on 127.0.0.1). At mount the harness's + // AccountStore.get() then reads THIS fresh username, so `if (!username) + // login(DEFAULT_HARNESS_USER)` is skipped and the scenario runs on a fresh, + // empty virtual wallet. Overwrites any value persisted in the Chromium profile + // (init scripts run on each navigation), so no accumulated wallet leaks in. + const freshUser = freshScenarioUsername(); + (this as any).freshUser = freshUser; + await this.page.addInitScript((u: string) => { + try { window.localStorage.setItem('festipod.account.username', u); } catch { /* opaque origin */ } + }, freshUser); + // Capture console for debugging this.page.on('pageerror', (err) => console.error('[Browser error]', err.message)); this.page.on('console', (msg) => { @@ -579,25 +611,12 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) { { timeout: 30000 }, ); - // PER-SCENARIO STATE ISOLATION (T03.j). The @data suite shares ONE - // persistent broker-backed wallet, so the emulated account registry - // ACCUMULATES every account any prior scenario/run created — growing the - // read fan-out (`allAccounts()` → per-account `listEntityDocs`) until it - // gets slow and flaky. Purge the registry anchor once here so each @data - // scenario starts from a CLEAN registry and the fan-out stays bounded to - // what this scenario re-provisions. A single SPARQL DELETE on ONE graph — - // not a fan-out delete. - // HARD-BOUNDED (≤10s): this reset shares the Before hook's 60s budget with - // the (already slow, intermittent) broker login. It must NEVER contend for - // that budget — a slow purge on a hugely-accumulated anchor graph, or a - // broker stall, is swallowed and the scenario proceeds (its own steps still - // gate on state). So race it against a 10s cap and never let it throw. - await Promise.race([ - this.appFrame.evaluate(async () => { - try { await (window as any).__testData?.resetDataState?.(); } catch { /* best-effort */ } - }), - new Promise((r) => setTimeout(r, 10000)), - ]).catch(() => { /* best-effort */ }); + // NO per-scenario registry/wallet reset needed anymore (was T03.j + // resetDataState). Each @data scenario now runs under a UNIQUE username + // (freshScenarioUsername, set into localStorage above), so the shim hands it + // a FRESH, EMPTY virtual wallet whose account registry starts empty by + // construction — nothing to purge. This also drops the ≤10s reset cost that + // shared the Before hook's budget with the (slow) broker login. } else { // Mock mode: load harness directly await this.page!.setContent('
'); diff --git a/src/shared/utils/ngBootstrap.ts b/src/shared/utils/ngBootstrap.ts index 50d746a..2d7a8a0 100644 --- a/src/shared/utils/ngBootstrap.ts +++ b/src/shared/utils/ngBootstrap.ts @@ -17,6 +17,7 @@ import { seedUsers, } from '../data/seedData'; import { writeEntity, ENTITY_TYPE, str, int, flt, bool } from '../data/entityWrites'; +import { submitEventToIndex } from '../data/discovery'; /** Scope of a seed entity + how to create its own document (SDK create). */ export type Scope = 'public' | 'protected' | 'private'; @@ -105,6 +106,12 @@ export async function bootstrapWallet( coverImage: str(e.coverImage), hostName: str(e.hostName), hostInitials: str(e.hostInitials), }); eventIdMap.set(e.id, id); + // Make the seeded PUBLIC event discoverable, exactly like the product's + // createEvent: submit its reference to the global discovery index. Awaited so + // the index is populated before any read (a fresh virtual wallet has no "own" + // seed docs — it sees the seeded events only through discovery). + await submitEventToIndex({ doc: graph, id, title: e.title }, null) + .catch(err => console.error('[Bootstrap] submit seed event to index failed:', err)); })); console.log('[Bootstrap] Seeded', eventIdMap.size, 'events (participations created live)');