/**
* 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([]);
});