/** * reload-inbox-drain.test.ts — signing in again, over an inbox that was opened last time. * * ── The report this reproduces ──────────────────────────────────────────── * *"`ensureIdentity()` rejects inside its own inbox processing, about a document the * application never named. The application deposits through a published call; the failure * happens afterwards, inside the package."* * * The document nobody named is an INBOX. An application opens one on a note it owns * (`storeRegistry.openDocumentInbox`) and hands out nothing: the deposit side names the * NOTE (`inbox.postToDocument`) and the read side names it too (`inbox.readForDocument`). * There is deliberately no published way to ask for an address, so the inbox document is * the package's from end to end. * * Connecting drains every inbox the user may read — its own two, plus one per document it * opened one on, read back from the User branch (the emulated `AddInboxCap`). Reading an * inbox is a guarded read like any other, so a session that does not hold the inbox's key * cannot drain it. And that key reaches the owner's hands exactly once, when the inbox is * MINTED; nothing puts it back on a later session. * * So the second connection asks for a document it may not touch, is refused, and the * refusal comes back out of `ensureIdentity()` — not as an empty screen but as a rejected * sign-in. It will be rejected at every future connection too, because an inbox that could * not be drained is still on the list next time. * * ── Why the applicative journey never saw it ────────────────────────────── * `e2e/notebook.ts` journey 3 has Alice open her note for messages and Bob deposit into * it; Bob reopens the application, Alice never does. The journey stops one reconnection * short of the state this suite starts from. */ import { test, expect, describe, afterAll, beforeEach } from "bun:test"; import { docs, inbox as inboxSurface, storeRegistry } from "../src/index"; import { getCaps } from "../src/shared-wallet/bootstrap"; import { runScheduledInboxProcessingNow } from "../src/emulated-verifier/inbox-processor"; import { bootPage, forgetEverything, reloadPage, signIn, SESSION, type Quad } from "./wallet-fake"; import type { Nuri } from "../src/model/types"; const TITLE = "urn:test:title"; const MESSAGE = "j'apporte le café"; /** * The first visit, in the application's own vocabulary: Alice writes a note and opens it * for messages; Bob leaves one on it. Both of them name the NOTE and nothing else. * * Returns the note, which is all an application ever holds — the inbox NURI is read off * the wallet afterwards, by the test alone, to say WHICH document the refusal is about. */ async function aNoteOpenForMessages(quads: Quad[]): Promise { bootPage(quads); await signIn("alice"); const note = await storeRegistry.createEntityDoc("public"); await docs.sparqlUpdate( SESSION.sessionId, `INSERT DATA { <${note}> <${TITLE}> "Courses" }`, note, "writeEntity", ); await storeRegistry.openDocumentInbox(note); await signIn("bob"); await inboxSurface.postToDocument(note, { payload: { text: MESSAGE }, from: "bob", ts: 1 }); return note; } /** * The inbox Alice's note was opened on, read off the WALLET — the emulated `AddInboxCap` * record, which is where the connection's drain list comes from. The test asks the wallet * because no application can ask the package. */ function inboxOnTheNote(quads: Quad[], note: Nuri): Nuri { const record = quads.find((q) => q.p === "urn:ng-eventually:shim:inboxCap" && q.o.startsWith(note + " ")); if (!record) throw new Error("no AddInboxCap record was written for the note"); return record.o.split(" ")[1] as Nuri; } /** Alice's note, found the way her application finds it: by listing her own store. */ async function myNote(): Promise { const mine = await storeRegistry.listMyEntityDocs("public"); const note = mine[0]; if (!note) throw new Error("the note Alice wrote is not in her store"); return note; } beforeEach(() => { forgetEverything(); }); afterAll(() => { forgetEverything(); }); describe("signing in again, after opening one of my documents for messages", () => { test("the connection work `ensureIdentity()` awaits does not reject", async () => { const quads: Quad[] = []; await aNoteOpenForMessages(quads); reloadPage(quads); await signIn("alice"); }); test("the reconnected session holds the key of the inbox it is asked to drain", async () => { const quads: Quad[] = []; const note = await aNoteOpenForMessages(quads); const inbox = inboxOnTheNote(quads, note); reloadPage(quads); await signIn("alice").catch(() => undefined); expect(getCaps().capFor(inbox)).toBeDefined(); }); test("the message left on my note is there when I come back", async () => { const quads: Quad[] = []; await aNoteOpenForMessages(quads); reloadPage(quads); await signIn("alice").catch(() => undefined); const mine = await inboxSurface.readForDocument(await myNote()); expect(mine.map((d) => (d.payload as { text: string }).text)).toEqual([MESSAGE]); }); // The deferred drain runs under whoever is connected — Bob, here — and reports to the // access log rather than to a caller. It must not decide whether the owner can sign in. test("a deferred drain having run first does not change the owner's sign-in", async () => { const quads: Quad[] = []; await aNoteOpenForMessages(quads); await runScheduledInboxProcessingNow(); reloadPage(quads); await signIn("alice"); }); });