import { test, expect, mock, beforeEach, afterAll } from "bun:test"; import { ensureAccount, allAccounts, loadShim, resolveWriteGraph, resolveReadGraphs, createEntityDoc, listEntityDocs, resetRegistryCache, } from "../src/store-registry"; import type { RegistrySession } from "../src/store-registry"; import { configure, configureStoreRegistry, resetStoreRegistry, resetConfig, } from "../src/polyfill"; // This suite injects a fake `ng` via configure(); bun runs test files in a // shared process with a single module singleton, and may run this file BEFORE // docs.test.ts's order-dependent "not configured" guard. Restore the un- // configured state when we're done so that guard still sees a null config. afterAll(() => { resetConfig(); resetStoreRegistry(); }); // NOTE ORDER: the "not configured → throw" case MUST run first — configure*() // sets module-level singletons and this suite never fully un-injects the real // `ng` (docs' getConfig has no reset), so we exercise the registry-deps guard. test("throws a clear error when configureStoreRegistry() was not called", async () => { resetStoreRegistry(); resetRegistryCache(); await expect(ensureAccount("alice")).rejects.toThrow( /configureStoreRegistry\(\) must be called before use/, ); }); // --- A stateful fake `ng` that emulates just enough SPARQL over an in-memory // quad store: INSERT DATA parsing + the two SELECT shapes the registry issues. interface Quad { g: string; s: string; p: string; o: string } function makeFakeNg() { const quads: Quad[] = []; let docCounter = 0; const doc_create = mock(async (..._a: unknown[]) => `did:ng:o:doc${++docCounter}`); // Parses `INSERT DATA { GRAPH {

"o"//;-lists } }`. const sparql_update = mock(async (...a: unknown[]) => { const query = a[1] as string; const gm = query.match(/GRAPH <([^>]+)>\s*\{([\s\S]*)\}/); if (!gm) return undefined; const g = gm[1]!; const body = gm[2]!; // Subject is the first <...> token in the body. const sm = body.match(/<([^>]+)>/); if (!sm) return undefined; const s = sm[1]!; // Predicate/object pairs: `

"o"` or `

` (a == rdf:type ignored form // handled as literal-free; here the registry only uses

"literal" and //

and `a `). const pairRe = /(?:a|<([^>]+)>)\s+(?:"([^"]*)"|<([^>]+)>)/g; let m: RegExpExecArray | null; // Skip the subject token so we don't treat it as a predicate. const after = body.slice(body.indexOf(sm[0]) + sm[0].length); while ((m = pairRe.exec(after)) !== null) { const p = m[1] ?? "urn:ng-eventually:shim:Account"; // `a` → rdf:type-ish const o = m[2] ?? m[3] ?? ""; quads.push({ g, s, p, o }); } return undefined; }); const sparql_query = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[3] as string | undefined; if (query.includes("")) { // Account SELECT, scoped to the anchor graph. const bySubject = new Map>(); for (const q of quads) { if (q.g !== anchor) continue; const rec = bySubject.get(q.s) ?? {}; if (q.p === "urn:ng-eventually:shim:username") rec.username = q.o; if (q.p === "urn:ng-eventually:shim:docPublic") rec.docPublic = q.o; if (q.p === "urn:ng-eventually:shim:docProtected") rec.docProtected = q.o; if (q.p === "urn:ng-eventually:shim:docPrivate") rec.docPrivate = q.o; bySubject.set(q.s, rec); } const bindings = [...bySubject.values()] .filter((r) => r.username) .map((r) => ({ username: { value: r.username! }, docPublic: { value: r.docPublic ?? "" }, docProtected: { value: r.docProtected ?? "" }, docPrivate: { value: r.docPrivate ?? "" }, })); return { results: { bindings } }; } // Entity-index SELECT: ` ?e` in the anchor graph. const bindings = quads .filter((q) => q.g === anchor && q.p === "urn:ng-eventually:shim:contains") .map((q) => ({ e: { value: q.o } })); return { results: { bindings } }; }); return { doc_create, sparql_update, sparql_query, _quads: quads }; } const SESSION: RegistrySession = { sessionId: "sid-1", privateStoreId: "PRIV" }; function inject() { const ng = makeFakeNg(); configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeUser: (u) => u.trim().replace(/^@+/, "").toLowerCase(), }); resetRegistryCache(); return ng; } let fake: ReturnType; beforeEach(() => { fake = inject(); }); test("ensureAccount creates 3 scope docs and persists them to the shim", async () => { const rec = await ensureAccount("Alice"); expect(rec.username).toBe("Alice"); expect(fake.doc_create).toHaveBeenCalledTimes(3); expect(rec.docPublic).toMatch(/^did:ng:o:doc/); expect(rec.docProtected).not.toBe(rec.docPublic); expect(rec.docPrivate).not.toBe(rec.docProtected); // Persisted into the shim anchor graph (did:ng:PRIV). expect(fake.sparql_update.mock.calls[0]![2]).toBe("did:ng:PRIV"); }); test("ensureAccount is idempotent (case/@-insensitive key), no extra docs", async () => { const a = await ensureAccount("Alice"); const b = await ensureAccount("@alice"); expect(b).toEqual(a); expect(fake.doc_create).toHaveBeenCalledTimes(3); // not 6 }); test("loadShim round-trips a persisted account across a cache reset", async () => { await ensureAccount("Bob"); resetRegistryCache(); // force a re-read from the fake store const map = await loadShim(); const rec = map.get("bob"); expect(rec?.username).toBe("Bob"); expect(rec?.docPublic).toMatch(/^did:ng:o:doc/); }); test("resolveWriteGraph returns the per-scope index doc; resolveReadGraphs fans out", async () => { const rec = await ensureAccount("Carol"); expect(await resolveWriteGraph("carol", "protected")).toBe(rec.docProtected); expect(await resolveReadGraphs("public")).toEqual([rec.docPublic]); }); test("createEntityDoc + listEntityDocs round-trip via the per-scope index", async () => { const rec = await ensureAccount("Dave"); const e1 = await createEntityDoc("dave", "public"); const e2 = await createEntityDoc("dave", "public"); const other = await createEntityDoc("dave", "protected"); // Public listing unions dave's public entities only. const pub = await listEntityDocs("public"); expect(pub.sort()).toEqual([e1, e2].sort()); const prot = await listEntityDocs("protected"); expect(prot).toEqual([other]); // The index append targets the account's public index doc. expect(rec.docPublic).toMatch(/^did:ng:o:doc/); }); test("listEntityDocs fans out across multiple accounts", async () => { await ensureAccount("Eve"); await ensureAccount("Frank"); const e = await createEntityDoc("eve", "public"); const f = await createEntityDoc("frank", "public"); expect((await listEntityDocs("public")).sort()).toEqual([e, f].sort()); }); test("allAccounts reflects every ensured account", async () => { await ensureAccount("Gina"); await ensureAccount("Hank"); const names = (await allAccounts()).map((a) => a.username).sort(); expect(names).toEqual(["Gina", "Hank"]); }); test("normalizeUser defaults to trim when not provided", async () => { const ng = makeFakeNg(); configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION }); resetRegistryCache(); const a = await ensureAccount(" Ivy "); const b = await ensureAccount("Ivy"); // trimmed key matches expect(b).toEqual(a); expect(ng.doc_create).toHaveBeenCalledTimes(3); });