Ng eventually #1

Open
Sylvain wants to merge 110 commits from ng-eventually into main
6 changed files with 63 additions and 36 deletions
Showing only changes of commit 02cda056b8 - Show all commits
+7
View File
@@ -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)
+2
View File
@@ -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)
+1
View File
@@ -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)
+8 -17
View File
@@ -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) {
+38 -19
View File
@@ -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('<!DOCTYPE html><html><body><div id="root"></div></body></html>');
+7
View File
@@ -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)');