/** * open-repo.test.ts — behavioral tests for ensureRepoOpen / ensureReposOpen * (src/open-repo.ts). * * Core invariant: on a fresh session over a persistent wallet, a scope-index * or entity repo is NOT yet in `self.repos`, so an anchored sparql_query returns * 0 rows. `ensureRepoOpen(nuri)` calls `doc_subscribe(nuri, …)` FIRST (which * pushes the repo into the session), then the anchored read returns data. * * Fake design: * - sparql_query returns EMPTY for a nuri UNTIL doc_subscribe has been called * for that nuri (tracked in a Set). * - doc_subscribe is a mock that records calls, fires the callback once * (simulating the initial State push), then returns an unsubscribe fn. * * We test ensureRepoOpen via readUnion (from read-model) because that is the * production caller — it gates on ensureReposOpen internally. */ import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"; import { ensureRepoOpen, ensureReposOpen, resetOpenedRepos } from "../src/open-repo"; import { readUnion } from "../src/read-model"; import { configure, configureStoreRegistry, resetStoreRegistry, resetConfig } from "../src/polyfill"; import { resetRegistryCache } from "../src/store-registry"; afterAll(() => { resetConfig(); resetStoreRegistry(); resetRegistryCache(); resetOpenedRepos(); }); beforeEach(() => { resetOpenedRepos(); resetRegistryCache(); }); const SESSION = { sessionId: "sid-or", privateStoreId: "PRIV-OR" }; const TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; const FP = "http://festipod.org/"; // --------------------------------------------------------------------------- // Fake ng builder: tracks which nuris have been doc_subscribe-d. // sparql_query returns rows only AFTER the corresponding nuri is subscribed. // --------------------------------------------------------------------------- function makeFakeNgWithSubscribe( triplesByDoc: Record>, ) { const subscribed = new Set(); const subscribeCallOrder: string[] = []; // doc_subscribe: record the call, fire callback immediately (initial push), return unsub const doc_subscribe = mock(async (nuri: string, _sid: string, cb: (r: unknown) => void) => { subscribed.add(nuri); subscribeCallOrder.push(nuri); // Simulate initial State push (synchronously deferred so the subscription // setup promise path in ensureRepoOpen can resolve it). setTimeout(() => cb({ V0: { State: {} } }), 0); return () => {}; // unsubscribe fn }); const sparql_query = mock(async (_sid: string, _query: string, _base: unknown, anchor: unknown) => { const doc = anchor as string | undefined; if (!doc) return { results: { bindings: [] } }; // Only return data if the repo has been subscribed (i.e. opened) if (!subscribed.has(doc)) return { results: { bindings: [] } }; const triples = triplesByDoc[doc]; if (!triples) return { results: { bindings: [] } }; const bindings = triples.map(([p, o]) => ({ s: { value: doc }, p: { value: p }, o: { value: o }, })); return { results: { bindings } }; }); const doc_create = mock(async () => "did:ng:o:new"); const sparql_update = mock(async () => undefined); return { doc_subscribe, sparql_query, doc_create, sparql_update, subscribed, subscribeCallOrder }; } function inject(ng: ReturnType) { configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (u: string) => u, provisionRetry: { attempts: 1 }, }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("ensureRepoOpen", () => { it("calls doc_subscribe BEFORE the anchored read returns data", async () => { const ng = makeFakeNgWithSubscribe({ "did:ng:o:a": [[TYPE, `${FP}Event`], [`${FP}title`, "Alpha"]], }); inject(ng); // Directly call ensureRepoOpen then verify read sees data await ensureRepoOpen("did:ng:o:a"); // doc_subscribe was called for the nuri expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); expect(ng.subscribeCallOrder[0]).toBe("did:ng:o:a"); // sparql_query was called AFTER subscribe (ensureRepoOpen guarantees ordering) const result = await readUnion(["did:ng:o:a"]); expect(result.length).toBe(1); expect(result[0]!.props[`${FP}title`]).toEqual(["Alpha"]); }); it("WITHOUT doc_subscribe, sparql_query returns 0 rows (verifies fake mechanics)", async () => { const ng = makeFakeNgWithSubscribe({ "did:ng:o:a": [[TYPE, `${FP}Event`], [`${FP}title`, "Alpha"]], }); inject(ng); // Do NOT call ensureRepoOpen — subscribed Set remains empty // Query directly (bypass readUnion which calls ensureReposOpen internally) const result = await ng.sparql_query("sid-or", "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, "did:ng:o:a"); const bindings = (result as any).results.bindings; expect(bindings.length).toBe(0); // not subscribed → 0 rows (confirms fake design) }); it("idempotence: a 2nd ensureRepoOpen for the same nuri does NOT re-subscribe", async () => { const ng = makeFakeNgWithSubscribe({ "did:ng:o:b": [[TYPE, `${FP}Event`]], }); inject(ng); await ensureRepoOpen("did:ng:o:b"); await ensureRepoOpen("did:ng:o:b"); // second call // doc_subscribe must have been called exactly ONCE expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); }); it("no-op when the fake ng has no doc_subscribe (unit fake path)", async () => { // Fake ng WITHOUT doc_subscribe const noSubscribeNg = { doc_create: mock(async () => "did:ng:o:new"), sparql_update: mock(async () => undefined), sparql_query: mock(async () => ({ results: { bindings: [] } })), }; configure({ ng: noSubscribeNg as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (u: string) => u, provisionRetry: { attempts: 1 }, }); // Must not throw; nuri is added to opened Set (guard skips subscribe) await expect(ensureRepoOpen("did:ng:o:c")).resolves.toBeUndefined(); // Calling again should also be a no-op (idempotent, already in opened) await expect(ensureRepoOpen("did:ng:o:c")).resolves.toBeUndefined(); }); }); describe("ensureReposOpen", () => { it("opens all provided nuris in parallel (one subscribe per unique nuri)", async () => { const ng = makeFakeNgWithSubscribe({ "did:ng:o:x": [[TYPE, `${FP}Event`]], "did:ng:o:y": [[TYPE, `${FP}Event`]], }); inject(ng); await ensureReposOpen(["did:ng:o:x", "did:ng:o:y"]); expect(ng.doc_subscribe).toHaveBeenCalledTimes(2); expect(ng.subscribed.has("did:ng:o:x")).toBe(true); expect(ng.subscribed.has("did:ng:o:y")).toBe(true); }); it("deduplicates: repeated nuri in input leads to exactly one subscribe", async () => { const ng = makeFakeNgWithSubscribe({ "did:ng:o:dup": [[TYPE, `${FP}Event`]], }); inject(ng); await ensureReposOpen(["did:ng:o:dup", "did:ng:o:dup", "did:ng:o:dup"]); expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); }); it("empty or all-falsy input is a no-op (no subscribe calls)", async () => { const ng = makeFakeNgWithSubscribe({}); inject(ng); await ensureReposOpen([]); await ensureReposOpen(["" as any]); expect(ng.doc_subscribe).toHaveBeenCalledTimes(0); }); it("readUnion triggers doc_subscribe then returns data (integration path)", async () => { const ng = makeFakeNgWithSubscribe({ "did:ng:o:p": [[TYPE, `${FP}Participation`], [`${FP}event`, "did:ng:o:e"]], }); inject(ng); const subjects = await readUnion(["did:ng:o:p"]); // doc_subscribe was called as part of ensureReposOpen inside readUnion expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); expect(subjects.length).toBe(1); expect(subjects[0]!.props[`${FP}event`]).toEqual(["did:ng:o:e"]); }); });