fix(client): anchored default-graph writes for inbox + scope index

Inbox deposits and the per-(account,scope) index append were written into an
explicit GRAPH <plainNuri> named graph, which the real broker stores separately
from the repo's default graph (repo_graph_name with overlay suffix) — so an
anchored default-graph read (read-model.readDoc) never saw them and deposits
did not round-trip. Drop the GRAPH wrapper: the anchor scopes the write to the
repo's default graph, matching the read. Mocks updated accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-06 12:45:11 +02:00
parent c85c635f63
commit d39b12885a
5 changed files with 73 additions and 24 deletions
+16 -3
View File
@@ -61,10 +61,23 @@ function makeFakeNg() {
const sparql_update = mock(async (...a: unknown[]) => {
const query = a[1] as string;
const anchor = a[2] as string | undefined;
// TWO shapes coexist: the shim account write STILL uses `GRAPH <${priv}>`
// (the private-store repo's graph name equals the plain store NURI → it
// round-trips; key by that GRAPH IRI). The inbox deposit write has NO
// explicit GRAPH — the real broker keys it by the ANCHORED repo's default
// graph (repo_graph_name(id, overlay)); key it by the ANCHOR arg (a[2]).
const gm = query.match(/GRAPH <([^>]+)>\s*\{([\s\S]*)\}/);
if (!gm) return undefined;
const g = gm[1]!;
const body = gm[2]!;
let g: string;
let body: string;
if (gm) {
g = gm[1]!;
body = gm[2]!;
} else {
if (!anchor) return undefined;
g = anchor;
body = query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, "");
}
const sm = body.match(/<([^>]+)>/);
if (!sm) return undefined;
const s = sm[1]!;
+14 -5
View File
@@ -60,12 +60,19 @@ function makeFakeNg() {
const doc_create = mock(async (..._a: unknown[]) => "did:ng:o:new");
// Parses one deposit: `<subj> a <Deposit> ; <payload> "..." ; <ts> "..." [; <from> "..."] .`
//
// The REAL broker keys triples by the ANCHORED repo's default graph, not by an
// explicit `GRAPH <…>` IRI (repo_graph_name(repo_id, overlay_id)). So this mock
// keys stored quads by the ANCHOR arg (a[2]) — the default graph of the anchored
// repo — and REJECTS any explicit `GRAPH <…>` wrapper, so the old wrong shape
// does NOT round-trip and can never regress silently.
const sparql_update = mock(async (...a: unknown[]) => {
const query = a[1] as string;
const gm = query.match(/GRAPH <([^>]+)>\s*\{([\s\S]*)\}/);
if (!gm) return undefined;
const g = gm[1]!;
const body = gm[2]!;
const anchor = a[2] as string | undefined;
if (/GRAPH\s*</.test(query)) return undefined; // explicit-GRAPH write → dropped
if (!anchor) return undefined;
const g = anchor;
const body = query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, "");
const sm = body.match(/<([^>]+)>/);
if (!sm) return undefined;
const s = sm[1]!;
@@ -140,7 +147,9 @@ test("post writes via the real injected ng.sparql_update (not makeNg), scoped to
const call = fake.sparql_update.mock.calls[0]!;
expect(call[0]).toBe("sid-1"); // sessionId from the injected session
expect(call[2]).toBe(TARGET); // anchored to the target inbox
expect(call[1] as string).toContain(`GRAPH <${TARGET}>`);
// The write targets the anchored DEFAULT graph — NO explicit `GRAPH <…>`
// wrapper (which the real broker would route to a phantom graph).
expect(call[1] as string).not.toContain("GRAPH <");
});
test("post → read round-trips payload, from and ts", async () => {
+19 -3
View File
@@ -72,10 +72,26 @@ function makeFakeNg() {
// injection escaping keeps the query well-formed.
const sparql_update = mock(async (...a: unknown[]) => {
const query = a[1] as string;
const anchor = a[2] as string | undefined;
// TWO shapes coexist here:
// - the shim account write STILL uses `GRAPH <${privateStore}>` — the
// private STORE repo's graph name equals the plain store NURI, so it
// round-trips (login must not regress). Key it by that GRAPH IRI.
// - the per-entity INDEX write has NO explicit GRAPH — the real broker keys
// it by the ANCHORED repo's default graph (repo_graph_name(id, overlay)),
// so key it by the ANCHOR arg (a[2]). An explicit `GRAPH <indexDoc>`
// would target a phantom graph → must NOT round-trip.
const gm = query.match(/GRAPH <([^>]+)>\s*\{([\s\S]*)\}/);
if (!gm) return undefined;
const g = gm[1]!;
const body = gm[2]!;
let g: string;
let body: string;
if (gm) {
g = gm[1]!;
body = gm[2]!;
} else {
if (!anchor) return undefined;
g = anchor;
body = query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, "");
}
// Subject is the first <...> token in the body.
const sm = body.match(/<([^>]+)>/);
if (!sm) return undefined;