/** * anti-fork.test.ts — behavioral tests for the ANTI-FORK guard in ensureAccount / * resolveAccountReliably (src/store-registry.ts). * * The guard is: BEFORE reading the shim, open the private-store repo and await * the first `State` push (the deterministic sync barrier, CONTRACT 3). After the * barrier, 0 rows is DEFINITIVE (account genuinely absent) → provision exactly * once; rows present → reuse (NO-FORK). No retry loop, no polling. * * With the unit fake `ng` (no `doc_subscribe`), `ensureRepoOpen` is a no-op * (getSyncState → "unknown") and the single shim read is immediate — no lag to * wait out, synchronous behaviour preserved. * * Timed-out barrier: if the barrier expires without a `State`, we refuse to * provision (throwing a clear error) rather than risk a fork. */ 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, _forceOpenedSyncState } 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 barrier mechanism (ensureRepoOpen) is a no-op in the fake-ng path, so // the single shim read after "the barrier" is already authoritative. // --------------------------------------------------------------------------- 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(), // provisionRetry is now unused by the barrier mechanism; kept for API compat. provisionRetry: { attempts: 1 }, }); resetRegistryCache(); resetOpenedRepos(); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("anti-fork: barrier-first resolveAccountReliably / ensureAccount", () => { 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). // In the barrier model the single post-barrier 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 barrier path: single shim read, no doc_subscribe calls", async () => { // The fake ng has no doc_subscribe → ensureRepoOpen is a no-op (getSyncState → "unknown"). // resolveAccountReliably must proceed to the single 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); }); it("(e) timed-out barrier: resolveAccountReliably throws rather than provisioning", async () => { // Simulate: the private store nuri is already in the `opened` set but with // sync state "timed-out" (forced via _forceOpenedSyncState so the test // does not have to wait 8s for the real OPEN_TIMEOUT_MS to fire). const fakeNg = makeFakeNg(); // empty quads → 0 rows inject(fakeNg); // Force the private-store nuri ("did:ng:PRIV-AF") into timed-out state. // resolveAccountReliably calls anchorNuri() → `did:ng:${privateStoreId}`. _forceOpenedSyncState("did:ng:PRIV-AF", "timed-out"); // ensureAccount must throw (conservative anti-fork guard: do not provision blind) await expect(ensureAccount("TimedOutUser")).rejects.toThrow( /sync barrier timed out/, ); // Must NOT have created any scope docs (refusing to provision) expect(fakeNg.doc_create).toHaveBeenCalledTimes(0); }); });