/** * Real-broker e2e runner for `@ng-eventually/client` — the polyfill's OWN suite, * in the SDK domain (no application concepts), with a DEDICATED wallet. * * Standalone (NOT `bun test`), so it never mixes into the fake-ng unit suite. * Run: `bun run e2e/run.ts` (or `bun run test:e2e` from packages/client). * * It: builds the SDK page bundle, creates/reuses the dedicated lib wallet, opens * the broker iframe on the real broker with that wallet, waits for `window.__sdk` * to connect, then drives every polyfill behavior through the bridge and asserts * the real-broker outcomes. Each check is event-driven where reactivity matters * (waitForFunction on a counter), never a blind sleep. */ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import type { Frame, Page, BrowserContext } from "playwright"; import { buildBundle, serveHarness, ensureWallet, launchWalletContext, launchCleanProfileContext, importWalletViaFile, setupBrokerPage, } from "./broker"; type Check = { name: string; ok: boolean; detail?: string }; const results: Check[] = []; function record(name: string, ok: boolean, detail?: string): void { results.push({ name, ok, detail }); const tag = ok ? "PASS" : "FAIL"; console.log(` [${tag}] ${name}${detail ? " — " + detail : ""}`); } function check(name: string, cond: boolean, detail?: string): void { record(name, !!cond, detail); } async function step(name: string, fn: () => Promise): Promise { try { await fn(); } catch (e: any) { record(name, false, "threw: " + String(e?.message ?? e)); } } // A short helper: call a bridge method inside the iframe. function sdk(frame: Frame, method: string, ...args: unknown[]): Promise { return frame.evaluate( ([m, a]) => (window as any).__sdk[m as string](...(a as unknown[])), [method, args] as const, ) as Promise; } function sdkGet(frame: Frame, method: string, ...args: unknown[]): Promise { // Same as sdk() but for synchronous getters (no await inside the bridge). return frame.evaluate( ([m, a]) => (window as any).__sdk[m as string](...(a as unknown[])), [method, args] as const, ) as Promise; } /** * 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 { console.log("[e2e] building SDK page bundle..."); buildBundle(); console.log("[e2e] ensuring dedicated lib wallet..."); await ensureWallet(); const { url, close: closeServer } = await serveHarness(); console.log(`[e2e] harness served at ${url}`); let ctx: BrowserContext | null = null; let page: Page | null = null; try { ctx = await launchWalletContext(); page = await ctx.newPage(); page.on("pageerror", (e) => console.error("[iframe error]", e.message)); page.on("console", (m) => { if (m.type() === "error") console.error("[iframe console]", m.text()); }); console.log("[e2e] loading SDK page in broker iframe..."); const frame = await setupBrokerPage(page, url); // Wait for the bridge to exist + the broker session to connect. await frame.waitForFunction(() => (window as any).__sdk !== undefined, { timeout: 30000 }); await frame.waitForFunction(() => (window as any).__sdk.status() === "connected", { timeout: 60000, }); const info = await sdkGet(frame, "sessionInfo"); check("broker session connected", info?.session_id !== undefined && info?.session_id !== null, `session=${JSON.stringify(info)}`); // ── docs primitives ───────────────────────────────────────────────────── console.log("\n── docs primitives ──"); await step("docCreate returns a usable NURI", async () => { const nuri = await sdk(frame, "docCreate"); check("docCreate returns a usable NURI", typeof nuri === "string" && nuri.length > 0, nuri); }); await step("SPARQL graph-behavior characterization (a/b/c)", async () => { const rt = await sdk(frame, "docRoundTrip"); // (a) THE load-bearing assertion fake-ng cannot verify: the anchored default- // graph write (no GRAPH clause) round-trips through the real broker's // repo_graph_name overlay. This is the canonical shape the lib writes, and // what read-model / inbox / store-registry all rely on. check( "(a) anchored default-graph write (no GRAPH) ROUND-TRIPS", rt.anchoredPresent === true, `predicates=${JSON.stringify(rt.predicates)}`, ); // (b) FINDING (reported, not gating): on THIS broker version an explicit // `INSERT DATA { GRAPH {…} }` ANCHORED to the same doc ALSO // round-trips — readable both via the anchored default-graph read // (explicitGraphPresent) AND via an explicit `GRAPH ` read // (explicitViaNamedGraph). i.e. when anchored, the plain NURI resolves to the // SAME repo graph — there is NO "phantom graph" here. The lib still writes the // no-GRAPH default-graph shape as the always-safe canonical convention; this // records what the broker actually does so the "phantom graph" comments can be // re-checked against this broker version by re-running this harness. record( "(b) [finding] explicit GRAPH ANCHORED resolves to the same repo (no phantom graph)", true, `defaultGraphRead=${rt.explicitGraphPresent} namedGraphRead=${rt.explicitViaNamedGraph} (informational)`, ); // (c) FINDING: the ANCHORLESS `GRAPH ?g { … }` union scan spans EVERY named // graph in the session store (it saw BOTH doc A's and doc B's graphs). This is // the O(wallet-size) cost the read path avoids by reading each doc with its own // anchored default-graph query. Reported, not gating (it is a perf property of // the union, not a correctness assertion of the lib's write/read shape). const us = rt.unionSpan ?? {}; record( "(c) [finding] anchorless GRAPH ?g scan spans ALL named graphs (O(wallet) union)", true, us.graphCount === -1 ? `anchorless scan errored: ${us.error} (union claim NOT re-verified here)` : `sawDocA=${us.sawDocA} sawDocB=${us.sawDocB} graphCount=${us.graphCount} (both ⇒ union spans all graphs)`, ); }); // ── read-model ────────────────────────────────────────────────────────── console.log("\n── read-model ──"); await step("readUnion over N docs → per-doc subjects", async () => { const r = await sdk(frame, "readUnionOverDocs", 3, false); check("readUnion returns one subject per doc", r.subjectCount === 3, `subjects=${r.subjectCount}/3`); }); await step("readUnion per-doc tolerance (bad NURI skipped)", async () => { const r = await sdk(frame, "readUnionOverDocs", 2, true); check("bad NURI does not abort the batch", r.subjectCount === 2, `subjects=${r.subjectCount}/2 (+1 bad)`); }); await step("readUnion cap gate", async () => { const r = await sdk(frame, "readUnionCapGate"); check("cap gate drops doc for stranger, keeps for owner", r.strangerCount === 0 && r.ownerCount === 1, `stranger=${r.strangerCount} owner=${r.ownerCount}`); }); // ── reactivity (doc_subscribe) ────────────────────────────────────────── console.log("\n── reactivity (doc_subscribe) ──"); await step("subscribeDoc initial + on-write", async () => { const { doc } = await sdk(frame, "subscribeDocStart", "h1"); // initial state push (event-driven wait) await frame.waitForFunction(() => (window as any).__sdk.subscribeCount("h1") >= 1, { timeout: 20000 }); const initial = await sdkGet(frame, "subscribeCount", "h1"); check("subscribeDoc fires on initial state", initial >= 1, `count=${initial}`); // subsequent real write → another push await sdk(frame, "writeTo", doc, "w1"); await frame.waitForFunction( (base) => (window as any).__sdk.subscribeCount("h1") > (base as number), initial, { timeout: 20000 }, ); const afterWrite = await sdkGet(frame, "subscribeCount", "h1"); check("subscribeDoc fires on a subsequent write", afterWrite > initial, `count=${afterWrite} (>${initial})`); // unsubscribe stops callbacks await sdk(frame, "subscribeStop", "h1"); const frozen = await sdkGet(frame, "subscribeCount", "h1"); await sdk(frame, "writeTo", doc, "w2"); await page!.waitForTimeout(3000); const afterUnsub = await sdkGet(frame, "subscribeCount", "h1"); check("unsubscribe stops callbacks", afterUnsub === frozen, `count stayed ${afterUnsub}`); }); await step("subscribeDocs per-doc isolation", async () => { const { good } = await sdk(frame, "subscribeDocsStart"); await frame.waitForFunction(() => (window as any).__sdk.multiSubCounts().good >= 1, { timeout: 20000 }); const base = await sdkGet(frame, "multiSubCounts"); await sdk(frame, "writeTo", good, "mw1"); await frame.waitForFunction( (b) => (window as any).__sdk.multiSubCounts().good > (b as number), base.good, { timeout: 20000 }, ); const after = await sdkGet(frame, "multiSubCounts"); check("good doc fires despite a dead doc in the set", after.good > base.good, `good=${after.good} bad=${after.bad}`); await sdk(frame, "multiSubStop"); }); // ── inbox ─────────────────────────────────────────────────────────────── console.log("\n── inbox ──"); await step("inbox post → read round-trip", async () => { const r = await sdk(frame, "inboxPostRead", { k: "a" }, { k: "b" }); const payloads = (r.deposits || []).map((d: any) => JSON.stringify(d.payload)); check( "post then read returns both deposits (sorted)", r.deposits.length === 2 && payloads.includes('{"k":"a"}') && payloads.includes('{"k":"b"}'), `deposits=${r.deposits.length}`, ); }); await step("inbox watch fires on deposit", async () => { await sdk(frame, "inboxWatchStart"); await frame.waitForFunction(() => (window as any).__sdk.inboxWatchState().fires >= 1, { timeout: 20000 }); const base = await sdkGet(frame, "inboxWatchState"); await sdk(frame, "inboxWatchDeposit", { landed: true }); await frame.waitForFunction( (b) => (window as any).__sdk.inboxWatchState().fires > (b as number), base.fires, { timeout: 20000 }, ); const after = await sdkGet(frame, "inboxWatchState"); check("watch fires when a deposit lands", after.fires > base.fires && after.lastLen >= 1, `fires=${after.fires} lastLen=${after.lastLen}`); await sdk(frame, "inboxWatchStop"); }); await step("inbox spoof guard", async () => { const r = await sdk(frame, "inboxSpoofGuard"); check("post as another principal is rejected; self + anon allowed", r.spoofRejected && r.selfOk && r.anonOk, `spoof=${r.spoofRejected} self=${r.selfOk} anon=${r.anonOk}`); }); // ── discovery index ───────────────────────────────────────────────────── console.log("\n── discovery index ──"); await step("discovery submit → read", async () => { const ref = { doc: "did:ng:o:some-public-doc", title: "t" }; const r = await sdk(frame, "discoverySubmitRead", ref); const refs = (r.entries || []).map((e: any) => JSON.stringify(e.ref)); check("submitToIndex then readIndex returns the entry", refs.includes(JSON.stringify(ref)), `entries=${r.entries.length}`); }); await step("discovery watchIndex fires reactively", async () => { await sdk(frame, "discoveryWatchStart"); await frame.waitForFunction(() => (window as any).__sdk.discoveryWatchState().fires >= 1, { timeout: 20000 }); const base = await sdkGet(frame, "discoveryWatchState"); await sdk(frame, "discoverySubmit", { doc: "did:ng:o:doc2", title: "t2", n: Date.now() }); await frame.waitForFunction( (b) => (window as any).__sdk.discoveryWatchState().fires > (b as number), base.fires, { timeout: 20000 }, ); const after = await sdkGet(frame, "discoveryWatchState"); check("watchIndex fires on a new submission", after.fires > base.fires, `fires=${after.fires}`); await sdk(frame, "discoveryWatchStop"); }); await step("reserved @index account isolation", async () => { const r = await sdk(frame, "discoveryIndexIsolation"); check("user '@index' resolves disjoint from the reserved index owner", r.disjoint === true, `disjoint=${r.disjoint}`); }); // ── store-registry ────────────────────────────────────────────────────── console.log("\n── store-registry ──"); await step("ensureAccount idempotent", async () => { const r = await sdk(frame, "ensureAccountIdempotent", "@alice-" + Date.now()); check("ensureAccount returns the same 3 docs on repeat", r.same === true, `same=${r.same}`); }); await step("createEntityDoc + listMyEntityDocs bounded to one account", async () => { const t = Date.now(); const r = await sdk(frame, "entityDocsBounded", "@ea-" + t, "@eb-" + t); check("listMyEntityDocs lists A's docs and does NOT leak B's", r.hasA1 && r.hasA2 && !r.leaksB, `A1=${r.hasA1} A2=${r.hasA2} leaksB=${r.leaksB} listA=${r.listA.length}`); }); await step("scope resolvers", async () => { const r = await sdk(frame, "scopeResolvers"); check("scope resolvers return NURIs (private distinct from protected/public)", !!r.priv && !!r.prot && !!r.pub && r.priv !== r.prot, `priv=${r.priv?.slice(0,16)}… prot=${r.prot?.slice(0,16)}…`); }); // ── caps / read-filter (in-memory cap model) ──────────────────────────── console.log("\n── caps / read-filter (in-memory cap model) ──"); await step("read-filter: protected hidden from stranger", async () => { const r = await sdk(frame, "capsReadFilter"); const ownerSeesProt = r.ownerView.includes("protected-item"); const strangerHiddenProt = !r.strangerView.includes("protected-item"); const bothSeePublic = r.ownerView.includes("public-item") && r.strangerView.includes("public-item"); const bothSeeUngoverned = r.ownerView.includes("ungoverned-item") && r.strangerView.includes("ungoverned-item"); check("owner reads protected; stranger does not; public+ungoverned visible to both", ownerSeesProt && strangerHiddenProt && bothSeePublic && bothSeeUngoverned, `owner=${JSON.stringify(r.ownerView)} stranger=${JSON.stringify(r.strangerView)}`); }); await step("read-filter: directed grant reveals the doc", async () => { const r = await sdk(frame, "capsDirectedGrant"); check("grantRead reveals the protected doc to the grantee", r.before === 0 && r.after === 1, `before=${r.before} after=${r.after}`); }); // ── accounts (IdentityStore) ──────────────────────────────────────────── console.log("\n── accounts (IdentityStore) ──"); await step("IdentityStore set/get/clear", async () => { const setr = await sdk(frame, "identitySet", "@ident-user"); const got = await sdkGet(frame, "identityGet"); const cleared = await sdk(frame, "identityClear"); check("IdentityStore set→get→clear", setr === "@ident-user" && got === "@ident-user" && cleared === null, `set=${setr} get=${got} cleared=${cleared}`); }); // ── reconnection cold-start (real-broker regression) ───────────────────── // Phase 1 (THIS session): seed — create a per-entity doc under (id, protected) // and write a marker triple (`protected` carries participations). Also EXPORT the // wallet `.ngw` so phase 2 can re-import the SAME wallet into a CLEAN browser // profile. // // Phase 2 (the faithful reconnect): import the wallet into a BRAND-NEW empty // profile dir (no local IndexedDB copy of the repos) and open a fresh SDK session // over it — the repos exist on the BROKER but NOT in this profile's local cache, // so this is a true reconnect (not a same-profile relaunch, which masks the gap by // eagerly rehydrating repos from local storage). Then re-read purely from the // wallet and assert the marker comes back. // // FINDING (recorded, see the digest): on THIS SDK/broker version the marker also // comes back WITHOUT the open-before-read heal — the broker-login bootstrap opens // the user's repos before the read (the `rawAnchoredNoOpen` detail below shows the // bare anchored query already resolves rows). So this test is a real-broker // REGRESSION guard for reconnection reads, NOT a fail-without-the-fix proof; the // cold-start the fix targets was diagnosed in the app and does not reproduce // through this harness's login path. console.log("\n── reconnection cold-start (real-broker regression) ──"); await step("fresh session (clean profile, same wallet) re-reads a persisted entity doc", async () => { const reconId = "@recon-" + Date.now(); const scope = "protected"; const seed = await sdk(frame, "reconnectSeed", reconId, scope); check( "seed: entity doc created + listed in the seeding session", seed.listedInSeed.includes(seed.entityNuri), `entity=${String(seed.entityNuri).slice(0, 24)}… listedInSeed=${seed.listedInSeed.length} origSession=${info?.session_id}`, ); // Export the wallet file and materialize it for the clean-profile import. const exp = await sdk(frame, "exportWalletFile"); const ngwPath = path.join(os.tmpdir(), `ng-eventually-recon-${Date.now()}.ngw`); fs.writeFileSync(ngwPath, Buffer.from(exp.b64, "base64")); let cleanCtx: BrowserContext | null = null; let cleanDir: string | null = null; let cleanPage: Page | null = null; try { const launched = await launchCleanProfileContext(); cleanCtx = launched.ctx; cleanDir = launched.dir; cleanPage = await cleanCtx.newPage(); cleanPage.on("pageerror", (e) => console.error("[iframe error:clean]", e.message)); cleanPage.on("console", (m) => { if (m.type() === "error") console.error("[iframe console:clean]", m.text()); }); // Import the SAME wallet into the empty profile (broker-only repos), then open // the SDK page in a fresh broker session over it. await importWalletViaFile(cleanPage, ngwPath); const cleanFrame = await setupBrokerPage(cleanPage, url); await cleanFrame.waitForFunction(() => (window as any).__sdk !== undefined, { timeout: 30000 }); await cleanFrame.waitForFunction(() => (window as any).__sdk.status() === "connected", { timeout: 60000 }); const cleanInfo = await sdkGet(cleanFrame, "sessionInfo"); check( "clean-profile session connected (fresh verifier, broker-only repos)", cleanInfo?.session_id !== undefined && cleanInfo?.session_id !== null, `session=${cleanInfo?.session_id}`, ); const r = await sdk(cleanFrame, "reconnectRead", reconId, scope, seed.entityNuri, seed.marker); check( "fresh clean-profile session re-reads the persisted marker (reconnection regression)", r.markerPresent === true, `rawAnchoredNoOpen=${r.rawRowCount} listed=${r.listedCount} foundEntity=${r.foundEntity} subjects=${r.subjectCount} markerPresent=${r.markerPresent}`, ); } finally { try { if (cleanPage) await cleanPage.close(); } catch { /* ignore */ } try { if (cleanCtx) await cleanCtx.close(); } catch { /* ignore */ } try { if (cleanDir) fs.rmSync(cleanDir, { recursive: true, force: true }); } catch { /* ignore */ } 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(frame, "reconnectSeed", reconId, "public"); const seedProt = await sdk(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(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(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(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(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 */ } closeServer(); } // ── 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 ══`); if (failed > 0) { console.log("Failures:"); for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`); process.exit(1); } } main().catch((e) => { console.error("[e2e] fatal:", e); process.exit(1); });