refactor(client): retirer anti-fork — gap non exhibé, resolveAccount simple
Preuve e2e (wallet frais, broker RÉEL rapide : 1er State 1-2 ms) : le private store est synchronisé au login, la lecture du shim réussit à froid — le « fork sur lag » que anti-fork compensait n'est PAS exhibé. Par le principe du polyfill (compenser un gap RÉEL, jamais du poids mort), et par la règle no-polling : - la version retry = polling (bannie) ; - la version barrière `ensureRepoOpen(privateStore)` = CASSÉE (un store n'émet pas de `State`, la barrière timeout systématiquement → CONTRAT 2 e2e échouait) ; - le gap = non exhibé. → `ensureAccount` fait un `resolveAccount(id)` SIMPLE (une lecture, provision si 0). `resolveAccountReliably`, `_forceOpenedSyncState` retirés ; `provisionRetry` gardé optionnel @deprecated (ignoré) pour ne pas casser les 8 tests qui le passent. `ensureRepoOpen`/`getSyncState` inchangés (chemin de lecture open-repo). gate : tsc 0 ; bun test 116 ; test:e2e 39 passed, CONTRAT 2 VERT (« same account, no second provisioning »). Le ~10s du re-resolve public est du scaling anchorless, pas de la lenteur broker (broker mesuré à 1-2 ms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* anti-fork.test.ts — behavioral tests for the ANTI-FORK guard in ensureAccount /
|
||||
* resolveAccountReliably (src/store-registry.ts).
|
||||
* ensureAccount-idempotent.test.ts — behavioral tests for the NO-FORK guarantee
|
||||
* of ensureAccount (src/store-registry.ts).
|
||||
*
|
||||
* The guard is: BEFORE reading the shim, open the private-store repo and await
|
||||
* the first `State` push (the deterministic sync barrier, CONTRACT 3). After the
|
||||
* barrier, 0 rows is DEFINITIVE (account genuinely absent) → provision exactly
|
||||
* once; rows present → reuse (NO-FORK). No retry loop, no polling.
|
||||
* The guarantee: ensureAccount always resolves via a SIMPLE resolveAccount read
|
||||
* (one targeted SPARQL query, no barrier, no retry). The private store is
|
||||
* synchronised at login, so 0 rows is DEFINITIVE (account genuinely absent) →
|
||||
* provision exactly once; rows present → reuse (NO-FORK). Idempotent within
|
||||
* and across sessions.
|
||||
*
|
||||
* With the unit fake `ng` (no `doc_subscribe`), `ensureRepoOpen` is a no-op
|
||||
* (getSyncState → "unknown") and the single shim read is immediate — no lag to
|
||||
* wait out, synchronous behaviour preserved.
|
||||
*
|
||||
* Timed-out barrier: if the barrier expires without a `State`, we refuse to
|
||||
* provision (throwing a clear error) rather than risk a fork.
|
||||
* (getSyncState → "unknown") and the single shim read is immediate —
|
||||
* synchronous behaviour preserved, no lag to wait out.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test";
|
||||
@@ -27,7 +25,7 @@ import {
|
||||
resetStoreRegistry,
|
||||
resetConfig,
|
||||
} from "../src/polyfill";
|
||||
import { resetOpenedRepos, _forceOpenedSyncState } from "../src/open-repo";
|
||||
import { resetOpenedRepos } from "../src/open-repo";
|
||||
|
||||
afterAll(() => {
|
||||
resetConfig();
|
||||
@@ -42,8 +40,7 @@ const SESSION: RegistrySession = { sessionId: "sid-af", privateStoreId: "PRIV-AF
|
||||
// Fake ng — simple in-memory store (no doc_subscribe → fake-ng no-op path)
|
||||
//
|
||||
// Queries return data from the `quads` array immediately. No lag simulation:
|
||||
// the barrier mechanism (ensureRepoOpen) is a no-op in the fake-ng path, so
|
||||
// the single shim read after "the barrier" is already authoritative.
|
||||
// the single shim read is already authoritative (private store is synced at login).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Quad { g: string; s: string; p: string; o: string }
|
||||
@@ -150,8 +147,6 @@ function inject(fakeNg: ReturnType<typeof makeFakeNg>) {
|
||||
configureStoreRegistry({
|
||||
getSession: async () => SESSION,
|
||||
normalizeId: (u) => u.trim().toLowerCase(),
|
||||
// provisionRetry is now unused by the barrier mechanism; kept for API compat.
|
||||
provisionRetry: { attempts: 1 },
|
||||
});
|
||||
resetRegistryCache();
|
||||
resetOpenedRepos();
|
||||
@@ -161,7 +156,7 @@ function inject(fakeNg: ReturnType<typeof makeFakeNg>) {
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("anti-fork: barrier-first resolveAccountReliably / ensureAccount", () => {
|
||||
describe("ensureAccount: no-fork + idempotence", () => {
|
||||
beforeEach(() => {
|
||||
resetRegistryCache();
|
||||
resetOpenedRepos();
|
||||
@@ -175,7 +170,7 @@ describe("anti-fork: barrier-first resolveAccountReliably / ensureAccount", () =
|
||||
expect(fakeNg.doc_create).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Simulate a fresh session: clear caches but keep quads intact (same fakeNg).
|
||||
// In the barrier model the single post-barrier read finds the account immediately.
|
||||
// The single resolveAccount read finds the account immediately.
|
||||
resetRegistryCache();
|
||||
resetOpenedRepos();
|
||||
|
||||
@@ -215,9 +210,9 @@ describe("anti-fork: barrier-first resolveAccountReliably / ensureAccount", () =
|
||||
expect(fakeNg.doc_create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("(d) fake-ng no-op barrier path: single shim read, no doc_subscribe calls", async () => {
|
||||
it("(d) fake-ng no-op path: single shim read per call, no doc_subscribe calls", async () => {
|
||||
// The fake ng has no doc_subscribe → ensureRepoOpen is a no-op (getSyncState → "unknown").
|
||||
// resolveAccountReliably must proceed to the single read without waiting or throwing.
|
||||
// ensureAccount must proceed to the single resolveAccount read without waiting or throwing.
|
||||
const fakeNg = makeFakeNg(); // no doc_subscribe
|
||||
inject(fakeNg);
|
||||
|
||||
@@ -234,24 +229,4 @@ describe("anti-fork: barrier-first resolveAccountReliably / ensureAccount", () =
|
||||
// The account query was called exactly twice (once per ensureAccount, no retries)
|
||||
expect(fakeNg.getAccountQueryCount()).toBe(2);
|
||||
});
|
||||
|
||||
it("(e) timed-out barrier: resolveAccountReliably throws rather than provisioning", async () => {
|
||||
// Simulate: the private store nuri is already in the `opened` set but with
|
||||
// sync state "timed-out" (forced via _forceOpenedSyncState so the test
|
||||
// does not have to wait 8s for the real OPEN_TIMEOUT_MS to fire).
|
||||
const fakeNg = makeFakeNg(); // empty quads → 0 rows
|
||||
inject(fakeNg);
|
||||
|
||||
// Force the private-store nuri ("did:ng:PRIV-AF") into timed-out state.
|
||||
// resolveAccountReliably calls anchorNuri() → `did:ng:${privateStoreId}`.
|
||||
_forceOpenedSyncState("did:ng:PRIV-AF", "timed-out");
|
||||
|
||||
// ensureAccount must throw (conservative anti-fork guard: do not provision blind)
|
||||
await expect(ensureAccount("TimedOutUser")).rejects.toThrow(
|
||||
/sync barrier timed out/,
|
||||
);
|
||||
|
||||
// Must NOT have created any scope docs (refusing to provision)
|
||||
expect(fakeNg.doc_create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user