test(client/e2e): CONTRAT — le 1er State d'un abonnement = barrière de sync

Épingle empiriquement (broker réel) le contrat implicite sur lequel open-repo
repose : après le 1er événement `State` d'un `doc_subscribe`, la PRÉSENCE d'une
donnée est GARANTIE (le triple écrit est déjà dans ce State, sans attente
supplémentaire) et l'ABSENCE est DÉFINITIVE (doc vide reste vide, +5s de grâce).
Si ce contrat casse (changement de version broker), ce test le détecte.

Constats mesurés : l'abonnement pousse `TabInfo` (~1-3ms) PUIS `State` (~2-3ms)
— le State est le 2e événement, pas le 1er ; latence 1er State ~2-3ms sur profil
frais. IMPORTANT (à corriger phase 2) : open-repo résout son attente sur le 1er
push (= TabInfo), PAS sur le State → il rend la main avant la vraie barrière ; le
commentaire « resolves on the FIRST push (the initial State) » est faux.

gate : test:e2e 39 passed ; bun test 117 ; tsc propre. src/ non touché.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-09 09:33:59 +02:00
parent d98777e6e3
commit 956a2228cc
2 changed files with 282 additions and 0 deletions
+134
View File
@@ -606,6 +606,140 @@ const identity = new IdentityStore(
identitySet(id: string) { return identity.set(id); },
identityGet() { return identity.get(); },
identityClear() { identity.clear(); return identity.get(); },
// ── CONTRACT: first-State barrier (doc_subscribe sync-point) ────────────
//
// The contract under test: the 1st event emitted by doc_subscribe is a `State`
// that marks the end of the initial broker sync. After that push:
// - if the doc has data → it MUST be present in the State (no "not yet synced")
// - if the doc is empty → it IS definitively empty (no later arrival)
//
// We drive this via the polyfill's subscribeDoc (which calls ng.doc_subscribe
// directly), but we capture the RAW AppResponse for each push so we can:
// (a) identify the event type (State vs Patch vs TabInfo vs other)
// (b) measure the time-to-first-State
// (c) correlate "State received" with the SPARQL content query result
//
// Bridge state for a single active first-State probe.
_stateProbe: null as null | {
doc: string;
startMs: number;
events: Array<{ typeKey: string; elapsedMs: number }>;
unsub: () => void;
},
/**
* PRESENCE probe — phase 1: write a triple into `doc` (same session, so the
* write is already committed on the broker before we subscribe). Returns the doc
* NURI so the test can hand it to stateProbeSubscribe. Split into write + subscribe
* so the test controls the ordering precisely.
*/
async stateProbeWrite(triple: { s: string; p: string; o: string }): Promise<string> {
const s = await sessionReady;
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
await docs.sparqlUpdate(
s.session_id,
`INSERT DATA { <${triple.s}> <${triple.p}> "${triple.o}" }`,
doc,
);
return doc;
},
/**
* Subscribe to `doc` and record every raw event (type key + elapsed ms from
* subscribe call). The state probe slot is single-use per call; a previous probe
* is torn down first. `subscribeDoc` is the polyfill wrapper — it passes the raw
* AppResponse straight through to the callback.
*/
stateProbeSubscribe(doc: string): void {
// Tear down any prior probe.
const prev = (window as any).__sdk._stateProbe;
if (prev) { try { prev.unsub(); } catch { /* ignore */ } }
const probe = {
doc,
startMs: Date.now(),
events: [] as Array<{ typeKey: string; elapsedMs: number }>,
unsub: () => {},
};
(window as any).__sdk._stateProbe = probe;
probe.unsub = subscribeDoc(doc, (resp: any) => {
const elapsedMs = Date.now() - probe.startMs;
// AppResponse shape: { V0: { State: … } } | { V0: { Patch: … } } | { V0: { TabInfo: … } } | …
let typeKey = "unknown";
try {
const v0 = resp?.V0 ?? resp?.v0 ?? resp;
if (v0 && typeof v0 === "object") {
const keys = Object.keys(v0);
typeKey = keys[0] ?? "empty";
} else {
typeKey = String(v0);
}
} catch { typeKey = "parse-error"; }
probe.events.push({ typeKey, elapsedMs });
});
},
/** Return the accumulated event log (snapshot — safe to call at any time). */
stateProbeEvents(): Array<{ typeKey: string; elapsedMs: number }> {
return (window as any).__sdk._stateProbe?.events ?? [];
},
/**
* Return the count of events with typeKey === `State` received so far.
* Used by waitForFunction to wait for the first State (not just any event —
* the broker pushes TabInfo first, State second).
*/
stateProbeStateCount(): number {
const events: Array<{ typeKey: string }> = (window as any).__sdk._stateProbe?.events ?? [];
return events.filter((e) => e.typeKey === "State").length;
},
/**
* After the first State has been received, run an anchored SPARQL query on the
* probe doc to read back its triples. Returns the raw rows so the test can assert
* presence/absence without re-querying from the test side.
*/
async stateProbeQuery(s_uri: string, p_uri: string): Promise<{ rows: number; found: boolean }> {
const s = await sessionReady;
const doc = (window as any).__sdk._stateProbe?.doc;
if (!doc) return { rows: -1, found: false };
try {
const res: any = await docs.sparqlQuery(
s.session_id,
`SELECT ?o WHERE { <${s_uri}> <${p_uri}> ?o }`,
undefined,
doc,
);
const rows = Array.isArray(res)
? res.length
: (res?.results?.bindings?.length ?? 0);
return { rows, found: rows > 0 };
} catch (e: any) {
return { rows: -2, found: false };
}
},
/** Stop the active probe's subscription. */
stateProbeStop(): void {
const probe = (window as any).__sdk._stateProbe;
if (probe) { try { probe.unsub(); } catch { /* ignore */ } }
(window as any).__sdk._stateProbe = null;
},
/**
* ABSENCE probe — subscribe to a doc NURI that was never written (non-existent).
* Returns the doc immediately after creating it (empty), then subscribes.
* The test waits for the first State, then verifies the doc is empty AND stays
* empty for a grace window (no late Patch arrival).
*/
async stateProbeEmptyDoc(): Promise<string> {
// Create a real doc so it's valid for doc_subscribe, but write NOTHING into it.
const s = await sessionReady;
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
// Subscribe immediately (no data written).
(window as any).__sdk.stateProbeSubscribe(doc);
return doc;
},
};
// ── Connect to the real broker ─────────────────────────────────────────────