import { test, expect, mock } from "bun:test"; import { readUnion } from "../src/read-model"; import { configure, configureStoreRegistry } from "../src/polyfill"; // A fake `ng` whose sparql_query answers the ANCHORLESS union query with the // triples of the requested subjects, and the anchored ASK (open step) with an // empty result. Each entity subject IRI IS its own document NURI (writeEntity // convention), so the fixture keys triples by the doc NURI. function fakeNgWith(triplesByDoc: Record>) { return { doc_create: mock(async () => "did:ng:o:new"), sparql_update: mock(async () => undefined), sparql_query: mock(async (_sid: string, query: string, _base: unknown, anchor: unknown) => { // The open step is `ASK { ?s ?p ?o }` with an anchor → return a truthy ASK. if (query.startsWith("ASK")) return { boolean: true }; // The union query is anchorless (anchor undefined) with a VALUES ?s block. if (anchor !== undefined) return { results: { bindings: [] } }; const bindings: Array> = []; for (const [doc, triples] of Object.entries(triplesByDoc)) { // Only surface docs whose NURI is named in the VALUES block. if (!query.includes(`<${doc}>`)) continue; for (const [p, o] of triples) { bindings.push({ g: { value: `${doc}:graph` }, s: { value: doc }, p: { value: p }, o: { value: o }, }); } } return { results: { bindings } }; }), }; } function inject(triplesByDoc: Record>) { const ng = fakeNgWith(triplesByDoc); configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => ({ sessionId: "sid-rm", privateStoreId: "priv" }), normalizeUser: (u: string) => u, }); return ng; } const TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; const FP = "http://festipod.org/"; test("readUnion opens each doc then runs ONE anchorless union query", async () => { const ng = inject({ "did:ng:o:a": [[TYPE, `${FP}Event`], [`${FP}title`, "A"]], "did:ng:o:b": [[TYPE, `${FP}Event`], [`${FP}title`, "B"]], }); const subjects = await readUnion(["did:ng:o:a", "did:ng:o:b"]); // Two per-doc ASK opens + one anchorless union query = 3 sparql_query calls. expect(ng.sparql_query).toHaveBeenCalledTimes(3); const anchorless = ng.sparql_query.mock.calls.filter( (c: unknown[]) => !String(c[1]).startsWith("ASK") && c[3] === undefined, ); expect(anchorless.length).toBe(1); expect(subjects.length).toBe(2); const a = subjects.find((s) => s.subject === "did:ng:o:a")!; expect(a.props[`${FP}title`]).toEqual(["A"]); }); test("readUnion groups predicates per subject", async () => { inject({ "did:ng:o:p": [ [TYPE, `${FP}Participation`], [`${FP}event`, "did:ng:o:e"], [`${FP}user`, "urn:festipod:user:x"], ], }); const s = (await readUnion(["did:ng:o:p"]))[0]!; expect(s.subject).toBe("did:ng:o:p"); expect(s.props[`${FP}event`]).toEqual(["did:ng:o:e"]); expect(s.props[`${FP}user`]).toEqual(["urn:festipod:user:x"]); }); test("readUnion returns [] for an empty doc set (no query)", async () => { const ng = inject({}); const subjects = await readUnion([]); expect(subjects).toEqual([]); expect(ng.sparql_query).toHaveBeenCalledTimes(0); }); test("a doc that fails to open is skipped, not aborting the union", async () => { const ng = fakeNgWith({ "did:ng:o:ok": [[TYPE, `${FP}Event`], [`${FP}title`, "ok"]] }); // Make the OPEN (ASK) throw for the bad doc only. const orig = ng.sparql_query; ng.sparql_query = mock(async (sid: string, query: string, base: unknown, anchor: unknown) => { if (query.startsWith("ASK") && anchor === "did:ng:o:bad") throw new Error("RepoNotFound"); return orig(sid, query, base, anchor); }) as any; configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => ({ sessionId: "sid-rm", privateStoreId: "priv" }), normalizeUser: (u: string) => u, }); const subjects = await readUnion(["did:ng:o:ok", "did:ng:o:bad"]); // The bad doc opened-failed but the good one still lists. expect(subjects.map((s) => s.subject)).toEqual(["did:ng:o:ok"]); });