chore(client): retirer la migration legacy + alléger les logs d'accès
- Migration des comptes legacy supprimée (migrateLegacyRecords + garde migratedInto + call-sites). Un wallet pré-fix (records store-root, pas de pointeur) provisionne simplement un doc-shim frais; contenu legacy ignoré (voulu, données = dev). La résolution barrière-autoritative + anti-fork (resolvePointer/ensureRepoOpen/ canonicalDoc/ensureInFlight/pointerGuard) est inchangée. - Logs d'accès SDK préfixés [polyfill] + NURI tronqué via shortNuri() (retire did:ng:o: et :v:…, garde 8 chars) → moins verbeux. Ex: [polyfill] [user1] READ vDlwbZio… (resolvePointer) → 1 triple-rows Tests: bun test unit 126/0. Docs (nextgraph-current-state/simulation/migration-guide) mis à jour (migration legacy retirée du modèle décrit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
* (a) OFF by default: reads + writes via sparqlQuery / sparqlUpdate / docCreate
|
||||
* emit nothing to console.log.
|
||||
* (b) ON via configure({ debugAccessLog: true }): each read/write emits a line
|
||||
* matching `[<identity>] READ/WRITE <nuri> (<label>)` plus row-count suffix
|
||||
* on READs.
|
||||
* matching `[polyfill] [<identity>] READ/WRITE <shortNuri> (<label>)` plus
|
||||
* row-count suffix on READs. The NURI is shortened by shortNuri (did:ng:o:
|
||||
* prefix + :v: suffix stripped, RepoID truncated to 8 chars + ellipsis).
|
||||
* (c) ON via env var NG_EVENTUALLY_ACCESS_LOG=1: same behavior without changing
|
||||
* calling code.
|
||||
* (d) Identity follows setCurrentUser: after setCurrentUser the prefix changes.
|
||||
@@ -17,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test";
|
||||
import { setAccessLog, enabled } from "../src/access-log";
|
||||
import { setAccessLog, enabled, shortNuri } from "../src/access-log";
|
||||
import { docCreate, sparqlUpdate, sparqlQuery } from "../src/docs";
|
||||
import {
|
||||
configure,
|
||||
@@ -159,9 +160,11 @@ describe("access-log: ON via configure({ debugAccessLog: true })", () => {
|
||||
restore();
|
||||
}
|
||||
expect(lines.length).toBe(1);
|
||||
expect(lines[0]).toMatch(/^\[polyfill\] /); // SDK-layer access-log prefix
|
||||
expect(lines[0]).toMatch(/\[alice\]/);
|
||||
expect(lines[0]).toMatch(/READ/);
|
||||
expect(lines[0]).toMatch(/did:ng:o:q/);
|
||||
expect(lines[0]).toContain(shortNuri("did:ng:o:q")); // NURI shortened
|
||||
expect(lines[0]).not.toContain("did:ng:o:"); // full prefix stripped
|
||||
expect(lines[0]).toMatch(/myLabel/);
|
||||
expect(lines[0]).toMatch(/→ 1 triple-rows/); // triple-count from the 1-row fake result
|
||||
});
|
||||
@@ -176,9 +179,10 @@ describe("access-log: ON via configure({ debugAccessLog: true })", () => {
|
||||
restore();
|
||||
}
|
||||
expect(lines.length).toBe(1);
|
||||
expect(lines[0]).toMatch(/^\[polyfill\] /);
|
||||
expect(lines[0]).toMatch(/\[alice\]/);
|
||||
expect(lines[0]).toMatch(/WRITE/);
|
||||
expect(lines[0]).toMatch(/did:ng:o:w/);
|
||||
expect(lines[0]).toContain(shortNuri("did:ng:o:w"));
|
||||
expect(lines[0]).toMatch(/writeLabel/);
|
||||
});
|
||||
|
||||
@@ -192,10 +196,11 @@ describe("access-log: ON via configure({ debugAccessLog: true })", () => {
|
||||
restore();
|
||||
}
|
||||
expect(lines.length).toBe(1);
|
||||
expect(lines[0]).toMatch(/^\[polyfill\] /);
|
||||
expect(lines[0]).toMatch(/\[alice\]/);
|
||||
expect(lines[0]).toMatch(/WRITE/);
|
||||
// The nuri is the value returned by ng.doc_create
|
||||
expect(lines[0]).toMatch(/did:ng:o:log-doc/);
|
||||
// The nuri is the value returned by ng.doc_create, shortened by shortNuri.
|
||||
expect(lines[0]).toContain(shortNuri("did:ng:o:log-doc"));
|
||||
});
|
||||
|
||||
it("enabled() returns true when set via setAccessLog", () => {
|
||||
@@ -218,9 +223,10 @@ describe("access-log: ON via env var NG_EVENTUALLY_ACCESS_LOG=1", () => {
|
||||
restore();
|
||||
}
|
||||
expect(lines.length).toBe(1);
|
||||
expect(lines[0]).toMatch(/^\[polyfill\] /);
|
||||
expect(lines[0]).toMatch(/\[bob\]/);
|
||||
expect(lines[0]).toMatch(/READ/);
|
||||
expect(lines[0]).toMatch(/did:ng:o:env-q/);
|
||||
expect(lines[0]).toContain(shortNuri("did:ng:o:env-q"));
|
||||
});
|
||||
|
||||
it("env var NG_EVENTUALLY_ACCESS_LOG=true also enables the log", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* anti-fork.test.ts — behavioral tests for the reconnection fix in the
|
||||
* polyfill-era shim (src/store-registry.ts), redesigned around the
|
||||
* pointer → doc-shim indirection. Three groups, one file:
|
||||
* pointer → doc-shim indirection. Two groups, one file:
|
||||
*
|
||||
* (1) DETERMINISTIC RESOLUTION — a doc-shim whose account subject carries
|
||||
* DUPLICATE scope-doc values (fork residue: several `shim:docPublic`) must
|
||||
@@ -17,11 +17,6 @@
|
||||
* authoritative, so a genuinely-present account is found on the first read
|
||||
* and never re-provisioned (no fork). This replaced the deleted
|
||||
* `resolveAccountReliably` / `provisionRetry` account retry.
|
||||
*
|
||||
* (3) MIGRATION — a wallet whose accounts were written at the OLD location (the
|
||||
* store-root graph, pre-indirection) must have those records MIGRATED into
|
||||
* the doc-shim at resolution, so a legacy account resolves (is READ, not
|
||||
* re-provisioned) after reconnect. No existing account is lost.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test";
|
||||
@@ -56,8 +51,7 @@ const ROOT = "did:ng:PRIV-AF";
|
||||
// - The POINTER (`<shim:root> <shim:shimDoc> <docShim>`) lives in the store-root
|
||||
// graph (keyed by GRAPH <ROOT>).
|
||||
// - AccountRecords live in the doc-shim (anchored default graph, keyed by the
|
||||
// anchor arg = the doc-shim NURI). A LEGACY record may ALSO be seeded directly
|
||||
// in the store-root graph (the pre-indirection location) to exercise migration.
|
||||
// anchor arg = the doc-shim NURI).
|
||||
// - doc_subscribe pushes an initial `State` so ensureRepoOpen resolves at once
|
||||
// (the barrier).
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -107,9 +101,9 @@ function makeSparqlUpdate(quads: Quad[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Build the account-SELECT bindings from the quads in ONE graph (the doc-shim, or
|
||||
* the store-root for the migration read), grouped per subject. A subject with
|
||||
* DUPLICATE scope docs yields a cross-product of bindings — a corrupted shim. */
|
||||
/** Build the account-SELECT bindings from the quads in ONE graph (the doc-shim),
|
||||
* grouped per subject. A subject with DUPLICATE scope docs yields a cross-product
|
||||
* of bindings — a corrupted shim. */
|
||||
function accountBindings(quads: Quad[], anchor: string | undefined, onlySubject: string | null) {
|
||||
const bySubject = new Map<string, { id: string; pub: string[]; prot: string[]; priv: string[] }>();
|
||||
for (const q of quads) {
|
||||
@@ -147,9 +141,8 @@ function makeFakeNg() {
|
||||
const quads: Quad[] = [];
|
||||
let docCounter = 0;
|
||||
// Count account SELECTs per anchor graph. The AUTHORITATIVE account read is anchored
|
||||
// to the doc-shim (a did:ng:o: NURI); the one-time legacy MIGRATION scan is anchored
|
||||
// to the store-root (ROOT). Counting per-anchor lets a test assert "exactly one
|
||||
// doc-shim account read" (no account RETRY) separately from the migration scan.
|
||||
// to the doc-shim (a did:ng:o: NURI). Counting per-anchor lets a test assert "exactly
|
||||
// one doc-shim account read" (no account RETRY).
|
||||
const accountReadsByAnchor = new Map<string, number>();
|
||||
const doc_create = mock(async (..._a: unknown[]) => `did:ng:o:doc${++docCounter}`);
|
||||
const sparql_update = makeSparqlUpdate(quads);
|
||||
@@ -163,8 +156,7 @@ function makeFakeNg() {
|
||||
.map((q) => ({ shimDoc: { value: q.o } }));
|
||||
return { results: { bindings } };
|
||||
}
|
||||
// Account SELECT — anchored either to the doc-shim (authoritative read) or, for
|
||||
// the migration scan, to the store-root (legacy location). Same shape either way.
|
||||
// Account SELECT — anchored to the doc-shim (the authoritative read).
|
||||
if (query.includes("<urn:ng-eventually:shim:id>")) {
|
||||
accountReadsByAnchor.set(anchor ?? "", (accountReadsByAnchor.get(anchor ?? "") ?? 0) + 1);
|
||||
const subjM = query.match(/<([^>]+)>\s+a\s+<urn:ng-eventually:shim:Account>/);
|
||||
@@ -185,7 +177,7 @@ function makeFakeNg() {
|
||||
_quads: quads,
|
||||
// Total account reads across all anchors.
|
||||
getAccountQueryCount: () => [...accountReadsByAnchor.values()].reduce((a, b) => a + b, 0),
|
||||
// Account reads anchored to a did:ng:o: doc-shim (excludes the store-root migration scan).
|
||||
// Account reads anchored to a did:ng:o: doc-shim.
|
||||
getDocShimAccountReads: () =>
|
||||
[...accountReadsByAnchor.entries()]
|
||||
.filter(([g]) => g.startsWith("did:ng:o:"))
|
||||
@@ -292,8 +284,7 @@ describe("reconnect resolves the SAME account through the doc-shim barrier (no f
|
||||
expect(resolved.docProtected).toBe(orig.docProtected);
|
||||
expect(resolved.docPrivate).toBe(orig.docPrivate);
|
||||
expect(reconnect.doc_create).toHaveBeenCalledTimes(0); // no new docs → no fork
|
||||
// Barrier-authoritative: found on the FIRST doc-shim read (the store-root migration
|
||||
// scan, which finds nothing to migrate here, is separate).
|
||||
// Barrier-authoritative: found on the FIRST doc-shim read.
|
||||
expect(reconnect.getDocShimAccountReads()).toBe(1);
|
||||
});
|
||||
|
||||
@@ -336,62 +327,3 @@ describe("reconnect resolves the SAME account through the doc-shim barrier (no f
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// (3) Migration — legacy store-root records folded into the doc-shim
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("migration: legacy store-root records are read (not re-provisioned) after reconnect", () => {
|
||||
beforeEach(() => { resetRegistryCache(); resetOpenedRepos(); });
|
||||
|
||||
it("(3a) a pre-indirection account (records in the store-root, no pointer) is migrated into a new doc-shim and resolves — no fork", async () => {
|
||||
const fakeNg = makeFakeNg();
|
||||
inject(fakeNg);
|
||||
// Seed a LEGACY account directly in the store-root graph (old location), NO pointer.
|
||||
const subj = "urn:ng-eventually:shim:account:legacyuser";
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:id", o: "legacyuser" });
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:docPublic", o: "did:ng:o:legacy-pub" });
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:docProtected", o: "did:ng:o:legacy-prot" });
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:docPrivate", o: "did:ng:o:legacy-priv" });
|
||||
|
||||
const rec = await ensureAccount("legacyuser");
|
||||
|
||||
// The legacy account was READ (migrated), NOT re-provisioned: original NURIs come
|
||||
// back, and NO scope docs were created (only the fresh doc-shim: 1 doc_create).
|
||||
expect(rec.docPublic).toBe("did:ng:o:legacy-pub");
|
||||
expect(rec.docProtected).toBe("did:ng:o:legacy-prot");
|
||||
expect(rec.docPrivate).toBe("did:ng:o:legacy-priv");
|
||||
expect(fakeNg.doc_create).toHaveBeenCalledTimes(1); // only the new doc-shim
|
||||
|
||||
// A pointer was written, and the record now also lives in the doc-shim.
|
||||
const pointerRow = fakeNg._quads.find(
|
||||
(q) => q.g === ROOT && q.p === "urn:ng-eventually:shim:shimDoc",
|
||||
);
|
||||
expect(pointerRow?.o).toMatch(/^did:ng:o:doc/);
|
||||
const inDocShim = fakeNg._quads.some(
|
||||
(q) => q.g === pointerRow!.o && q.p === "urn:ng-eventually:shim:docPublic" && q.o === "did:ng:o:legacy-pub",
|
||||
);
|
||||
expect(inDocShim).toBe(true);
|
||||
});
|
||||
|
||||
it("(3b) a pointer that predates migration still folds legacy store-root records into its doc-shim", async () => {
|
||||
const fakeNg = makeFakeNg();
|
||||
inject(fakeNg);
|
||||
// A doc-shim + pointer already exist (empty doc-shim), records still only in the root.
|
||||
const docShim = "did:ng:o:existing-shim";
|
||||
fakeNg._quads.push({ g: ROOT, s: "urn:ng-eventually:shim:root", p: "urn:ng-eventually:shim:shimDoc", o: docShim });
|
||||
const subj = "urn:ng-eventually:shim:account:oldtimer";
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:id", o: "oldtimer" });
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:docPublic", o: "did:ng:o:old-pub" });
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:docProtected", o: "did:ng:o:old-prot" });
|
||||
fakeNg._quads.push({ g: ROOT, s: subj, p: "urn:ng-eventually:shim:docPrivate", o: "did:ng:o:old-priv" });
|
||||
|
||||
const rec = await ensureAccount("oldtimer");
|
||||
|
||||
expect(rec.docPublic).toBe("did:ng:o:old-pub");
|
||||
expect(fakeNg.doc_create).toHaveBeenCalledTimes(0); // doc-shim already existed
|
||||
const inDocShim = fakeNg._quads.some(
|
||||
(q) => q.g === docShim && q.p === "urn:ng-eventually:shim:docPublic" && q.o === "did:ng:o:old-pub",
|
||||
);
|
||||
expect(inDocShim).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user