/** * 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 session. Settling happens before `init()` has been delegated to — and the * reference application built its `sessionReady` promise AROUND that very `init()` call, so * at that instant the promise it would wait on did not exist. The session could not be * answered. 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 reach for the session at * all, and signing in must not come back until the session has actually answered. * * ── What moved on 2026-08-12, and what it does not excuse ───────────────── * The session is no longer the application's to supply: the package captures it from * `init()`'s callback and holds it (`shared-wallet/session.ts`), so the promise the * connection work waits on is now the library's own. That closes the *shape* of the failure * above — a holder that WAITS cannot throw, so a run can no longer abandon for want of an * answer. It closes nothing about the ordering: the session still arrives only through * `init()`, so a half that reached for it too early would still be waiting on something * that does not exist yet. Both properties below are therefore still worth their assertion, * and the fixture instruments the package's own holder rather than a consumer's thunk. */ import { test, expect, afterEach } from "bun:test"; import { configure, ensureIdentity } from "../src/index"; import { init } from "../src/surface/lifecycle"; import { configureStoreRegistry, resetConfig, resetStoreRegistry, setCurrentUser, } from "../src/shared-wallet/bootstrap"; import { sharedWalletSession } from "../src/shared-wallet/session"; 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()`, then `init()` — and nothing else, since the session stopped being * something an application assembles. * * The injected `init` produces the session the way the real one does: by calling the * callback it was handed, with `{ status: "loggedin", session }` (`ngweb.js:124`). Before * it is called, nothing in the system can make the session exist — which is the whole * cycle these two tests measure. * * The counters sit on the REGISTRY's route to the session, substituted through the internal * wiring path. That is where the connection work actually asks, so it is where "was it * reached, and did it answer" can be told apart. `refused` is kept though the package's * holder cannot throw: a future change that put a refusing thunk back on this route is * exactly the regression the file exists to catch, and a counter nobody kept would let it * back in silently. */ function bootTheApplication(identifier: string) { /** What the registry's route to the session was asked, and what it was able to say. */ const thunk = { asked: 0, answered: 0, refused: 0 }; configure({ ng: {} as never, // no store behind it: the assertions are about what is REACHED useShape: (() => {}) as never, init: (...args: unknown[]): Promise => { const callback = args[0]; if (typeof callback === "function") { void (callback as (e: unknown) => unknown)({ status: "loggedin", session: { session_id: "s", private_store_id: "did:ng:o:private" }, }); } return Promise.resolve("delegated"); }, sharedWallet: { fileUrl: "/w.ngw", password: "pw" }, }); configureStoreRegistry({ getSession: async (): Promise => { thunk.asked += 1; try { const answer = await sharedWalletSession(); thunk.answered += 1; return answer; } catch (refusal) { thunk.refused += 1; throw refusal; } }, normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(), }); inBrokerIframe(`${APP}?ng-id=${encodeURIComponent(identifier)}`); const delegated = init(() => {}, true, []) as Promise; return { thunk, delegated }; } test("settling the identity never reaches 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 session must not have been asked for ONCE — not // asked-and-blocked, not asked-and-refused, not asked at all. Anything the gate reaches // that ends up at the session 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 session 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); });