/** * document-inbox-surfaces-failure.test.ts - "this document has no inbox" must be an ANSWER. * * -- The defect this pins, and why it is worse than it looks --------------- * `documentInboxAddress` reads a document's Header branch for the address to deposit at, * and answers `undefined` when there is none. Two published calls turn that value into a * statement an application acts on: `inbox.postToDocument` says "its owner never opened * one", and `inbox.readForDocument` says zero deposits. * * Until `1.0.0-dev.9` it answered `undefined` for two things that are not that. The public * store could not be asked for the document's cap, and `fetchReadCap` reported a breakdown * with the same `false` a document exposing no cap answers with; or the address read itself * broke, and a `catch` returned `undefined`. Either way a depositor was told the document * has no inbox, and a reader was handed an empty list - for a document whose inbox is full. * That is the failure-as-absence this package has closed roughly eight times. * * -- What is under test, and what must NOT move --------------------------- * The pair is the point, and neither half proves anything alone. A document nobody opened * an inbox on still answers empty, and still without an error: **an absence is an answer**, * and a repair that turned it into a failure would only have moved the lie. The tests below * run BOTH over the same harness, so one cannot be satisfied at the other's expense. * * -- The fault is the broker's -------------------------------------------- * Nothing here reaches into the library to make one of its functions throw. The world is * built through the surface - Alice writes a public note and opens it for messages - and * then the fake broker stops answering for one named repo (`wallet-fake._failReadsOn`), * which is what `resolve_target_for_sparql` does upstream for a repo the verifier does not * hold: it rejects, and never answers 0 rows. */ import { test, expect, describe, afterAll, beforeEach } from "bun:test"; import { inbox as inboxSurface, storeRegistry } from "../src/index"; import { bootPage, forgetEverything, signIn, type Quad } from "./wallet-fake"; import type { Nuri } from "../src/model/types"; /** The broker's own cold start, so the barrier is a real one here too. */ const COLD = { unsyncedUntilSubscribed: true } as const; beforeEach(() => { forgetEverything(); }); afterAll(() => { forgetEverything(); }); describe("a document's inbox address says nothing it does not know", () => { test("a broker that cannot answer for the document is a REJECTION, not 'no inbox'", async () => { const quads: Quad[] = []; const fake = bootPage(quads, COLD); await signIn("alice"); const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); await storeRegistry.openDocumentInbox(note); // Bob holds nothing: the public store is what would serve him the note's cap, and it // is exactly that round-trip the broker now cannot answer. await signIn("bob"); fake._failReadsOn.add(note); // Before `1.0.0-dev.9` this resolved to `undefined` and `postToDocument` turned it // into "its owner never opened one" - a statement about Alice's note, made out of a // broker outage. Bob would have gone away believing there was nowhere to write. await expect( inboxSurface.postToDocument(note, { payload: { text: "j'apporte le cafe" } }), ).rejects.toThrow(/UNKNOWN/); }); test("the reader is told too: a failed address read does not read as zero deposits", async () => { const quads: Quad[] = []; const fake = bootPage(quads, COLD); await signIn("alice"); const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); await storeRegistry.openDocumentInbox(note); // Alice's own page, so the cap question never arises and `fetchReadCap` is a memo hit: // what breaks is the ADDRESS read itself, which used to be caught and answered // `undefined`. This is the second of the two routes, and it needs its own test because // repairing the first one leaves it untouched. fake._failReadsOn.add(note); await expect(inboxSurface.readForDocument(note)).rejects.toThrow(/RepoNotFound/); await expect(inboxSurface.readSyncedForDocument(note)).rejects.toThrow(/RepoNotFound/); }); test("and a document nobody opened an inbox on still answers EMPTY, and succeeds", async () => { const quads: Quad[] = []; bootPage(quads, COLD); await signIn("alice"); // Created through the surface and never opened for messages: a genuine absence, on a // broker answering perfectly well. This must stay a success, or the repair above // would have swapped one lie for another. const bare: Nuri = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); expect(await inboxSurface.readForDocument(bare)).toEqual([]); expect(await inboxSurface.readSyncedForDocument(bare)).toEqual([]); // And the deposit side says the same thing in its own words - a refusal that names // the real state, not an outage. await expect( inboxSurface.postToDocument(bare, { payload: { text: "hello" } }), ).rejects.toThrow(/has no inbox/); }); });