/** * Signing in CONNECTS — and settling, on its own, does not reach for the session. * * These two are one subject seen from both ends, and the applicative e2e is what found it: * after Alice shared a protected note with Bob, Bob reopened the application and read * "(illisible)". Nothing threw anywhere. It cost minutes of real browser and real broker to * see, so it is pinned here for the price of a millisecond. * * ── The mechanism, so a future reader can judge a change against it ──────── * Settling the identity called `setCurrentUser`, which FIRES the connection work. Firing is * not awaiting, but it is still running: the work's first act is `resolveAccount`, which * awaits the consumer's `getSession` thunk. Settling happens before `init()` has been * delegated to — and the reference application builds its `sessionReady` promise AROUND * that very `init()` call, so at that instant the promise it would wait on does not exist. * The thunk could not answer. It threw, `resolveAccount` answered null, and the run * abandoned before restoring a single capability — after registering itself as the * connection in flight. The `connectedUser()` that `ensureIdentity()` awaits then JOINED * that abandoned run instead of doing the work, and resolved having done nothing. The * application rendered, and Bob held no key to a note that had been shared with him. * * So what is pinned is not "the calls happen in this order" — a regression would still call * them in order. It is what each half TOUCHES: settling must not call the session thunk at * all, and signing in must not come back until the thunk has actually answered. */ import { test, expect, afterEach } from "bun:test"; import { configure, ensureIdentity } from "../src/index"; import { init } from "../src/surface/lifecycle"; import { resetConfig, resetStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap"; import type { RegistrySession } from "../src/shared-wallet/account-registry"; const APP = "https://app.example/"; /** A localStorage double — the real one is absent in `bun test`. */ function fakeStorage() { const map = new Map(); return { getItem: (k: string) => map.get(k) ?? null, setItem: (k: string, v: string) => void map.set(k, v), removeItem: (k: string) => void map.delete(k), }; } /** * A page whose address bar MOVES on `replaceState`, as the gate and `init()` both rely on. * * INSIDE THE BROKER IFRAME (`window.self !== window.top`), and that is not decoration: it * is the side of the frontier this whole file describes. The identifier has already crossed * in the URL, so the barrier stands aside; and the session exists only here, opened by the * callback `init()` is given. Top-level, `init()` navigates away and no session is ever * established, so the deadlock pinned below could not even be reached. */ function inBrokerIframe(url: string): void { let href = url; Object.assign(globalThis, { location: { get href(): string { return href; }, get search(): string { return new URL(href).search; }, }, history: { replaceState: (_s: unknown, _t: string, next: string): void => void (href = next) }, localStorage: fakeStorage(), window: { self: {}, top: {}, addEventListener: (): void => {} }, }); } const PAGE_GLOBALS = ["location", "localStorage", "history", "document", "window"] as const; afterEach(() => { setCurrentUser(null); resetConfig(); resetStoreRegistry(); for (const name of PAGE_GLOBALS) Reflect.deleteProperty(globalThis, name); }); /** * The reference application's bootstrap, in its real order (`examples/notebook/app.ts`): * `configure()` first, then `init()` called from INSIDE the executor that builds the very * promise the session thunk waits on. * * That shape is not a curiosity of this fixture, it is what the e2e serves: in the bundle, * `sessionReady` is a hoisted `var`, so while the executor runs it is still `undefined` and * the thunk has nothing to wait on. It therefore **refuses** rather than blocking — and an * application is entitled to refuse, since at that moment there is genuinely nothing to * return. A thunk that merely blocked would hide the whole defect, which is why the double * here refuses exactly as the application's does. * * The injected `init` resolves the session, as the real one does through its callback: * before it is called, nothing in the system can make the session exist. */ function bootTheApplication(identifier: string) { /** What the consumer's thunk was asked, and what it was able to say. */ const thunk = { asked: 0, answered: 0, refused: 0 }; let session: RegistrySession | null = null; // Deliberately assigned AFTER `init()` runs — see above. `undefined` until then. let sessionReady: Promise | undefined; let arrive!: (s: RegistrySession) => void; configure({ ng: {} as never, // no store behind it: the assertions are about what is REACHED useShape: (() => {}) as never, init: (..._args: unknown[]): Promise => { arrive({ sessionId: "s", privateStoreId: "did:ng:o:private" }); return Promise.resolve("delegated"); }, getSession: async (): Promise => { thunk.asked += 1; try { const s = session ?? (await sessionReady!); const answer = { sessionId: s.sessionId, privateStoreId: s.privateStoreId }; thunk.answered += 1; return answer; } catch (refusal) { thunk.refused += 1; throw refusal; } }, normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(), sharedWallet: { fileUrl: "/w.ngw", password: "pw" }, }); inBrokerIframe(`${APP}?ng-id=${encodeURIComponent(identifier)}`); let delegated!: Promise; sessionReady = new Promise((resolve) => { arrive = resolve; delegated = init(() => {}, true, []) as Promise; }); void sessionReady.then((s) => { session = s; }); return { thunk, delegated }; } test("settling the identity never asks the application for a session", async () => { // The session-free half, taken at its word. `init()` awaits settling and nothing else, so // by the time it has delegated, the consumer's thunk must not have been called ONCE — // not called-and-blocked, not called-and-refused, not called at all. Anything the gate // reaches that ends up at `getSession` is outside the half it claims to be. const app = bootTheApplication("bob"); await app.delegated; expect(app.thunk.asked).toBe(0); expect(app.thunk.refused).toBe(0); }); test("signing in does not come back until the connection work has reached a live session", async () => { // The consequence, from the application's side. `ensureIdentity()` promises that what was // shared with you is readable when it resolves; it can only keep that promise by having // restored and drained, and both begin by resolving the account — which needs the // session. So a run that resolved without the thunk ever ANSWERING did no such work, // whatever it reported. That is exactly the state Bob's page was in. const app = bootTheApplication("bob"); await app.delegated; await ensureIdentity(); expect(app.thunk.answered).toBeGreaterThanOrEqual(1); expect(app.thunk.refused).toBe(0); });