/** * cold-read-for-document.test.ts — coming back to the messages left on my note. * * ── The gap this closes ─────────────────────────────────────────────────── * The inbox surface offered a read with the SYNC GUARANTEE (`readSynced`) and a read * ADDRESSED BY DOCUMENT (`readForDocument`), and not their intersection. An application * materializing deposits needs both, so it had to resolve an inbox address itself — the * one gesture the contract says an application never performs. * * ── Why the document-addressed path needs the barrier TWICE ─────────────── * Reading a document's messages crosses two repos: the DOCUMENT, whose Header branch * carries the address, and the INBOX, which carries the deposits. On a fresh session over * the same persistent wallet both are present and unsynced, and an anchored read of an * unsynced repo returns no rows — no error (`emulated-verifier/open-repo.ts`). So the * ADDRESS read comes back empty, `readForDocument` concludes "this document has no inbox", * and answers `[]` for a note whose inbox holds the message somebody left on it. * * That is the state this suite starts from, reached the way a real page reaches it: Alice * writes a note and opens it for messages, Bob leaves one, and Alice comes back on a new * page. Nothing is planted — a second page sees exactly what the first one WROTE, and the * broker fake only withholds what this page has not subscribed to yet * (`wallet-fake.ts`, `unsyncedUntilSubscribed`). * * The pair of tests is the point: on ONE state, the ungated read answers empty and the * gated one answers the message. A test that only showed `readSyncedForDocument` returning * deposits would pass just as well over a plain `read`. */ import { test, expect, describe, afterAll, beforeEach } from "bun:test"; import { docs, inbox as inboxSurface, storeRegistry } from "../src/index"; import { getSyncState } from "../src/emulated-verifier/open-repo"; 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 broker's own cold start — see `wallet-fake.WalletOptions`. */ const COLD = { unsyncedUntilSubscribed: true } as const; /** * The first visit, in the application's own vocabulary: Alice writes a note and opens it * for messages; Bob leaves one on it. Both name the NOTE and nothing else. * * Returns the note, which is all an application ever holds. */ async function aNoteWithAMessageOnIt(quads: Quad[]): Promise { bootPage(quads, COLD); 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 the note was opened on, read off the WALLET — the emulated `AddInboxCap` * record. The test asks the wallet because no application can ask the package: there is * deliberately no published call that hands out an address, which is the whole reason * `readSyncedForDocument` has to exist. */ 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; } /** Alice comes back on a NEW page, over the wallet the first one wrote. */ async function aliceComesBack(quads: Quad[]): Promise { reloadPage(quads, COLD); await signIn("alice"); return myNote(); } beforeEach(() => { forgetEverything(); }); afterAll(() => { forgetEverything(); }); describe("reading the messages left on my note, on a page that has just loaded", () => { test("the ungated document-addressed read answers EMPTY — the note's repo never synced", async () => { const quads: Quad[] = []; await aNoteWithAMessageOnIt(quads); const note = await aliceComesBack(quads); // Not a failure anyone can see: the message is on the broker, Alice owns the inbox, // and the call returns a perfectly ordinary empty list. expect(await inboxSurface.readForDocument(note)).toEqual([]); // …because nothing ever brought the NOTE into view. The address lives on it. expect(getSyncState(note)).toBe("unknown"); }); test("the synced document-addressed read answers the message, over that same state", async () => { const quads: Quad[] = []; await aNoteWithAMessageOnIt(quads); const note = await aliceComesBack(quads); const mine = await inboxSurface.readSyncedForDocument(note); expect(mine.map((d) => (d.payload as { text: string }).text)).toEqual([MESSAGE]); expect(mine.map((d) => d.from)).toEqual(["bob"]); }); test("it crosses the sync barrier on BOTH repos the answer depends on", async () => { const quads: Quad[] = []; const written = await aNoteWithAMessageOnIt(quads); const inbox = inboxOnTheNote(quads, written); const note = await aliceComesBack(quads); await inboxSurface.readSyncedForDocument(note); // The guarantee itself, not the payload: past the first `State` on each, presence is // guaranteed and absence definitive — so an empty answer would MEAN empty. The note's // barrier is the one this call adds (nothing else on the page opens a note); the // inbox's is `readSynced`'s, and connecting may have crossed it already. expect(getSyncState(note)).toBe("synced"); expect(getSyncState(inbox)).toBe("synced"); }); test("a document nobody opened an inbox on answers empty, not an error", async () => { const quads: Quad[] = []; bootPage(quads, COLD); await signIn("alice"); const bare = await storeRegistry.createEntityDoc("public"); reloadPage(quads, COLD); await signIn("alice"); expect(await inboxSurface.readSyncedForDocument(bare)).toEqual([]); }); test("it is still a read of MY inbox — the owner's guard is not bypassed", async () => { const quads: Quad[] = []; await aNoteWithAMessageOnIt(quads); const note = await aliceComesBack(quads); // Bob can find where to deposit for Alice's public note, and that is all: reading it // would collect the caps addressed to her. A second door onto the same read must not // be a way around the guard the first one carries. await signIn("bob"); await expect(inboxSurface.readSyncedForDocument(note)).rejects.toThrow( /does not belong to the connected wallet/i, ); }); });