/** * cold-start-anchor.test.ts — the shim ANCHOR (private-store-root) must be OPENED * before the registry reads/writes it, or a cold anchor throws `RepoNotFound`. * * ── The gap this pins ────────────────────────────────────────────────────── * The shim lives in the private-store-root graph (`did:ng:${privateStoreId}`, the * "anchor"). Unlike a per-entity doc — whose anchored read on an unopened repo * SILENTLY returns 0 rows — the private/store target resolves through the verifier's * `resolve_target_for_sparql`, which HARD-errors `RepoNotFound` when the repo is not * in `self.repos` (verified in nextgraph-rs `request_processor.rs`). On a wallet whose * anchor repo is not yet loaded, both the shim READ (`resolveAccount`/`loadShim`) and * the provision WRITE (`ensureAccount`) throw — so the account never provisions. * * The heal: `resolveAccount`/`loadShim`/`ensureAccount` call `ensureRepoOpen(anchor)` * (open-repo.ts, via `doc_subscribe` + first-`State` barrier) before touching the * shim — the same open-before-read guard `readScopeIndex` already applies to its * index doc. This suite models a fake `ng` where the anchor throws `RepoNotFound` * UNTIL it has been `doc_subscribe`-d, and asserts the registry provisions cleanly. * * RED without the heal (ensureAccount would throw on the cold anchor); GREEN with it. */ import { describe, it, expect, mock, afterAll, beforeEach } from "bun:test"; import { ensureAccount, resolveWriteGraph, resetRegistryCache } from "../src/store-registry"; import { resetOpenedRepos } from "../src/open-repo"; import { configure, configureStoreRegistry, resetStoreRegistry, resetConfig, } from "../src/polyfill"; const SESSION = { sessionId: "sid-cold", privateStoreId: "PRIV-COLD" }; const ANCHOR = `did:ng:${SESSION.privateStoreId}`; afterAll(() => { resetConfig(); resetStoreRegistry(); resetRegistryCache(); resetOpenedRepos(); }); beforeEach(() => { resetRegistryCache(); resetOpenedRepos(); }); 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; } /** * A fake `ng` whose ANCHOR repo behaves like the real private-store target: * `sparql_query`/`sparql_update` anchored to it THROW `RepoNotFound` until the * anchor has been `doc_subscribe`-d (i.e. opened into `self.repos`). Any OTHER * anchor (per-entity docs) behaves normally. `doc_subscribe` fires the first * `State` so `ensureRepoOpen` crosses the barrier. */ function makeColdAnchorNg() { const quads: Quad[] = []; let docCounter = 0; const opened = new Set(); let anchorSubscribes = 0; const doc_create = mock(async () => `did:ng:o:doc${++docCounter}`); const doc_subscribe = mock(async (nuri: string, _sid: string, cb: (r: unknown) => void) => { if (nuri === ANCHOR) anchorSubscribes += 1; opened.add(nuri); setTimeout(() => cb({ V0: { State: {} } }), 0); return () => {}; }); const sparql_update = mock(async (_sid: string, query: string, anchor?: string) => { if (anchor === ANCHOR && !opened.has(ANCHOR)) throw new Error("RepoNotFound"); // TWO shapes: the POINTER write uses `GRAPH ` (keyed by IRI); the account // record write into the doc-shim has NO explicit GRAPH (keyed by the anchor arg). 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 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] ?? "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 (_sid: string, query: string, _base: unknown, anchor?: string) => { if (anchor === ANCHOR && !opened.has(ANCHOR)) throw new Error("RepoNotFound"); // Pointer SELECT (store-root -> doc-shim). if (query.includes("")) { const bindings = quads .filter((q) => q.g === anchor && q.p === "urn:ng-eventually:shim:shimDoc") .map((q) => ({ shimDoc: { value: q.o } })); return { results: { bindings } }; } const subjM = query.match( /<([^>]+)>\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 } }; }); return { doc_create, doc_subscribe, sparql_update, sparql_query, _quads: quads, anchorSubscribeCount: () => anchorSubscribes, }; } function inject(ng: ReturnType) { configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (u: string) => u.trim().replace(/^@+/, "").toLowerCase(), }); resetRegistryCache(); resetOpenedRepos(); } describe("cold-start anchor heal", () => { it("ensureAccount provisions over a COLD anchor (RepoNotFound-until-opened) without throwing", async () => { const ng = makeColdAnchorNg(); inject(ng); // Without the open-before-shim heal, the read AND the provision write would both // throw RepoNotFound on the cold anchor and the account would never persist. const rec = await ensureAccount("@cold-alice"); expect(rec.docPublic).toBeTruthy(); expect(rec.docProtected).toBeTruthy(); expect(rec.docPrivate).toBeTruthy(); // The anchor repo was actually opened (doc_subscribe-d) before the shim op. expect(ng.anchorSubscribeCount()).toBeGreaterThan(0); }); it("the provisioned account re-resolves from the shim (real persistence, no RepoNotFound)", async () => { const ng = makeColdAnchorNg(); inject(ng); const first = await ensureAccount("@cold-bob"); // Fresh cache → a real anchored re-read of the shim (anchor already opened → OK). resetRegistryCache(); const again = await ensureAccount("@cold-bob"); expect(again.docPublic).toBe(first.docPublic); expect(again.docProtected).toBe(first.docProtected); expect(again.docPrivate).toBe(first.docPrivate); }); it("resolveWriteGraph (scope resolver) works over a cold anchor", async () => { const ng = makeColdAnchorNg(); inject(ng); const g = await resolveWriteGraph("@cold-carol", "protected"); expect(g).toBeTruthy(); }); });