diff --git a/packages/client/e2e/broker.ts b/packages/client/e2e/broker.ts index 095762b..144d58b 100644 --- a/packages/client/e2e/broker.ts +++ b/packages/client/e2e/broker.ts @@ -72,15 +72,38 @@ export function serveHarness(): Promise<{ url: string; close: () => void }> { /** * Create the dedicated lib wallet once (headless UI flow on nextgraph.eu), - * persisted in PROFILE_DIR. Mirrors Festipod's ensureAuth but with the lib's own - * wallet name + profile. Idempotent via the ready marker. + * persisted in PROFILE_DIR for the duration of the batch. + * + * ── One PHYSICAL user per batch, not one forever ────────────────────────── + * This used to reuse a single wallet across every run, guarded by a ready marker. That + * made the suite slow itself down, monotonically: each batch mints ~11 FRESH virtual + * identities (`@alice-…`, `@owner-…`, `@recon-…`), each with three scope documents and + * an inbox, and they all land in the SAME physical user. Nothing ever removed them. A + * cold resynchronisation is O(the physical user's size) — which this library's own docs + * state — so the wallet created on 2026-07-10 had grown enough to take 286s on a single + * sync step, against 250s a week earlier, and the drift was invisible because no one + * measured it. + * + * The fresh identities are not the mistake — they are what makes a batch reproducible + * (a stable inbox accumulates its past runs' deposits otherwise). The mistake was + * keeping the physical user that holds them. So: a new one per batch, which also makes + * the cold-sync duration comparable from one run to the next instead of being a number + * that only ever grows. + * + * What this does NOT change: the profile stays persistent WITHIN a batch, because + * CONTRACT 1 and 2 test exactly that (a faithful reconnect over the same profile, and + * the absence of an account fork across it). */ export async function ensureWallet(): Promise { if (fs.existsSync(WALLET_READY_MARKER)) { - console.log("[e2e] dedicated lib wallet present — skipping creation"); - return; + const age = Date.now() - fs.statSync(WALLET_READY_MARKER).mtimeMs; + console.log( + `[e2e] discarding the previous batch's wallet (${Math.round(age / 60000)} min old) — ` + + "a physical user is per-batch, see ensureWallet", + ); + fs.rmSync(PROFILE_DIR, { recursive: true, force: true }); } - console.log("[e2e] creating dedicated lib wallet on nextgraph.eu..."); + console.log("[e2e] creating this batch's wallet on nextgraph.eu..."); fs.mkdirSync(PROFILE_DIR, { recursive: true }); const ctx = await chromium.launchPersistentContext(PROFILE_DIR, { headless: true, diff --git a/packages/client/e2e/run.ts b/packages/client/e2e/run.ts index 2785b2c..ea61356 100644 --- a/packages/client/e2e/run.ts +++ b/packages/client/e2e/run.ts @@ -91,6 +91,37 @@ async function faithfulReconnect( return { page: p, frame }; } +/** + * The batch's own budget, and the measurement that explains an overrun. + * + * Not a `timeout` wrapped around the command from outside: when that fired it killed the + * browser, and the suite reported `Target page, context or browser has been closed` — + * which reads as an application bug and was twice diagnosed as one. A budget belongs to + * the thing that knows what it is spending it on, and it must say so when it runs out. + */ +const BATCH_BUDGET_MS = 45 * 60 * 1000; +const batchStart = Date.now(); +/** + * The slowest cold resynchronisation of the batch — the number that drifted from 250s to + * 286s over a month without anyone looking, because it only ever appeared inside one + * step's detail line. It is the health indicator of the physical user, so it is reported + * with the summary. + */ +let coldSyncMs = 0; + +/** Fail with the cause named, rather than letting a killed browser look like a defect. */ +function assertWithinBudget(): void { + const spent = Date.now() - batchStart; + if (spent > BATCH_BUDGET_MS) { + throw new Error( + `[e2e] batch budget exceeded (${Math.round(spent / 60000)} min > ` + + `${BATCH_BUDGET_MS / 60000} min). This is almost always the physical user having ` + + "grown: a cold resync is O(its size). Check the cold-sync figure printed above — " + + "it should be stable from batch to batch now that each gets a fresh wallet.", + ); + } +} + async function main(): Promise { console.log("[e2e] building SDK page bundle..."); buildBundle(); @@ -522,7 +553,8 @@ async function main(): Promise { `[SYNC] reconnect re-reads its OWN persisted ${scope} marker (cold-read)`, found, found - ? `synced in ${syncMs}ms (reconnect-login ${loginMs}ms) — ${lastDetail}` + ? ((coldSyncMs = Math.max(coldSyncMs, syncMs)), + `synced in ${syncMs}ms (reconnect-login ${loginMs}ms) — ${lastDetail}`) : `NEVER synced within 120s (reconnect-login ${loginMs}ms) — ${lastDetail}`, ); } @@ -744,7 +776,14 @@ async function main(): Promise { // ── Summary ─────────────────────────────────────────────────────────────── const passed = results.filter((r) => r.ok).length; const failed = results.length - passed; - console.log(`\n══ SDK e2e summary: ${passed} passed, ${failed} failed, ${results.length} total ══`); + const batchMin = ((Date.now() - batchStart) / 60000).toFixed(1); + console.log( + `\n══ SDK e2e summary: ${passed} passed, ${failed} failed, ${results.length} total ` + + `— batch ${batchMin} min, slowest cold sync ${Math.round(coldSyncMs / 1000)}s ══`, + ); + // A fresh wallet per batch is what should keep the cold sync flat; if it climbs from + // one batch to the next, the per-batch wallet is not being discarded. + assertWithinBudget(); if (failed > 0) { console.log("Failures:"); for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`);