Files
ng-eventually/packages/client/test/open-repo.test.ts
T
Sylvain Duchesne 95010ddd66 test(client): couverture comportementale des 3 changements SDK (fake déterministe)
Les fix récents (anti-fork, open-repo, access-log) n'avaient AUCUN test de
comportement dans le domaine de la lib. Ajout de tests DÉTERMINISTES à base de `ng`
factice modélisant la condition d'échec (pas de broker réel — le harness e2e réel
réhydrate trop vite et ne reproduit pas ces cas).

- anti-fork.test.ts : fake avec lag de sync (0 lignes les K premières lectures du
  record de compte, puis les vraies). Asserte le cœur du fix : compte retrouvé au
  retry → réutilisé, 0 doc_create (pas de fork) ; compte réellement neuf → 1 seul
  jeu de docs provisionné, budget de retry prouvé consommé ; idempotence de session ;
  cas limite « trouvé à la dernière tentative ».
- open-repo.test.ts : fake où une requête ancrée rend vide tant que doc_subscribe
  n'a pas ouvert le repo. Asserte subscribe-avant-read + idempotence (Set des repos
  ouverts) + no-op si le ng n'a pas doc_subscribe.
- access-log.test.ts : off par défaut (silencieux), on via config ET via env
  NG_EVENTUALLY_ACCESS_LOG, préfixe = identité active (suit setCurrentUser),
  row-count sur READ. Restaure console.log/env fidèlement.

bun test : 91 → 117 pass, 0 fail. tsc --noEmit propre. src/ non touché.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:37:43 +02:00

215 lines
8.0 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/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<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,
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"]);
});
});