0c21adebf2
readUnion etait N requetes ancrees plus une politique de lot : une lecture qui echoue avalee en reponse vide, un document dont on ne detient pas la cle ecarte au lieu d'etre refuse, un regroupement en sacs de proprietes, et le filtrage du prefixe reserve. Rien en amont ne repond a cette composition, et son annotation citait sparql_query -- une citation exacte attachee a une affirmation fausse. Une composition ne s'aligne pas sur une fonction. Ce qui reste est ce qu'il y avait dessous : docs.sparqlQuery ancre sur un document, qui va deja chercher la cle servie par le magasin public -- donc une reference nue suffit toujours pour lire un document public, sans rien changer a useShapeQuery. Lire plusieurs documents, c'est cet appel une fois par document, et la tolerance a une lecture qui echoue revient a l'appelant, la ou l'amont la laisse : une requete ancree sur un depot que le verifier ne detient pas rejette. Cote src, l'unique lecture interne restante devient read-document.ts : un document, une requete ancree, precedee de l'ouverture du depot -- barriere que index-deposit consulte pour distinguer une absence d'un depot non synchronise. Le harnais e2e et l'exemple composent la leur, avec leur catch visible au point d'appel. Version majeure : 1.0.0-dev.8.
223 lines
8.6 KiB
TypeScript
223 lines
8.6 KiB
TypeScript
/**
|
|
* 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 `readDocumentSubjects` (from `surface/read-document`)
|
|
* because that is the production caller — it opens the document before its anchored
|
|
* read, and `emulated-verifier/index-deposit.ts` depends on that ordering to tell an
|
|
* absence from an unsynced repo. It was `readUnion` until `1.0.0-dev.8`; the integration
|
|
* path is the same one, minus the batch.
|
|
*/
|
|
|
|
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
import { ensureRepoOpen, ensureReposOpen, resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
|
import { readDocumentSubjects } from "../src/surface/read-document";
|
|
import { configure } from "../src/index";
|
|
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
|
|
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
|
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
|
|
|
|
afterAll(() => {
|
|
resetConfig();
|
|
resetStoreRegistry();
|
|
resetRegistryCache();
|
|
resetOpenedRepos();
|
|
});
|
|
|
|
// The reach guard and the cap registry are process-wide: once ANY cap exists the
|
|
// boundary applies to every reader. A suite that declares none must start from an
|
|
// empty one, or it inherits another suite's enforcement.
|
|
beforeEach(() => {
|
|
resetOpenedRepos();
|
|
resetRegistryCache();
|
|
resetCaps();
|
|
setCurrentUser(null);
|
|
});
|
|
|
|
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<string, Array<[string, string]>>,
|
|
) {
|
|
const subscribed = new Set<string>();
|
|
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<typeof makeFakeNgWithSubscribe>) {
|
|
configure({ ng: ng as any, useShape: (() => {}) as any });
|
|
configureStoreRegistry({
|
|
getSession: async () => SESSION,
|
|
normalizeId: (u: string) => u,
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 readDocumentSubjects("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 `readDocumentSubjects`, which opens the repo 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,
|
|
});
|
|
|
|
// 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("the anchored document read 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 readDocumentSubjects("did:ng:o:p");
|
|
|
|
// doc_subscribe was called as part of the `ensureRepoOpen` inside the read
|
|
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
|
expect(subjects.length).toBe(1);
|
|
expect(subjects[0]!.props[`${FP}event`]).toEqual(["did:ng:o:e"]);
|
|
});
|
|
});
|