/** * Only a VERIFIED ABSENCE may mint an inbox — the whole case space of `userInbox`. * * `userInbox(id, scope)` answers *which inbox document does this virtual user own for this * scope*, and mints one the first time. Everything downstream is addressed through that * answer: `share` deposits into it, `connect.connectedUser` drains it, `isOwnInbox` guards * reads with it. So the one thing it must never do is treat a failure as an absence — where * `connect.ts` lost a restore for the same fault (2026-08-13), this one WRITES: * * - a read that could not answer, taken for "no inbox exists", MINTS a second document for * one (user, scope). Two `shim:docInbox` triples then sit on the same subject and which * one wins later is decided by `canonicalDoc` picking among them — the owner and a * depositor can resolve different documents; * - a persist that failed, followed by handing the document back anyway, gives the owner a * reference whose triple was never written. Nobody else can resolve it: a depositor * finding nothing mints yet another. The owner reads a box nobody writes to, depositors * write to boxes nobody reads. * * The rule this file pins, one line: **a failure surfaces; only an absence the broker * actually confirmed may mint.** Resolving from the cache, joining a run in flight, and * minting on a confirmed 0 are unchanged — they are the states that are true. * * ── Why every branch is here, not just the interesting ones ─────────────── * A swallowed failure is invisible by construction, so a suite covering the happy path and * one error leaves exactly the places a bug hides. Every path through the function is * asserted below: the cache, a run in flight (both outcomes), each of the four collaborators * it awaits before deciding (session, doc-shim, account, the read), the read answering and * the read finding nothing, and each of the three writes the mint performs. * * ── The faults are the broker's, not the test's ─────────────────────────── * Every failure is injected at the `ng` boundary — a read that throws `RepoNotFound` (what * the engine hard-errors when a repo is not in `self.repos`, * `engine/verifier/src/request_processor.rs:264`), a write or a `doc_create` that cannot * reach the broker — or at the consumer's injected `getSession`, which is the other edge of * the library. Nothing reaches into the library to make one of its own functions reject: a * fake that fabricates a state the real system never produces goes green while leaving the * real state untested. */ import { test, expect, mock, afterEach } from "bun:test"; import { configure } from "../src/index"; import { adoptCurrentUser, configureStoreRegistry, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, } from "../src/shared-wallet/bootstrap"; import { createEntityDoc, ensureAccount, isKnownInbox, resetRegistryCache, userInbox, } from "../src/shared-wallet/account-registry"; import type { RegistrySession } from "../src/shared-wallet/account-registry"; import { documentInboxAddress, openDocumentInbox, } from "../src/emulated-verifier/branch-registers"; import { resetOpenedRepos } from "../src/emulated-verifier/open-repo"; import type { InboxScope, Nuri } from "../src/model/types"; const SESSION: RegistrySession = { sessionId: "sid-inbox", privateStoreId: "PRIV-INBOX" }; const SHIM = "urn:ng-eventually:shim"; interface Quad { g: string; s: string; p: string; o: string } /** * What the broker refuses to do, and when. Every field arms a REAL failure of the * corresponding platform call; `false` means "answer normally". Mutable after `inject`, so * a test can build a healthy world first and only then break the one call it is about. */ interface Faults { /** The consumer's `getSession` thunk rejects — the wallet cannot answer. */ session: boolean; /** `doc_create` throws — the broker cannot mint a document. */ docCreate: boolean; /** The doc-shim read that answers "which inbox does this user own" throws. */ inboxLookup: boolean; /** The shim write that records "this NURI IS an inbox" throws. */ inboxRecord: boolean; /** The shim write that associates the inbox with its (user, scope) throws. */ inboxPersist: boolean; /** The store-root read that answers "where is the doc-shim" throws. */ pointerRead: boolean; /** The store-root write that publishes the doc-shim's address throws. */ pointerWrite: boolean; /** The doc-shim read that answers "does this account exist" throws. */ accountLookup: boolean; /** The doc-shim write that records an account's three scope documents throws. */ accountRecord: boolean; /** The User-branch write that records "I opened this document's inbox" throws. */ inboxCapPersist: boolean; /** The Header-branch write that publishes WHERE to deposit for a document throws. */ addressPublish: boolean; } function noFaults(): Faults { return { session: false, docCreate: false, inboxLookup: false, inboxRecord: false, inboxPersist: false, pointerRead: false, pointerWrite: false, accountLookup: false, accountRecord: false, inboxCapPersist: false, addressPublish: false, }; } /** Reverse of the lib's `escapeLiteral`: one left-to-right pass over `\x`. */ function unescapeLiteral(s: string): string { let out = ""; for (let i = 0; i < s.length; i++) { if (s[i] === "\\" && i + 1 < s.length) { const next = s[++i]; out += next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? "\t" : next!; } else out += s[i]; } return out; } /** * A stateful fake `ng` over an in-memory quad store, modelling the pointer → doc-shim * indirection the registry is built on: the store-root graph carries the write-once pointer, * the doc-shim carries the account records, the inbox index and the (user, scope) → inbox * associations. Same shape as the one `anti-fork.test.ts` drives the registry with, plus the * fault switches above. * * `doc_subscribe` pushes a first `State` so the barrier `ensureRepoOpen` waits on resolves * at once — the platform's real behaviour, and it keeps every failure below attributable to * the call that was armed rather than to a missing primitive. */ function makeFakeNg(faults: Faults) { const quads: Quad[] = []; let docCounter = 0; /** How many times the (user, scope) → inbox association was READ. */ let inboxLookups = 0; const doc_create = mock(async () => { if (faults.docCreate) throw new Error("BrokerError: cannot create document"); return `did:ng:o:idoc${++docCounter}`; }); const sparql_update = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[2] as string | undefined; if (query.includes(`${SHIM}:isInbox`) && faults.inboxRecord) { throw new Error("BrokerError: cannot write the inbox index"); } if (query.includes(`${SHIM}:docInbox`) && faults.inboxPersist) { throw new Error("BrokerError: cannot write the inbox association"); } if (query.includes(`${SHIM}:shimDoc`) && faults.pointerWrite) { throw new Error("BrokerError: cannot write the pointer"); } if (query.includes(`${SHIM}:docPublic`) && faults.accountRecord) { throw new Error("BrokerError: cannot write the account record"); } if (query.includes(`${SHIM}:inboxCap`) && faults.inboxCapPersist) { throw new Error("BrokerError: cannot write the inbox cap"); } // Only the INSERT half: the `DELETE` that clears the previous address lands, which is // the state that bites — the old address gone and the new one never written. if ( query.includes("INSERT DATA") && query.includes(`${SHIM}:inboxAddress`) && faults.addressPublish ) { throw new Error("BrokerError: cannot publish the inbox address"); } // `DELETE WHERE {

?x }` — the clear half of a Header-branch replacement. const dm = query.match(/DELETE WHERE\s*\{\s*<([^>]+)>\s+<([^>]+)>\s+\?\w+\s*\}/); if (dm) { for (let i = quads.length - 1; i >= 0; i--) { const q = quads[i]!; if (q.g === anchor && q.s === dm[1] && q.p === dm[2]) quads.splice(i, 1); } return undefined; } // `INSERT DATA { GRAPH { … } }` — the shape the store-ROOT pointer write uses; // everything else writes the anchored default graph. const gm = query.match(/GRAPH\s+<([^>]+)>\s*\{([\s\S]*)\}/); const g = gm ? gm[1]! : anchor; if (!g) return undefined; const body = gm ? gm[2]! : query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, ""); const sm = body.match(/<([^>]+)>/); if (!sm) return undefined; const s = sm[1]!; const after = body.slice(body.indexOf(sm[0]) + sm[0].length); const pairRe = /(?:a|<([^>]+)>)\s+(?:"((?:[^"\\]|\\.)*)"|<([^>]+)>)/g; let m: RegExpExecArray | null; while ((m = pairRe.exec(after)) !== null) { const p = m[1] ?? `${SHIM}:Account`; const o = m[2] !== undefined ? unescapeLiteral(m[2]) : (m[3] ?? ""); quads.push({ g, s, p, o }); } return undefined; }); const rows = (anchor: string | undefined, pred: string, name: string) => ({ results: { bindings: quads .filter((q) => q.g === anchor && q.p === pred) .map((q) => ({ [name]: { value: q.o } })), }, }); const sparql_query = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[3] as string | undefined; // Store-root pointer → the doc-shim. if (query.includes(`<${SHIM}:shimDoc>`)) { if (faults.pointerRead) throw new Error("RepoNotFound"); return rows(anchor, `${SHIM}:shimDoc`, "shimDoc"); } // The account record — the read `lookupAccount` issues, bounded to one subject. if (query.includes(`<${SHIM}:id>`)) { if (faults.accountLookup) throw new Error("RepoNotFound"); const subjM = query.match(/<([^>]+)>\s+a\s+/); const only = subjM ? subjM[1]! : null; const bySubject = new Map>(); for (const q of quads) { if (q.g !== anchor) continue; if (only !== null && q.s !== only) continue; const rec = bySubject.get(q.s) ?? {}; if (q.p === `${SHIM}:id`) rec.id = q.o; if (q.p === `${SHIM}:docPublic`) rec.docPublic = q.o; if (q.p === `${SHIM}:docProtected`) rec.docProtected = q.o; if (q.p === `${SHIM}:docPrivate`) rec.docPrivate = q.o; bySubject.set(q.s, rec); } return { results: { bindings: [...bySubject.values()].filter((r) => r.id).map((r) => ({ id: { value: r.id! }, docPublic: { value: r.docPublic ?? "" }, docProtected: { value: r.docProtected ?? "" }, docPrivate: { value: r.docPrivate ?? "" }, })), }, }; } // Which inbox does this (user, scope) own — the read the whole file is about. if (query.includes(`${SHIM}:docInbox`)) { inboxLookups += 1; if (faults.inboxLookup) throw new Error("RepoNotFound"); const pm = query.match(/<(urn:ng-eventually:shim:docInbox:[a-z]+)>/); const pred = pm ? pm[1]! : ""; const sm = query.match(/<([^>]+)>\s+ q.g === anchor && q.p === pred && (subj === null || q.s === subj)) .map((q) => ({ d: { value: q.o } })), }, }; } if (query.includes(`${SHIM}:isInbox`)) return rows(anchor, `${SHIM}:isInbox`, "i"); // The registers a document and a store carry — enough for `createEntityDoc`, // `ownsDocument` and `openDocumentInbox` to run against this wallet. if (query.includes(`<${SHIM}:exposedReadCap>`)) return rows(anchor, `${SHIM}:exposedReadCap`, "c"); if (query.includes(`<${SHIM}:readCap>`)) return rows(anchor, `${SHIM}:readCap`, "c"); if (query.includes(`<${SHIM}:contains>`)) return rows(anchor, `${SHIM}:contains`, "e"); if (query.includes(`<${SHIM}:inboxCap>`)) return rows(anchor, `${SHIM}:inboxCap`, "c"); if (query.includes(`<${SHIM}:inboxAddress>`)) return rows(anchor, `${SHIM}:inboxAddress`, "a"); // Anchored per-doc read (`SELECT ?s ?p ?o`). return { results: { bindings: quads .filter((q) => q.g === anchor) .map((q) => ({ s: { value: q.s }, p: { value: q.p }, o: { value: q.o } })), }, }; }); // Push a `State` on subscribe — the sync barrier `open-repo.ts` waits for. const doc_subscribe = mock(async (_repo: unknown, _sid: unknown, cb: unknown) => { if (typeof cb === "function") (cb as (r: unknown, t?: string) => void)({ V0: { State: {} } }); return () => {}; }); return { doc_create, sparql_query, sparql_update, doc_subscribe, /** Documents minted so far — what "did it mint a second one" reads. */ created: (): number => docCounter, /** Times the (user, scope) → inbox association was read. */ lookups: (): number => inboxLookups, /** The inbox NURIs DURABLY associated with `scope`, across every account subject. */ associated: (scope: InboxScope): string[] => quads.filter((q) => q.p === `${SHIM}:docInbox:${scope}`).map((q) => q.o), /** Every triple written with `pred`, whatever its subject or graph. */ written: (pred: string): string[] => quads.filter((q) => q.p === `${SHIM}:${pred}`).map((q) => q.o), }; } /** Wire a clean world: fresh fake broker, fresh caches, nobody connected. */ function inject(faults: Faults) { const ng = makeFakeNg(faults); configure({ ng: ng as never, useShape: (() => {}) as never }); configureStoreRegistry({ getSession: async (): Promise => { if (faults.session) throw new Error("WalletError: the session cannot answer yet"); return SESSION; }, normalizeId: (id: string) => id.trim().toLowerCase(), }); resetRegistryCache(); resetOpenedRepos(); resetCaps(); setCurrentUser(null); return ng; } afterEach(() => { setCurrentUser(null); resetConfig(); resetStoreRegistry(); resetRegistryCache(); resetOpenedRepos(); resetCaps(); }); /** A later session over the SAME wallet: every in-memory cache gone, the quads kept. */ function freshSession(): void { resetRegistryCache(); resetOpenedRepos(); } // --- resolving without asking: the two states that are already true --------- test("A — an inbox already resolved comes back from the cache, untouched", async () => { const ng = inject(noFaults()); adoptCurrentUser("bob"); const inbox = await userInbox("bob", "public"); const createdBefore = ng.created(); const lookupsBefore = ng.lookups(); const again = await userInbox("bob", "public"); expect(again).toBe(inbox); expect(ng.lookups()).toBe(lookupsBefore); // no read at all expect(ng.created()).toBe(createdBefore); // and nothing minted }); test("B — a concurrent caller joins the resolution in flight instead of racing a second inbox", async () => { // Two independent callers ask at once on a cold cache — the ordinary shape on a fresh // page. Without the join each would see "no inbox" and mint one, forking the (user, // scope) association in-session. const ng = inject(noFaults()); adoptCurrentUser("bob"); await ensureAccount("bob"); const [first, second] = await Promise.all([ userInbox("bob", "public"), userInbox("bob", "public"), ]); expect(second).toBe(first); expect(ng.associated("public")).toEqual([first]); // ONE association, not two }); test("B — a concurrent caller joins the resolution in flight and inherits its FAILURE", async () => { // The join is only safe if what it hands out is the run's real outcome. A joiner that // inherits a resolved promise over a failed run is the sibling defect fixed in // `connect.ts`: a caller that did nothing wrong carries on over work that never happened. const faults = noFaults(); inject(faults); adoptCurrentUser("bob"); await ensureAccount("bob"); faults.inboxPersist = true; // Both handlers attached in the same tick, as two real concurrent callers would: awaiting // one and only THEN the other leaves the second rejection momentarily unobserved, which // the runtime reports as an unhandled rejection rather than as the outcome under test. const outcomes = await Promise.allSettled([ userInbox("bob", "public"), userInbox("bob", "public"), ]); expect(outcomes.map((o) => o.status)).toEqual(["rejected", "rejected"]); }); // --- the collaborators awaited before any decision is taken ----------------- test("C — a session that cannot answer fails the call", async () => { // The consumer's `getSession` is the other edge of the library. A wallet that cannot // answer says nothing about which inbox a user owns, so there is nothing to conclude. const faults = noFaults(); inject(faults); adoptCurrentUser("bob"); faults.session = true; await expect(userInbox("bob", "public")).rejects.toThrow(/the session cannot answer/); }); test("D — a doc-shim that cannot be reached fails the call", async () => { // No pointer yet (a first login), so resolving the doc-shim mints it. A broker that // cannot create it leaves the registry itself unresolved — every account record and every // inbox association lives in that document. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); faults.docCreate = true; await expect(userInbox("bob", "public")).rejects.toThrow(/cannot create document/); expect(ng.created()).toBe(0); }); test("E — an account that cannot be provisioned fails the call", async () => { // A user must exist before it can own an inbox. `ensureAccount` mints its three scope // documents on first sight; a broker that cannot returns no record, and an inbox filed // under a user with no stores is filed under nothing. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("alice"); await ensureAccount("alice"); // the doc-shim now exists, so the fault lands on the account const createdBefore = ng.created(); faults.docCreate = true; await expect(userInbox("bob", "public")).rejects.toThrow(/cannot create document/); expect(ng.associated("public")).toEqual([]); expect(ng.created()).toBe(createdBefore); }); // --- the read: an answer, an absence, or a failure -------------------------- test("F — a lookup that could not answer is not an absent inbox: no second one is minted", async () => { // The shipped defect. The read that asks *which inbox does this user own* was wrapped in a // catch that only logged, and execution fell through to the mint — so a broker that could // not answer produced a SECOND document for one (user, scope). Which of the two later wins // is then decided by `canonicalDoc` picking among the triples: the owner drains one, a // depositor may write to the other. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); const inbox = await userInbox("bob", "public"); freshSession(); const createdBefore = ng.created(); faults.inboxLookup = true; await expect(userInbox("bob", "public")).rejects.toThrow(/RepoNotFound/); // The durable consequence: the wallet still holds exactly ONE inbox for (bob, public), // and no document was minted behind the failure. expect(ng.associated("public")).toEqual([inbox]); expect(ng.created()).toBe(createdBefore); // …so once the broker answers again, it is that same inbox that comes back. faults.inboxLookup = false; freshSession(); expect(await userInbox("bob", "public")).toBe(inbox); }); test("G — an inbox the shim already records is resolved, never re-minted", async () => { const ng = inject(noFaults()); adoptCurrentUser("bob"); const inbox = await userInbox("bob", "public"); freshSession(); // a later session over the same wallet: nothing warm const createdBefore = ng.created(); const resolved = await userInbox("bob", "public"); expect(resolved).toBe(inbox); expect(ng.created()).toBe(createdBefore); expect(ng.associated("public")).toEqual([inbox]); }); test("H — a first ask on a VERIFIED absence mints the inbox, records it, and associates it", async () => { // The normal case, and the only one entitled to write: the broker answered, and it // answered nothing. The three writes that make the answer usable all have to land — the // document, the shim's "this IS an inbox", and the (user, scope) association. const ng = inject(noFaults()); adoptCurrentUser("bob"); const publicInbox = await userInbox("bob", "public"); const protectedInbox = await userInbox("bob", "protected"); expect(protectedInbox).not.toBe(publicInbox); // two store repos upstream, two documents expect(ng.associated("public")).toEqual([publicInbox]); expect(ng.associated("protected")).toEqual([protectedInbox]); // Durably an inbox, not merely one this session happens to remember. freshSession(); expect(await isKnownInbox(publicInbox)).toBe(true); expect(await isKnownInbox(protectedInbox)).toBe(true); }); // --- the mint: three writes, and none of them may fail in silence ----------- test("I — a broker that cannot mint the document fails the call", async () => { const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); await ensureAccount("bob"); // exists, and has never opened an inbox faults.docCreate = true; await expect(userInbox("bob", "public")).rejects.toThrow(/cannot create document/); expect(ng.associated("public")).toEqual([]); }); test("J — an inbox the shim could not record as one is not handed back", async () => { // `recordInbox` is what stands in for the broker's `inboxes: PubKey → RepoId` table: it is // how a depositor learns that a NURI is an inbox at all, and `inbox.post` refuses anything // it cannot confirm. It swallowed its own write failure, so the call went on to associate a // document that no depositor will ever be allowed to write to — the owner's inbox, silently // closed to everyone, forever. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); await ensureAccount("bob"); faults.inboxRecord = true; await expect(userInbox("bob", "public")).rejects.toThrow(/cannot write the inbox index/); expect(ng.associated("public")).toEqual([]); // nothing points at a box deposits bounce off }); test("K — an association that could not be persisted is not returned as an inbox", async () => { // The other shipped defect. The `INSERT DATA` that records WHICH inbox belongs to (user, // scope) was wrapped in a catch that only logged, and the function cached and returned the // document anyway. The owner then holds a reference whose triple was never written: nobody // else resolves it, so a depositor finding nothing mints yet another. The owner reads a box // nobody writes to, depositors write to a box nobody reads. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); await ensureAccount("bob"); faults.inboxPersist = true; await expect(userInbox("bob", "public")).rejects.toThrow(/cannot write the inbox association/); expect(ng.associated("public")).toEqual([]); // …and the reference a healthy resolution DOES hand back is one everybody resolves the // same. Alice obtains Bob's inbox the way `inbox.share` does — by asking the registry // under her own identity — rather than being handed a value across the boundary. faults.inboxPersist = false; freshSession(); const ownersView: Nuri = await userInbox("bob", "protected"); freshSession(); adoptCurrentUser("alice"); const depositorsView: Nuri = await userInbox("bob", "protected"); expect(depositorsView).toBe(ownersView); expect(ng.associated("protected")).toEqual([ownersView]); }); // --- the same shape, swept out of the rest of the registry ------------------- // // `userInbox` was the third member of this family found by accident, so the sweep that // followed the fix looked for every other `catch` that logs and lets execution carry on as // though the thing looked for was absent — especially where what follows creates or writes. // These are the ones whose consequence was a DURABLE false state; each is pinned here, // because a swallow that comes back is invisible again by construction. test("SWEEP — a pointer read that never answered does not mint a second registry root", async () => { // `resolvePointer` retries a bounded number of times and answered `""` when the budget ran // out — the same value it uses for "no pointer yet", which is what makes `resolveShimDoc` // CREATE one. A store-root that could not be read therefore forked the registry itself: a // second doc-shim, a second pointer, and every account record afterwards split between two // documents, the loser's simply invisible. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); await ensureAccount("bob"); const pointersBefore = ng.written("shimDoc"); const createdBefore = ng.created(); freshSession(); faults.pointerRead = true; await expect(ensureAccount("bob")).rejects.toThrow(/RepoNotFound/); expect(ng.written("shimDoc")).toEqual(pointersBefore); // still ONE registry root expect(ng.created()).toBe(createdBefore); // and nothing minted behind the failure }); test("SWEEP — a pointer that could not be written fails the first login", async () => { // The pointer is the only NURI a fresh session can name without a lookup. Carrying on // without it means writing every account into a doc-shim no later session can find — and // the next login, seeing no pointer, mints another one and re-provisions everybody. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); faults.pointerWrite = true; await expect(ensureAccount("bob")).rejects.toThrow(/cannot write the pointer/); expect(ng.written("id")).toEqual([]); // no account was filed into an unreachable shim }); test("SWEEP — an account read that could not answer does not provision a second account", async () => { // `ensureAccount` asked through the TOLERANT resolver, which answers `null` for a read // that failed exactly as for one that found nothing — and what follows an absence here is // a PROVISION. So a broker hiccup minted a second set of three store documents and a // second record: the account fork the doc-shim barrier was introduced to end, walking back // in through the error path. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); const first = await ensureAccount("bob"); const createdBefore = ng.created(); freshSession(); faults.accountLookup = true; await expect(ensureAccount("bob")).rejects.toThrow(/RepoNotFound/); expect(ng.created()).toBe(createdBefore); // no second set of scope documents faults.accountLookup = false; freshSession(); expect(await ensureAccount("bob")).toEqual(first); // and the one account is intact }); test("SWEEP — an account record that could not be persisted is not handed back", async () => { // `ensureAccount` cached and returned the record right after the write. The session then // wrote its entities into three documents the shim never heard of, and the next session, // finding no record, provisioned the account again — everything created in between // orphaned, with no error anywhere. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); await ensureAccount("alice"); // the doc-shim exists, so the fault lands on the record faults.accountRecord = true; await expect(ensureAccount("bob")).rejects.toThrow(/cannot write the account record/); // Nothing remembers a user that was never filed: asking again re-provisions from scratch // rather than handing back the set the first attempt abandoned. faults.accountRecord = false; const createdBefore = ng.created(); const record = await ensureAccount("bob"); expect(ng.created()).toBe(createdBefore + 3); expect(record.docPrivate).not.toBe(""); }); test("SWEEP — a document inbox the owner's branch could not record is never published", async () => { // `openDocumentInbox` writes twice: the pair on the owner's User branch (what makes the // inbox DRAINABLE — `myInboxes` builds the connection's drain list from it) and the // address on the document (what makes it REACHABLE). Swallowing the first went on to // publish the second, inviting depositors into a queue its owner never enumerates: every // message delivered, none ever applied, permanently. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); const note = await createEntityDoc("bob", "protected"); faults.inboxCapPersist = true; await expect(openDocumentInbox(note)).rejects.toThrow(/cannot write the inbox cap/); expect(ng.written("inboxAddress")).toEqual([]); // nobody was told where to deposit expect(await documentInboxAddress(note)).toBeUndefined(); }); test("SWEEP — an address that could not be published fails opening the inbox", async () => { // The other half. Publishing is the only way a third party learns where to deposit here, // and the address is a REPLACEMENT: the `DELETE` lands, the `INSERT` does not, and the // document is left with no address at all while its owner is handed an inbox and told // nothing. const faults = noFaults(); const ng = inject(faults); adoptCurrentUser("bob"); const note = await createEntityDoc("bob", "protected"); faults.addressPublish = true; await expect(openDocumentInbox(note)).rejects.toThrow(/cannot publish the inbox address/); expect(ng.written("inboxAddress")).toEqual([]); });