test(@wip): reconnexion même identité — documente le défaut de relecture à froid

Scénario @data @wip (exclu du run par défaut) qui documente un défaut RÉEL non
encore corrigé : une PAGE FRAÎCHE pour la MÊME identité, sur le même wallet
persistant (nouveau login broker → session verifier fraîche), relit VIDE ses
propres données (accueil vide, isParticipating=false, count=0).

Mesuré au niveau app sur broker réel. Le fix lib `open-repo` (branche
fix/session-rehydration-on-login, non mergée) fait remonter le PROTECTED
(participation) au cold-start mais PAS l'accueil PUBLIC : `readScopeIndex` de
l'index de scope public rend 0 — observé même côté écrivain même-session — alors
que le code d'index de la lib est prouvé scope-symétrique. Cause exacte encore à
mesurer sous broker (l'hypothèse « mauvais graphe » est déjà réfutée en amont).

Reste @wip tant que le fix n'est pas complet et validé.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-08 10:45:51 +02:00
parent c869c56a17
commit 005c052bc6
2 changed files with 116 additions and 0 deletions
@@ -0,0 +1,32 @@
# language: fr
@EVENT @priority-1 @data @wip
Fonctionnalité: Reconnexion d'une même identité sur le wallet persistant
En tant qu'utilisateur qui, sur le MÊME wallet physique, ouvre une PAGE FRAÎCHE
(nouveau login broker, session fraîche) sous le MÊME identifiant qu'avant,
Je dois relire MES PROPRES données (mon événement, ma participation)
Afin que rien ne disparaisse à la reconnexion.
# Régression de RECONNEXION (distincte de l'isolation deux-identités). @wip : le
# défaut est RÉEL mais non encore corrigé ce scénario le documente et échoue
# tant que le fix n'est pas complet.
#
# SYMPTÔME observé (broker réel, mesuré) : une identité A crée E + s'y inscrit
# sur la page principale ; une PAGE FRAÎCHE pour la MÊME identité A (même wallet,
# nouveau login session verifier fraîche) relit VIDE (home=[],
# isParticipating=false, count=0). La page fraîche DOIT retrouver E, sa
# participation, et un count autoritatif de 1.
#
# MÉCANISME (sous investigation, à confirmer sous broker) : la lecture ancrée à
# froid tape des repos pas encore ouverts. Le fix lib `open-repo` fait remonter
# le PROTECTED (participation) au cold-start, mais PAS l'accueil PUBLIC :
# `readScopeIndex` de l'index de scope public rend 0 (observé même côté écrivain
# même-session) alors que le code d'index de la lib est scope-symétrique cause
# exacte encore à mesurer (broker requis).
@data
Scénario: Une page fraîche pour la même identité relit ses propres données
Étant donné que l'identité A crée l'événement "Événement de reconnexion de A" et s'y inscrit
Quand une page fraîche pour la MÊME identité A recharge sur le même wallet
Alors l'événement "Événement de reconnexion de A" est sur l'accueil de la page fraîche A
Et la page fraîche A est participante de l'événement "Événement de reconnexion de A"
Et le compte autoritatif de participation de A à l'événement "Événement de reconnexion de A" est 1
@@ -0,0 +1,84 @@
import { When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import type { FestipodWorld } from '../../../../shared/support/world';
import { pool } from '../../../../shared/support/browserPool';
// RECONNECTION of the SAME identity on the SAME persistent wallet (@data, real
// broker). Distinct from the two-identity isolation scenario: here the fresh page
// re-enters under the SAME identifier A (same virtual wallet), on a NEW broker
// login (fresh verifier session). The defect under test: the fresh page reads its
// OWN data EMPTY (home=[], isParticipating=false, authCount=0) because the anchored
// listing path (listMyEntityDocs → readScopeIndex, then readUnion/readDoc) queries
// repos not yet in `self.repos` at cold-start and silently returns 0 rows.
//
// Identity A is the scenario's fresh virtual-wallet username (this.freshUser, set
// by the Before hook into localStorage on every origin). A creates E via the REAL
// app path (createEventReal) and joins it (appJoinEvent) on the MAIN page. Then a
// FRESH PAGE is brought up on the SAME persistent wallet context with the SAME
// identifier A in localStorage BEFORE any script — the closest analogue to the real
// app's re-enter-gate / reload path (a brand-new NgDataProvider mount + a fresh
// broker session that must re-open A's own repos). Montage identical to
// isolation.steps.ts, except the fresh page reuses this.freshUser (SAME A) rather
// than minting a new identifier B.
// The Given "l'identité A crée l'événement {string} et s'y inscrit" is REUSED from
// isolation.steps.ts (same wording, same behavior — A creates E and joins on the
// main page). It stores this.isoEventId / this.isoAId, which the steps below read.
When('une page fraîche pour la MÊME identité A recharge sur le même wallet', { timeout: 120000 }, async function (this: FestipodWorld) {
// SAME identity A as the main page: reuse the scenario's fresh virtual-wallet
// username (set by the Before hook). NOT a new identifier — this is a reconnect,
// not an identity switch.
const aIdentifier = (this as any).freshUser as string;
const ctx = this.page!.context();
const freshPage = await ctx.newPage();
await freshPage.addInitScript((u: string) => {
try { window.localStorage.setItem('festipod.account.username', u); } catch { /* opaque */ }
}, aIdentifier);
await freshPage.addInitScript(() => {
(globalThis as Record<string, unknown>).__FESTIPOD_ACCESS_GATE_DISABLED__ = true;
});
freshPage.on('console', (msg) => { if (msg.type() === 'error') console.error('[FreshApage console]', msg.text()); });
// New broker login → fresh verifier session on the SAME persistent wallet.
const freshFrame = await pool.setupBrokerPage!(freshPage, pool.harnessUrl!);
await freshFrame.waitForFunction(() => (window as any).__testData?.ready === true, { timeout: 60000 });
// Let A's listing effect + anchored union read run on the fresh session (this is
// exactly the cold-start read path the fix heals).
await freshFrame.evaluate(async () => {
const td = (window as any).__testData;
await td.ensureCurrentUser();
await new Promise(r => setTimeout(r, 6000));
});
(this as any).recoFreshFrame = freshFrame;
});
Then('l\'événement {string} est sur l\'accueil de la page fraîche A', async function (this: FestipodWorld, title: string) {
const freshFrame = (this as any).recoFreshFrame;
const onHome = await freshFrame.evaluate((title: string) => {
const td = (window as any).__testData;
return td.homeEventTitles().includes(title);
}, title);
expect(onHome, `"${title}" MUST appear on the fresh A page's home (getUserEvents(A) after reconnect)`).to.be.true;
});
Then('la page fraîche A est participante de l\'événement {string}', async function (this: FestipodWorld, title: string) {
const freshFrame = (this as any).recoFreshFrame;
const eventId = (this as any).isoEventId;
const isPart = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => {
const td = (window as any).__testData;
const aId = await td.ensureCurrentUser();
return td.isParticipating(eventId, aId);
}, { eventId });
expect(isPart, `The fresh A page MUST read its own participation in "${title}" after reconnect`).to.be.true;
});
Then('le compte autoritatif de participation de A à l\'événement {string} est {int}', { timeout: 60000 }, async function (this: FestipodWorld, _title: string, expected: number) {
const freshFrame = (this as any).recoFreshFrame;
const eventId = (this as any).isoEventId;
const count = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => {
const td = (window as any).__testData;
const aId = await td.ensureCurrentUser();
return td.authParticipationCount(eventId, aId);
}, { eventId });
expect(count, `Authoritative broker count of A's participation must be ${expected} after reconnect`).to.equal(expected);
});