/** * The app-facing surface, pinned where it changed shape on 2026-08-10. * * Every test here exists because behaviour shipped without one and an adversarial pass * had to find it: an application that cannot learn its own identity, a share that invents * its recipient, a creation that reports success on a half-written document. They are * about what a CALLER sees, not about the emulation's internals. */ import { test, expect, mock, afterEach } from "bun:test"; import { configure, ensureIdentity, storeRegistry } from "../src/index"; import { configureStoreRegistry, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, } from "../src/shared-wallet/bootstrap"; import { createEntityDoc as registryCreateEntityDoc, resetRegistryCache } from "../src/shared-wallet/account-registry"; import { share } from "../src/surface/inbox"; const SHIM = "urn:ng-eventually:shim"; const SESSION = { sessionId: "sid-app", privateStoreId: "PRIV-APP" }; interface Quad { g: string; s: string; p: string; o: string } /** A stateful fake `ng`, with a switch that makes a chosen register write fail. */ function inject(failWriteMatching?: RegExp) { const quads: Quad[] = []; let created = 0; const doc_create = mock(async () => `did:ng:o:doc${++created}`); const sparql_update = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[2] as string | undefined; if (failWriteMatching && failWriteMatching.test(query)) throw new Error("broker refused"); if (!anchor) return undefined; const gm = query.match(/GRAPH\s+<([^>]+)>\s*\{([\s\S]*)\}/); const body = gm ? gm[2]! : query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, ""); const sm = body.match(/<([^>]+)>/); if (!sm) return undefined; const subj = sm[1]!; const pairRe = /(?:a|<([^>]+)>)\s+(?:"((?:[^"\\]|\\.)*)"|<([^>]+)>)/g; let m: RegExpExecArray | null; const after = body.slice(body.indexOf(sm[0]) + sm[0].length); while ((m = pairRe.exec(after)) !== null) { quads.push({ g: anchor, s: subj, p: m[1] ?? `${SHIM}:Account`, o: m[2] ?? m[3] ?? "" }); } return undefined; }); const sparql_query = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[3] as string | undefined; const byPred = (pred: string, v: string) => ({ results: { bindings: quads.filter((q) => q.g === anchor && q.p === pred).map((q) => ({ [v]: { value: q.o } })) }, }); if (query.includes(`${SHIM}:shimDoc`)) return byPred(`${SHIM}:shimDoc`, "shimDoc"); if (query.includes(`${SHIM}:readCap`)) return byPred(`${SHIM}:readCap`, "c"); if (query.includes(`${SHIM}:contains`)) return byPred(`${SHIM}:contains`, "e"); if (query.includes(`${SHIM}:id`)) { // Filter by SUBJECT when the query names one — the account read asks about ONE // account. A fake that ignores it hands back somebody else's record, and then // "bob does not see alice's document" and "share refuses an unknown name" both // fail for a reason that has nothing to do with the code. (Made that mistake here // first; it is the same one this review found in the other fakes.) 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 ?? "" }, })), }, }; } return { results: { bindings: [] } }; }); configure({ ng: { doc_create, sparql_update, sparql_query } as never, useShape: (() => {}) as never, }); // The session comes from `init()` upstream and from the package's capture here, so a // suite with no browser and no broker substitutes one through the internal wiring path — // the same one the e2e harness uses. AFTER `configure`, which wires the package's own. configureStoreRegistry({ getSession: async () => SESSION }); resetRegistryCache(); resetCaps(); setCurrentUser(null); return { quads }; } afterEach(() => { resetConfig(); resetStoreRegistry(); resetCaps(); setCurrentUser(null); }); // ── identity ─────────────────────────────────────────────────────────────── // An application has to know which user it is — to display it, at least. Upstream it // does: it passes `user_id` to `session_start`, having got it from the wallet it opened. // Here the gate chooses, so the gate returns. Without this the example application read // the gate's own private storage key, which is a boundary no consumer should see. test("ensureIdentity returns the identity it settled", async () => { inject(); setCurrentUser("alice"); expect(await ensureIdentity()).toBe("alice"); }); // The other half of the same decision: no placement call TAKES an identity, because a // session belongs to one user and the target's `doc_create` carries none. Calling one // before signing in is a caller error worth naming, not an empty result. test("a placement call before signing in names the mistake", async () => { inject(); setCurrentUser(null); await expect(storeRegistry.createEntityDoc("protected")).rejects.toThrow(/ensureIdentity/i); await expect(storeRegistry.listMyEntityDocs("protected")).rejects.toThrow(/no identity/i); }); test("placement acts as the connected user, with nothing passed", async () => { inject(); setCurrentUser("alice"); const doc = await storeRegistry.createEntityDoc("protected"); expect(await storeRegistry.listMyEntityDocs("protected")).toContain(doc); setCurrentUser("bob"); expect(await storeRegistry.listMyEntityDocs("protected")).not.toContain(doc); }); // ── sharing names someone who exists ─────────────────────────────────────── test("share refuses a recipient nobody has signed in as, instead of creating them", async () => { inject(); setCurrentUser("alice"); const doc = await storeRegistry.createEntityDoc("protected"); // "bpb" is a typo for "bob". It used to mint that name's three stores and an inbox, // and the cap landed where nobody will ever look — with no error at all. await expect(share(doc, "bpb")).rejects.toThrow(/no such recipient/i); }); // …and the refusal must rest on ABSENCE, never on ignorance: `resolveAccount` answers // `null` for a failed read as well as for a missing one, so a refusal built on it would // tell a user "nobody has signed in as bob" because a query timed out. test("a failed lookup surfaces as a failure, not as 'no such recipient'", async () => { const { quads } = inject(); setCurrentUser("alice"); const doc = await storeRegistry.createEntityDoc("protected"); setCurrentUser("bob"); await registryCreateEntityDoc("bob", "private"); // bob genuinely exists setCurrentUser("alice"); resetRegistryCache(); // force a read rather than the cache const { ng } = (await import("../src/shared-wallet/bootstrap")).getConfig(); const realQuery = ng.sparql_query; (ng as { sparql_query: unknown }).sparql_query = async () => { throw new Error("broker unreachable"); }; await expect(share(doc, "bob")).rejects.toThrow(/broker unreachable/i); (ng as { sparql_query: unknown }).sparql_query = realQuery; void quads; }); // ── a creation that half-worked is a failure, and says which half ────────── test("createEntityDoc reports a half-written document instead of returning its reference", async () => { inject(/shim:contains/); // the LISTING write fails setCurrentUser("alice"); await expect(storeRegistry.createEntityDoc("protected")).rejects.toThrow(/not listed in its store/i); }); // Both writes are attempted before the failure is raised — the cap must land even when // the listing did not, because either is worth having without the other. Throwing on the // first one (2026-08-07) skipped the second and orphaned the document entirely. test("a failed listing does not cost the document its key", async () => { const { quads } = inject(/shim:contains/); setCurrentUser("alice"); await storeRegistry.createEntityDoc("protected").catch(() => {}); expect(quads.some((q) => q.p === `${SHIM}:readCap`)).toBe(true); }); test("a failed key write is reported too, and names that half", async () => { inject(/shim:readCap/); setCurrentUser("alice"); await expect(storeRegistry.createEntityDoc("protected")).rejects.toThrow(/key is not recorded/i); }); // The third write a PUBLIC creation makes, and the one that was still silent: exposing the // cap on the document is what lets anyone else read it (`public-store.exposeReadCap`, the // emulated `expose_outer`), it happens once at creation and nothing ever redoes it. A // swallowed failure therefore left a document sitting in a public store serving nothing — // unreadable by everyone but its creator, permanently, with a reference handed back as // though it had worked. test("a public document whose cap could not be exposed is reported, not returned", async () => { inject(/shim:exposedReadCap/); setCurrentUser("alice"); await expect(storeRegistry.createEntityDoc("public")).rejects.toThrow(/broker refused/i); });