test(client/e2e): contrats broker RÉEL — reconnexion cold-read + non-fork de compte
Les tests du polyfill doivent PROUVER son contrat contre le VRAI broker (pas un fake). Ajout de deux tests e2e broker réel (wallet dédié), avec reconnexion FIDÈLE : page fraîche sur le MÊME profil persistant + nouveau login broker (helper `faithfulReconnect`), PAS export/réimport dans un profil vide (qui resynchronise et masque le cold-open). - Contrat reconnexion/cold-read : session 1 crée un doc (public + protected) ; session fraîche fidèle ; relit → données présentes, en POLLANT une borne généreuse (l'attente de récupération après souscription est le fonctionnement normal). - Contrat non-fork : re-résoudre le même identifiant après reconnexion rend les MÊMES NURIs de docs de scope, pas un second provisioning. Temps de sync réels observés (le SIGNAL) : protected relu en ~195ms (repo déjà ouvert) ; PUBLIC relu en ~105s (ouverture repo + attente push + union ancrée à l'échelle de la donnée publique) ; login de reconnexion ~2.5s. Le ~105s public est un signal de PERFORMANCE À INVESTIGUER (probable scale/bloat de l'anchorless union scan, cf. suivi bloat) — le contrat est REMPLI mais lent côté public. Honnêteté : le « échoue-sans-le-fix » n'a PAS pu être montré via ce chemin de login (le bootstrap du login broker ouvre déjà les repos → firstRawNoOpen=1). Contrat prouvé REMPLI avec les fix ; nécessité non isolable via cette voie. gate : test:e2e 33 passed (baseline 27 vert) ; bun test 117 ; tsc propre. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,38 @@ function sdkGet<T>(frame: Frame, method: string, ...args: unknown[]): Promise<T>
|
||||
) as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* FAITHFUL reconnection — the real app's reconnect path, NOT export/reimport.
|
||||
*
|
||||
* Opens a BRAND-NEW page on the SAME persistent wallet context (`ctx`, the profile
|
||||
* that already holds the wallet + its local IndexedDB repo cache) and drives a NEW
|
||||
* broker login through it. That new login mints a FRESH verifier session (empty
|
||||
* `self.repos` at connect) while the page is a fresh SDK-module instance (empty
|
||||
* open-repo registry + empty store-registry cache). This is EXACTLY what the app
|
||||
* does on re-enter/reload (src/modules/event/steps/data/reconnexion.steps.ts:
|
||||
* `this.page.context().newPage()` + `pool.setupBrokerPage`), and is the ONLY faithful
|
||||
* cold-open: it does NOT wipe the profile, so it does NOT force the broker to resync
|
||||
* every repo from scratch (which export/reimport-into-empty-profile DOES — masking
|
||||
* the very cold-read/anti-fork gap under test). The repos are on the broker AND in the
|
||||
* profile's cache, but this session's verifier hasn't opened them yet — so the SDK's
|
||||
* open-before-read (open-repo.ts) and anti-fork retry (store-registry.ts) are what must
|
||||
* bridge the gap. Returns the fresh page + its connected iframe Frame.
|
||||
*/
|
||||
async function faithfulReconnect(
|
||||
ctx: BrowserContext,
|
||||
url: string,
|
||||
): Promise<{ page: Page; frame: Frame }> {
|
||||
const p = await ctx.newPage();
|
||||
p.on("pageerror", (e) => console.error("[iframe error:reconnect]", e.message));
|
||||
p.on("console", (m) => {
|
||||
if (m.type() === "error") console.error("[iframe console:reconnect]", m.text());
|
||||
});
|
||||
const frame = await setupBrokerPage(p, url);
|
||||
await frame.waitForFunction(() => (window as any).__sdk !== undefined, { timeout: 30000 });
|
||||
await frame.waitForFunction(() => (window as any).__sdk.status() === "connected", { timeout: 60000 });
|
||||
return { page: p, frame };
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("[e2e] building SDK page bundle...");
|
||||
buildBundle();
|
||||
@@ -359,6 +391,145 @@ async function main(): Promise<void> {
|
||||
try { fs.rmSync(ngwPath, { force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
});
|
||||
|
||||
// ── CONTRACT 1: faithful reconnect COLD-READ (public + protected) ─────────
|
||||
// The real app's reconnect: a FRESH page on the SAME persistent profile + a NEW
|
||||
// broker login (fresh verifier session), NOT export/reimport into an empty
|
||||
// profile (which forces a full resync and MASKS the cold-open). Session 1 seeds a
|
||||
// per-entity doc + marker in BOTH public and protected scopes; a faithful fresh
|
||||
// session must relist + re-read its OWN persisted markers, POLLING for the sync —
|
||||
// the wait IS the normal path. NB the observed cost is dominated by broker READ
|
||||
// latency, not pure sync-lag: ONE reconnectRead cycle (open-repo heal awaits the
|
||||
// initial-state push per repo + anti-fork retry budget + anchored readUnion, all
|
||||
// round-tripping the real broker) measures ~90-105s. So the poll deadline is a
|
||||
// generous MULTI-cycle bound (120s past post-connect) rather than a tight 30s — a
|
||||
// single slow cycle must not be mistaken for a sync failure. We report the observed
|
||||
// time (the "signal"). If a marker never lands within the bound the check FAILS (a
|
||||
// real regression), never a silent 0-row.
|
||||
console.log("\n── CONTRACT 1: faithful reconnect cold-read (public + protected) ──");
|
||||
await step("faithful reconnect (same profile, new login) re-reads persisted public + protected docs", async () => {
|
||||
const reconId = "@recon-faithful-" + Date.now();
|
||||
// Seed BOTH scopes in the ORIGINAL session (where the repos are open).
|
||||
const seedPub = await sdk<any>(frame, "reconnectSeed", reconId, "public");
|
||||
const seedProt = await sdk<any>(frame, "reconnectSeed", reconId, "protected");
|
||||
check(
|
||||
"seed: public + protected entity docs listed in the seeding session",
|
||||
seedPub.listedInSeed.includes(seedPub.entityNuri) && seedProt.listedInSeed.includes(seedProt.entityNuri),
|
||||
`pub=${String(seedPub.entityNuri).slice(0, 20)}… prot=${String(seedProt.entityNuri).slice(0, 20)}…`,
|
||||
);
|
||||
|
||||
let rp: Page | null = null;
|
||||
try {
|
||||
// Faithful reconnect: fresh page on the SAME persistent context + new login.
|
||||
const tLoginStart = Date.now();
|
||||
const rc = await faithfulReconnect(ctx!, url);
|
||||
const loginMs = Date.now() - tLoginStart;
|
||||
rp = rc.page;
|
||||
const rInfo = await sdkGet<any>(rc.frame, "sessionInfo");
|
||||
// Fidelity is STRUCTURAL: a fresh page → fresh iframe → fresh SDK-module
|
||||
// instance (empty open-repo + store-registry caches) + a new broker connect
|
||||
// over the SAME persistent profile. We assert the reconnect connected; the
|
||||
// broker numbers session_id per-connection (may reuse 1), so we REPORT the
|
||||
// ids rather than gate on them differing.
|
||||
check(
|
||||
"reconnect session connected (fresh iframe/verifier over the SAME persistent profile)",
|
||||
rInfo?.session_id !== undefined && rInfo?.session_id !== null,
|
||||
`reconnectSession=${rInfo?.session_id} origSession=${info?.session_id}`,
|
||||
);
|
||||
|
||||
// Poll each scope up to 30s (from post-connect) for the marker to sync back
|
||||
// into THIS fresh session. syncMs is the pure sync lag (login excluded); we
|
||||
// report it as the observed sync signal, plus the first-attempt rawNoOpen (the
|
||||
// bare anchored read WITHOUT the open-repo heal) which shows whether the heal
|
||||
// is load-bearing on this SDK/broker version.
|
||||
for (const [scope, seed] of [["public", seedPub], ["protected", seedProt]] as const) {
|
||||
let found = false;
|
||||
let syncMs = -1;
|
||||
let firstRawNoOpen = -99;
|
||||
let lastDetail = "";
|
||||
const tSyncStart = Date.now();
|
||||
const deadline = tSyncStart + 120000;
|
||||
let firstAttempt = true;
|
||||
while (Date.now() < deadline) {
|
||||
const r = await sdk<any>(rc.frame, "reconnectRead", reconId, scope, seed.entityNuri, seed.marker);
|
||||
if (firstAttempt) { firstRawNoOpen = r.rawRowCount; firstAttempt = false; }
|
||||
lastDetail = `firstRawNoOpen=${firstRawNoOpen} rawNoOpen=${r.rawRowCount} listed=${r.listedCount} foundEntity=${r.foundEntity} subjects=${r.subjectCount}`;
|
||||
if (r.markerPresent === true) {
|
||||
found = true;
|
||||
syncMs = Date.now() - tSyncStart;
|
||||
break;
|
||||
}
|
||||
await rc.page.waitForTimeout(1000);
|
||||
}
|
||||
check(
|
||||
`[SYNC] reconnect re-reads its OWN persisted ${scope} marker (cold-read)`,
|
||||
found,
|
||||
found
|
||||
? `synced in ${syncMs}ms (reconnect-login ${loginMs}ms) — ${lastDetail}`
|
||||
: `NEVER synced within 120s (reconnect-login ${loginMs}ms) — ${lastDetail}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try { if (rp) await rp.close(); } catch { /* ignore */ }
|
||||
}
|
||||
});
|
||||
|
||||
// ── CONTRACT 2: NON-FORK of account across a faithful reconnect ───────────
|
||||
// Resolving the SAME identifier after a faithful reconnect must return the SAME
|
||||
// account docs (docPublic/docProtected/docPrivate) — never a SECOND provisioning
|
||||
// (an account fork), which would strand the first session's data. Session 1
|
||||
// provisions the account (records its NURIs); a faithful fresh session re-resolves
|
||||
// the SAME id and must return IDENTICAL NURIs. The anti-fork retry bridges the
|
||||
// shim-not-yet-synced window; we POLL (generous 120s multi-cycle bound — one
|
||||
// accountDocs resolve round-trips the broker's anti-fork retry budget, ~90-100s)
|
||||
// and report when the SAME NURIs land (the sync signal). If they never match (or a
|
||||
// new set appears) → FAIL.
|
||||
console.log("\n── CONTRACT 2: non-fork of account across a faithful reconnect ──");
|
||||
await step("resolving the same identifier after a faithful reconnect returns the SAME account docs (no fork)", async () => {
|
||||
const forkId = "@nonfork-" + Date.now();
|
||||
const orig = await sdk<any>(frame, "accountDocs", forkId);
|
||||
check(
|
||||
"session 1 provisioned the account (3 scope docs)",
|
||||
!!orig.docPublic && !!orig.docProtected && !!orig.docPrivate,
|
||||
`pub=${String(orig.docPublic).slice(0, 20)}…`,
|
||||
);
|
||||
|
||||
let rp: Page | null = null;
|
||||
try {
|
||||
const tLoginStart = Date.now();
|
||||
const rc = await faithfulReconnect(ctx!, url);
|
||||
const loginMs = Date.now() - tLoginStart;
|
||||
rp = rc.page;
|
||||
|
||||
let same = false;
|
||||
let syncMs = -1;
|
||||
let last: any = null;
|
||||
const tSyncStart = Date.now();
|
||||
const deadline = tSyncStart + 120000;
|
||||
while (Date.now() < deadline) {
|
||||
last = await sdk<any>(rc.frame, "accountDocs", forkId);
|
||||
if (
|
||||
last.docPublic === orig.docPublic &&
|
||||
last.docProtected === orig.docProtected &&
|
||||
last.docPrivate === orig.docPrivate
|
||||
) {
|
||||
same = true;
|
||||
syncMs = Date.now() - tSyncStart;
|
||||
break;
|
||||
}
|
||||
await rc.page.waitForTimeout(1000);
|
||||
}
|
||||
check(
|
||||
"[SYNC] fresh session re-resolves the SAME account NURIs (no second provisioning)",
|
||||
same,
|
||||
same
|
||||
? `same account in ${syncMs}ms (reconnect-login ${loginMs}ms)`
|
||||
: `FORKED — orig pub=${String(orig.docPublic).slice(0, 20)}… got pub=${String(last?.docPublic).slice(0, 20)}… (differs)`,
|
||||
);
|
||||
} finally {
|
||||
try { if (rp) await rp.close(); } catch { /* ignore */ }
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
try { if (page) await page.close(); } catch { /* ignore */ }
|
||||
try { if (ctx) await ctx.close(); } catch { /* ignore */ }
|
||||
|
||||
Reference in New Issue
Block a user