/** * ensureAccount-idempotent.test.ts — behavioral tests for the NO-FORK guarantee * of ensureAccount (src/store-registry.ts). * * The guarantee: ensureAccount always resolves via a SIMPLE resolveAccount read * (one targeted SPARQL query, no barrier, no retry). The private store is * synchronised at login, so 0 rows is DEFINITIVE (account genuinely absent) → * provision exactly once; rows present → reuse (NO-FORK). Idempotent within * and across sessions. * * With the unit fake `ng` (no `doc_subscribe`), `ensureRepoOpen` is a no-op * (getSyncState → "unknown") and the single shim read is immediate — * synchronous behaviour preserved, no lag to wait out. */ import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"; import { ensureAccount, resetRegistryCache, } from "../src/store-registry"; import type { RegistrySession } from "../src/store-registry"; import { configure, configureStoreRegistry, resetStoreRegistry, resetConfig, } from "../src/polyfill"; import { resetOpenedRepos } from "../src/open-repo"; afterAll(() => { resetConfig(); resetStoreRegistry(); resetRegistryCache(); resetOpenedRepos(); }); const SESSION: RegistrySession = { sessionId: "sid-af", privateStoreId: "PRIV-AF" }; // --------------------------------------------------------------------------- // Fake ng — simple in-memory store (no doc_subscribe → fake-ng no-op path) // // Queries return data from the `quads` array immediately. No lag simulation: // the single shim read is already authoritative (private store is synced at login). // --------------------------------------------------------------------------- interface Quad { g: string; s: string; p: string; o: string } 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; } function makeFakeNg() { const quads: Quad[] = []; let docCounter = 0; let accountQueryCount = 0; const doc_create = mock(async (..._a: unknown[]) => `did:ng:o:doc${++docCounter}`); const sparql_update = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[2] as string | undefined; const gm = query.match(/GRAPH <([^>]+)>\s*\{([\s\S]*)\}/); let g: string; let body: string; if (gm) { g = gm[1]!; body = gm[2]!; } else { if (!anchor) return undefined; g = anchor; body = query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, ""); } const sm = body.match(/<([^>]+)>/); if (!sm) return undefined; const s = 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) { const p = m[1] ?? "urn:ng-eventually:shim:Account"; const o = m[2] !== undefined ? unescapeLiteral(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 (resolveAccount / loadShim) accountQueryCount++; const subjM = query.match(/GRAPH <[^>]+>\s*\{\s*<([^>]+)>\s+a\s+/); const onlySubject = subjM ? subjM[1]! : null; const bySubject = new Map>(); for (const q of quads) { if (q.g !== anchor) continue; if (onlySubject !== null && q.s !== onlySubject) continue; const rec = bySubject.get(q.s) ?? {}; if (q.p === "urn:ng-eventually:shim:id") rec.id = 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.id) .map((r) => ({ id: { value: r.id! }, docPublic: { value: r.docPublic ?? "" }, docProtected: { value: r.docProtected ?? "" }, docPrivate: { value: r.docPrivate ?? "" }, })); return { results: { bindings } }; } // Entity-index SELECT 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, getAccountQueryCount: () => accountQueryCount, }; } function inject(fakeNg: ReturnType) { // No doc_subscribe → ensureRepoOpen is a no-op (fake-ng path). configure({ ng: fakeNg as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (u) => u.trim().toLowerCase(), }); resetRegistryCache(); resetOpenedRepos(); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("ensureAccount: no-fork + idempotence", () => { beforeEach(() => { resetRegistryCache(); resetOpenedRepos(); }); it("(a) NO-FORK: account already in shim → reused, 0 new doc_create", async () => { // Provision an account in an initial "session" (quads are written). const fakeNg = makeFakeNg(); inject(fakeNg); const first = await ensureAccount("LauraBarrier"); expect(fakeNg.doc_create).toHaveBeenCalledTimes(3); // Simulate a fresh session: clear caches but keep quads intact (same fakeNg). // The single resolveAccount read finds the account immediately. resetRegistryCache(); resetOpenedRepos(); const second = await ensureAccount("LauraBarrier"); // ANTI-FORK: same scope docs returned, no new provisioning expect(second.docPublic).toBe(first.docPublic); expect(second.docProtected).toBe(first.docProtected); expect(second.docPrivate).toBe(first.docPrivate); expect(fakeNg.doc_create).toHaveBeenCalledTimes(3); // still exactly 3, not 6 }); it("(b) genuinely new account (fake returns 0 rows) → provisioned exactly once (3 doc_create)", async () => { const fakeNg = makeFakeNg(); // empty quads → 0 rows on any account query inject(fakeNg); const rec = await ensureAccount("BrandNewUser"); // Exactly 3 doc_create calls (1 set of scope docs, no fork) expect(fakeNg.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); // Single read — no retry loop expect(fakeNg.getAccountQueryCount()).toBe(1); }); it("(c) idempotence within a session: calling ensureAccount twice never creates 2 sets", async () => { const fakeNg = makeFakeNg(); inject(fakeNg); const a = await ensureAccount("SameUser"); const b = await ensureAccount("SameUser"); expect(b).toEqual(a); // Must still be exactly 3 (not 6) expect(fakeNg.doc_create).toHaveBeenCalledTimes(3); }); it("(d) fake-ng no-op path: single shim read per call, no doc_subscribe calls", async () => { // The fake ng has no doc_subscribe → ensureRepoOpen is a no-op (getSyncState → "unknown"). // ensureAccount must proceed to the single resolveAccount read without waiting or throwing. const fakeNg = makeFakeNg(); // no doc_subscribe inject(fakeNg); // Provision once, then verify a second resolve (fresh cache) finds it immediately. const first = await ensureAccount("FakeNgUser"); resetRegistryCache(); resetOpenedRepos(); const second = await ensureAccount("FakeNgUser"); // Same docs reused (no fork), exactly 3 total doc_create across both calls expect(second.docPublic).toBe(first.docPublic); expect(fakeNg.doc_create).toHaveBeenCalledTimes(3); // The account query was called exactly twice (once per ensureAccount, no retries) expect(fakeNg.getAccountQueryCount()).toBe(2); }); });