54f8389e9e
L'app d'exemple a servi de juge, et elle a immédiatement montré ce que l'inventaire ne montrait pas : pour partager une note elle résolvait l'inbox du destinataire, pour lire ses messages elle résolvait l'adresse de la sienne. Deux gestes qu'aucune application n'aura à faire une fois la chose native — donc deux gestes qu'elle ne doit pas apprendre. - `shareCap(cap, toUser)` remplace `shareCap(cap, toInbox)`. Partager est un acte envers quelqu'un ; où est son inbox regarde la bibliothèque. - `inbox.readForDocument(doc)` : le propriétaire lit ses messages en nommant la note, comme le déposant la nomme pour en laisser un. - `storeRegistry.userInbox` et `documentInboxAddress` sortent de la surface publiée. Ils restent joignables en interne, où le shim en a besoin. Sortent aussi de `/polyfill`, chacun parce qu'une app qui code contre apprend ce qu'il faudra désapprendre : - `getCaps` / `CapRegistry` — la salle des machines. La question du consommateur est `capFor(doc)` : est-ce que je le détiens ? Le registre n'a ni successeur ni forme inerte ; ce qui s'appuie dessus sera à réécrire, pas à laisser en place. - `getCurrentUser` — une app sait qui elle a connecté ; le redemander à la bibliothèque est une commodité du wallet partagé. - `virtualUsers` / `IdentityStore` — se souvenir d'une identité entre deux sessions est aussi le travail de l'app en amont. L'écran d'accès persiste ce dont IL a besoin ; rien d'autre n'a à être exposé. Reste sur `/polyfill` ce qu'une app appelle vraiment : `configure` et `setCurrentUser`. Le reste y est du test ou de l'injection interne. 170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la bibliothèque, l'exemple et le harnais.
220 lines
8.3 KiB
TypeScript
220 lines
8.3 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 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/emulated-verifier/open-repo";
|
|
import { readUnion } from "../src/surface/read-model";
|
|
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser} from "../src/polyfill";
|
|
import { resetInfrastructure } from "../src/emulated-verifier/reach";
|
|
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();
|
|
resetInfrastructure();
|
|
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 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,
|
|
});
|
|
|
|
// 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"]);
|
|
});
|
|
});
|