Files
ng-eventually/packages/polyfill/test/cold-read-for-document.test.ts
T

171 lines
7.5 KiB
TypeScript

/**
* 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 { listMyEntityDocs } from "../src/shared-wallet/account-registry";
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<Nuri> {
bootPage(quads, COLD);
await signIn("alice");
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
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 by listing her own store.
*
* That listing is the LIBRARY's own path now, not an application's: `listMyEntityDocs`
* stopped being published in `1.0.0-dev.5` (no counterpart at any level upstream), so
* this reaches it where it lives. What is under test is unchanged - the cold store still
* has to answer, and it still has to refile the caps while it does. */
async function myNote(): Promise<Nuri> {
const mine = await listMyEntityDocs("alice", "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<Nuri> {
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(await storeRegistry.publicStore());
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,
);
});
});