feat(logs): logs données restructurés — identité en préfixe, trace résolution/barrière, inspection outbox

Chemin données bas-niveau du polyfill rendu lisible pour diagnostiquer en session live :

- Format identité-en-premier : `[<identity>][polyfill] OP shortNuri (label)` ;
  console.error épars (store-registry, inbox) unifiés au même préfixe.
- Trace (derrière le flag debug) : issue de la barrière ensureRepoOpen
  (synced|timed-out + durée) et résultat sémantique de chaque étage de résolution
  (resolvePointer/canonicalDoc/resolveAccount/resolveShimDoc/readScopeIndex).
- outbox-log.ts (nouveau) : inspection read-only de l'outbox hors-ligne au
  démarrage de session ; console.warn si non vide (anomalie, toujours visible),
  sous flag si vide. Le comptage seul est fiable (payloads BARE opaques côté JS).

Pas de changement fonctionnel. bun test 126 pass ; tsc 0 erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
Sylvain Duchesne
2026-07-14 10:37:46 +02:00
parent 3547967d37
commit 70de7afa3c
7 changed files with 207 additions and 33 deletions
+37 -7
View File
@@ -56,11 +56,39 @@ export function enabled(): boolean {
* (`getCurrentUser`) — the account/space the operation is scoped under, which is * (`getCurrentUser`) — the account/space the operation is scoped under, which is
* the discriminating signal for the isolation leak. NOT the physical wallet id * the discriminating signal for the isolation leak. NOT the physical wallet id
* (shared, constant → useless). `(none)` when no identity is set yet (startup). * (shared, constant → useless). `(none)` when no identity is set yet (startup).
* Exported so every other polyfill-layer log site (store-registry, inbox,
* outbox-log, …) shares the exact same identity resolution as the access log,
* instead of re-deriving it — see {@link accessLogPrefix}.
*/ */
function activeIdentity(): string { export function activeIdentity(): string {
return getCurrentUser() ?? "(none)"; return getCurrentUser() ?? "(none)";
} }
/**
* The common line prefix for every polyfill-layer low-level-data-path log:
* `[<identity>][polyfill]` — identity FIRST (the discriminating scan signal),
* `[polyfill]` glued right after with no space between the two brackets. Used by
* {@link logAccess} itself, by the unified `console.error`s in store-registry.ts /
* inbox.ts, and by the BARRIER / stage-resolution / OUTBOX lines (open-repo.ts,
* store-registry.ts, outbox-log.ts) — one single prefix builder so every polyfill
* log line is visually groupable by identity when scanning a live session.
*/
export function accessLogPrefix(): string {
return "[" + activeIdentity() + "][polyfill]";
}
/**
* Emit one CONCISE diagnostic line — ONLY when {@link enabled} — prefixed by
* {@link accessLogPrefix}. Used for the precise data-path trace (BARRIER
* resolution, per-stage resolution outcome, the empty-OUTBOX line): a single
* line per stage/event, never a dump. Callers pass the fully-composed suffix
* (e.g. `"BARRIER " + shortNuri(nuri) + " synced (842ms)"`).
*/
export function logStage(line: string): void {
if (!enabled()) return;
console.log(accessLogPrefix() + " " + line);
}
/** /**
* Shorten a NURI for the access log: the full form (`did:ng:o:<RepoID>:v:<...>`, * Shorten a NURI for the access log: the full form (`did:ng:o:<RepoID>:v:<...>`,
* ~100 chars) is too verbose to scan. Drop the `did:ng:o:` prefix and the `:v:<...>` * ~100 chars) is too verbose to scan. Drop the `did:ng:o:` prefix and the `:v:<...>`
@@ -77,11 +105,13 @@ export function shortNuri(nuri: string): string {
/** /**
* Log one document access — but ONLY when {@link enabled}. Off → returns * Log one document access — but ONLY when {@link enabled}. Off → returns
* immediately, prints nothing. Format: * immediately, prints nothing. Format:
* `[polyfill] [<identity>] READ <shortNuri> (<label>)` — optionally with `<extra>` * `[<identity>][polyfill] READ <shortNuri> (<label>)` — identity FIRST (the
* appended (e.g. ` → 3 rows`, a strong signal a doc rendered data under an identity * discriminating scan signal), `[polyfill]` glued right after with no space
* that should see nothing). The `[polyfill]` prefix marks these as SDK-layer access * between the two brackets — optionally with `<extra>` appended (e.g. ` → 3
* logs (distinct from the consumer app's own logs). The NURI is shortened by * rows`, a strong signal a doc rendered data under an identity that should see
* {@link shortNuri} to keep the line scannable. * nothing). The `[polyfill]` tag marks these as SDK-layer access logs (distinct
* from the consumer app's own logs). The NURI is shortened by {@link shortNuri}
* to keep the line scannable.
*/ */
export function logAccess( export function logAccess(
op: AccessOp, op: AccessOp,
@@ -91,6 +121,6 @@ export function logAccess(
): void { ): void {
if (!enabled()) return; if (!enabled()) return;
console.log( console.log(
"[polyfill] [" + activeIdentity() + "] " + op + " " + shortNuri(nuri) + " (" + label + ")" + (extra ?? ""), accessLogPrefix() + " " + op + " " + shortNuri(nuri) + " (" + label + ")" + (extra ?? ""),
); );
} }
+2 -1
View File
@@ -29,6 +29,7 @@ import { subscribeDoc } from "./subscribe";
import { ensureRepoOpen } from "./open-repo"; import { ensureRepoOpen } from "./open-repo";
import { getCurrentUser, getStoreRegistryDeps } from "./polyfill"; import { getCurrentUser, getStoreRegistryDeps } from "./polyfill";
import { escapeLiteral } from "./sparql"; import { escapeLiteral } from "./sparql";
import { accessLogPrefix } from "./access-log";
import type { Nuri, PrincipalId } from "./types"; import type { Nuri, PrincipalId } from "./types";
// --- deposit model -------------------------------------------------------- // --- deposit model --------------------------------------------------------
@@ -255,7 +256,7 @@ export function watch(
onDeposits(deposits); onDeposits(deposits);
} }
} catch (error) { } catch (error) {
console.error("[inbox] watch read failed:", error); console.error(accessLogPrefix() + " watch read failed:", error);
} }
}; };
+8
View File
@@ -61,6 +61,7 @@
import { getConfig, getStoreRegistryDeps } from "./polyfill"; import { getConfig, getStoreRegistryDeps } from "./polyfill";
import { subscribeDoc, type Unsubscribe } from "./subscribe"; import { subscribeDoc, type Unsubscribe } from "./subscribe";
import { logStage, shortNuri } from "./access-log";
import type { Nuri } from "./types"; import type { Nuri } from "./types";
/** /**
@@ -182,6 +183,11 @@ export async function ensureRepoOpen(nuri: Nuri): Promise<void> {
// Subscribed, first `State` not yet seen. // Subscribed, first `State` not yet seen.
syncState.set(nuri, "syncing"); syncState.set(nuri, "syncing");
// Barrier clock: how long the subscribe→first-State (or fallback) round-trip
// took, surfaced on the BARRIER trace line below — the most important line in
// the whole low-level data-path trace: it distinguishes a genuine absence
// (`synced` → a 0-row read means it) from a not-yet-synced read (`timed-out`).
const barrierStartedAt = Date.now();
const p = (async () => { const p = (async () => {
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@@ -191,6 +197,7 @@ export async function ensureRepoOpen(nuri: Nuri): Promise<void> {
if (settled) return; if (settled) return;
settled = true; settled = true;
syncState.set(nuri, "synced"); syncState.set(nuri, "synced");
logStage("BARRIER " + shortNuri(nuri) + " synced (" + (Date.now() - barrierStartedAt) + "ms)");
resolve(); resolve();
}; };
// Bounded fallback: proceed WITHOUT a `State`, but mark "timed-out" — NOT // Bounded fallback: proceed WITHOUT a `State`, but mark "timed-out" — NOT
@@ -200,6 +207,7 @@ export async function ensureRepoOpen(nuri: Nuri): Promise<void> {
if (settled) return; if (settled) return;
settled = true; settled = true;
syncState.set(nuri, "timed-out"); syncState.set(nuri, "timed-out");
logStage("BARRIER " + shortNuri(nuri) + " timed-out (" + (Date.now() - barrierStartedAt) + "ms)");
resolve(); resolve();
}; };
// The bootstrap subscription is kept ALIVE for the session — holding it open // The bootstrap subscription is kept ALIVE for the session — holding it open
+98
View File
@@ -0,0 +1,98 @@
/**
* outbox-log — read-only diagnostic inspection of `@ng-org/web`'s offline write
* outbox, at session bootstrap (polyfill-era, low-level-data-path trace).
*
* ── What this surfaces ──────────────────────────────────────────────────────
* `@ng-org/web` (the real injected SDK) queues writes made while offline/
* disconnected in an "outbox", persisted client-side in `sessionStorage` by the
* WASM verifier (see `sdk/rust/src/local_broker.rs` `JsStorageConfig::
* get_js_storage_config` in the `nextgraph-rs` core repo — read-only reference,
* NOT vendored here). A non-empty outbox at session start is an ANOMALY worth
* surfacing unconditionally: it means writes from a previous (disconnected)
* session are still queued and haven't reached the broker yet.
*
* ── sessionStorage key shapes (verified in the core repo, not guessed) ──────
* The outbox is keyed per LOCAL PEER id (`peer_id`, the persistent local peer's
* pubkey — NOT the ng-eventually shim's `account`/`identity` concept), via two
* key families written by `session_write`/read by `session_read`:
* - `ng_peer_last_seq@<peerId>` — the peer's last reserved seq number.
* - `ng_outboxes@<peerId>@start` — the seq number the outbox starts at.
* - `ng_outboxes@<peerId>@<00000-idx>` — one queued (base64url + BARE-encoded)
* event per zero-padded index, contiguous from 0 until the first miss (the
* exact shape `outbox_read_function` walks — see `local_broker.rs`).
* We don't know `peerId` ahead of time (it's internal to the injected SDK), so
* we DISCOVER it by scanning `sessionStorage` for `@start` markers instead of
* requiring it to be passed in — this also means the probe works unchanged
* however many peers/wallets the browser session has touched.
*
* ── Read-only, defensive, best-effort ────────────────────────────────────────
* This NEVER writes or deletes a key (unlike the real `outbox_read_function`,
* which drains on read) — it only counts. The queued event bytes are opaque
* (BARE-encoded Rust structs, base64url'd); decoding them to report concrete
* write TARGETS (topics/docs) would mean duplicating the WASM verifier's wire
* format in this polyfill, which is explicitly out of scope (SDK internals live
* in the `@ng-eventually/client`-independent core repo, per this repo's
* doctrine) — so only the pending COUNT is reported, never fabricated targets.
* `sessionStorage` access itself can throw (sandboxed iframe, disabled storage —
* see the exact error string handled in the core repo's `main.ts`
* `convert_error`), so the whole probe is wrapped in one try/catch: unavailable
* → skip silently, never throw into the caller.
*
* Polyfill-era; removed at the real multi-store migration alongside the rest of
* this low-level trace instrumentation.
*/
import { accessLogPrefix, logStage } from "./access-log";
/** Matches an outbox "start" marker key, capturing the peer id. */
const OUTBOX_START_KEY = /^ng_outboxes@(.+)@start$/;
/** Safety bound on the per-peer index walk, so a corrupted/mocked storage
* (e.g. a `@start` marker with no matching index gaps) can't spin forever.
* Real outboxes are queued-while-offline writes — nowhere near this size. */
const MAX_SCAN_PER_PEER = 10_000;
/**
* Inspect the outbox NOW and log its state — count only, never targets (see
* module doc). Non-empty → `console.warn`, ALWAYS printed (anomaly, not gated
* by the access-log flag). Empty → a normal {@link logStage} line, gated by the
* access-log flag like the rest of the low-level trace. Read-only: never
* mutates `sessionStorage`. Never throws.
*/
export function inspectOutbox(): void {
try {
const storage = (globalThis as any)?.sessionStorage;
if (!storage) return;
const peers = new Set<string>();
const length: number = storage.length ?? 0;
for (let i = 0; i < length; i++) {
const key = storage.key?.(i);
if (!key) continue;
const m = OUTBOX_START_KEY.exec(key);
const peerId = m?.[1];
if (peerId) peers.add(peerId);
}
let total = 0;
for (const peer of peers) {
let idx = 0;
while (idx < MAX_SCAN_PER_PEER) {
const idxKey = "ng_outboxes@" + peer + "@" + String(idx).padStart(5, "0");
if (storage.getItem(idxKey) === null) break;
total++;
idx++;
}
}
if (total > 0) {
// Anomaly: ALWAYS visible, regardless of the access-log flag.
console.warn(accessLogPrefix() + " OUTBOX " + total + " pending write(s)");
} else {
logStage("OUTBOX empty");
}
} catch {
// sessionStorage unavailable / access denied — skip silently. Diagnostic
// only, never a hard dependency of the read/write path.
}
}
+20 -1
View File
@@ -12,6 +12,7 @@ import type { NgLike, UseShapeLike, PrincipalId } from "./types";
import type { RegistrySession } from "./store-registry"; import type { RegistrySession } from "./store-registry";
import { CapRegistry } from "./caps"; import { CapRegistry } from "./caps";
import { setAccessLog } from "./access-log"; import { setAccessLog } from "./access-log";
import { inspectOutbox } from "./outbox-log";
/** /**
* Consumer-injected dependencies of the storeRegistry (polyfill-era). The * Consumer-injected dependencies of the storeRegistry (polyfill-era). The
@@ -99,8 +100,26 @@ export function resetConfig(): void {
* disappears at migration. * disappears at migration.
*/ */
export function configureStoreRegistry(deps: StoreRegistryDeps): void { export function configureStoreRegistry(deps: StoreRegistryDeps): void {
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,
// on the FIRST successful `getSession()` resolution — the most reliable
// "a session is established" signal available: every low-level reader/writer
// (store-registry, open-repo, read-model, subscribe, inbox) reaches its
// session through this SAME injected `getSession`, so wrapping it HERE catches
// the first success from whichever caller happens to run first, instead of
// tying the probe to one particular call site. Only on SUCCESS (an error
// propagates untouched, exactly as before) and only ONCE per
// `configureStoreRegistry()` call (a fresh session config → a fresh check).
let outboxInspected = false;
const getSession = async (): Promise<RegistrySession> => {
const session = await deps.getSession();
if (!outboxInspected) {
outboxInspected = true;
inspectOutbox();
}
return session;
};
registryDeps = { registryDeps = {
getSession: deps.getSession, getSession,
normalizeId: deps.normalizeId ?? ((id: string) => id.trim()), normalizeId: deps.normalizeId ?? ((id: string) => id.trim()),
// Default: single read (no re-read). Only the real-broker consumers (app + e2e) // Default: single read (no re-read). Only the real-broker consumers (app + e2e)
// opt into the bounded pointer micro-guard; unit fakes stay synchronous. // opt into the bounded pointer micro-guard; unit fakes stay synchronous.
+30 -9
View File
@@ -63,6 +63,7 @@ import { docCreate, sparqlUpdate, sparqlQuery } from "./docs";
import { getStoreRegistryDeps } from "./polyfill"; import { getStoreRegistryDeps } from "./polyfill";
import { ensureRepoOpen } from "./open-repo"; import { ensureRepoOpen } from "./open-repo";
import { escapeLiteral, escapeIri, assertNuri } from "./sparql"; import { escapeLiteral, escapeIri, assertNuri } from "./sparql";
import { accessLogPrefix, logStage, shortNuri } from "./access-log";
import type { Nuri, Scope } from "./types"; import type { Nuri, Scope } from "./types";
// --- sharedWalletShim model ---------------------------------------------- // --- sharedWalletShim model ----------------------------------------------
@@ -248,11 +249,20 @@ function bindingValue(row: Record<string, { value: string }>, key: string): stri
*/ */
function canonicalDoc(rows: Array<Record<string, { value: string }>>, key: string): Nuri { function canonicalDoc(rows: Array<Record<string, { value: string }>>, key: string): Nuri {
let chosen = ""; let chosen = "";
const distinct = new Set<string>();
for (const row of rows) { for (const row of rows) {
const v = bindingValue(row, key); const v = bindingValue(row, key);
if (!v) continue; if (!v) continue;
distinct.add(v);
if (chosen === "" || v < chosen) chosen = v; if (chosen === "" || v < chosen) chosen = v;
} }
// Stage trace: which doc got picked, and out of how many DISTINCT candidate
// values — >1 flags residual fork residue (see the module doc above) even
// when resolution still converges correctly on the canonical (smallest) one.
logStage(
"canonicalDoc(" + key + ") → " + (chosen ? shortNuri(chosen) : "none") +
" (" + distinct.size + (distinct.size === 1 ? " candidate)" : " candidates)"),
);
return chosen; return chosen;
} }
@@ -327,15 +337,19 @@ async function resolvePointer(): Promise<Nuri> {
try { try {
const result = await sparqlQuery(s.sessionId, query, undefined, root, "resolvePointer"); const result = await sparqlQuery(s.sessionId, query, undefined, root, "resolvePointer");
const doc = canonicalDoc(readBindings(result), "shimDoc"); const doc = canonicalDoc(readBindings(result), "shimDoc");
if (doc) return doc; if (doc) {
logStage("resolvePointer → 1 target: " + shortNuri(doc));
return doc;
}
} catch (error) { } catch (error) {
console.error("[storeRegistry] resolvePointer read failed:", error); console.error(accessLogPrefix() + " resolvePointer failed:", error);
} }
if (i < attempts - 1) { if (i < attempts - 1) {
await sleep(step); await sleep(step);
step = Math.min(step * 2, maxStepMs); step = Math.min(step * 2, maxStepMs);
} }
} }
logStage("resolvePointer → 0 targets");
return ""; return "";
} }
@@ -355,7 +369,7 @@ async function writePointer(doc: Nuri): Promise<void> {
try { try {
await sparqlUpdate(s.sessionId, update, root, "writePointer"); await sparqlUpdate(s.sessionId, update, root, "writePointer");
} catch (error) { } catch (error) {
console.error("[storeRegistry] writePointer failed:", error); console.error(accessLogPrefix() + " writePointer failed:", error);
} }
} }
@@ -394,6 +408,7 @@ async function resolveShimDoc(): Promise<Nuri> {
// so a cold 0 on the doc-shim is authoritative (genuinely absent), not sync-lag. // so a cold 0 on the doc-shim is authoritative (genuinely absent), not sync-lag.
await ensureRepoOpen(existing); await ensureRepoOpen(existing);
shimDocNuri = existing; shimDocNuri = existing;
logStage("resolveShimDoc → " + shortNuri(existing));
return existing; return existing;
} }
@@ -403,6 +418,7 @@ async function resolveShimDoc(): Promise<Nuri> {
await writePointer(doc); await writePointer(doc);
await ensureRepoOpen(doc); await ensureRepoOpen(doc);
shimDocNuri = doc; shimDocNuri = doc;
logStage("resolveShimDoc → " + shortNuri(doc));
return doc; return doc;
})(); })();
@@ -456,7 +472,7 @@ export async function loadShim(): Promise<Map<string, AccountRecord>> {
accountCache.set(key, record); accountCache.set(key, record);
} }
} catch (error) { } catch (error) {
console.error("[storeRegistry] loadShim failed:", error); console.error(accessLogPrefix() + " loadShim failed:", error);
} }
cache = map; cache = map;
return map; return map;
@@ -502,16 +518,20 @@ export async function resolveAccount(id: string): Promise<AccountRecord | null>
try { try {
const result = await sparqlQuery(s.sessionId, query, undefined, doc, "resolveAccount"); const result = await sparqlQuery(s.sessionId, query, undefined, doc, "resolveAccount");
const rows = readBindings(result); const rows = readBindings(result);
if (rows.length === 0) return null; if (rows.length === 0) {
logStage("resolveAccount(" + key + ") → null");
return null;
}
// DETERMINISTIC: a corrupted shim may return SEVERAL bindings for this one // DETERMINISTIC: a corrupted shim may return SEVERAL bindings for this one
// account subject (duplicate scope-doc values from past forks). Pick the // account subject (duplicate scope-doc values from past forks). Pick the
// canonical (lexicographically-smallest) doc per scope so writer and reader // canonical (lexicographically-smallest) doc per scope so writer and reader
// always resolve the SAME docPublic (robustness against PAST fork residue). // always resolve the SAME docPublic (robustness against PAST fork residue).
const record = recordFromRows(rows, id); const record = recordFromRows(rows, id);
accountCache.set(key, record); accountCache.set(key, record);
logStage("resolveAccount(" + key + ") → 1 record");
return record; return record;
} catch (error) { } catch (error) {
console.error("[storeRegistry] resolveAccount failed:", error); console.error(accessLogPrefix() + " resolveAccount failed:", error);
return null; return null;
} }
} }
@@ -542,7 +562,7 @@ async function writeRecord(doc: Nuri, record: AccountRecord): Promise<void> {
try { try {
await sparqlUpdate(s.sessionId, update, doc, "writeRecord"); await sparqlUpdate(s.sessionId, update, doc, "writeRecord");
} catch (error) { } catch (error) {
console.error("[storeRegistry] writeRecord persist failed:", error); console.error(accessLogPrefix() + " writeRecord persist failed:", error);
} }
} }
@@ -733,7 +753,7 @@ export async function createEntityDoc(id: string, scope: Scope): Promise<Nuri> {
"createEntityDoc", "createEntityDoc",
); );
} catch (error) { } catch (error) {
console.error("[storeRegistry] createEntityDoc index append failed:", error); console.error(accessLogPrefix() + " createEntityDoc index append failed:", error);
} }
return entityNuri; return entityNuri;
} }
@@ -765,8 +785,9 @@ async function readScopeIndex(indexDoc: Nuri): Promise<Nuri[]> {
if (v) out.push(v); if (v) out.push(v);
} }
} catch (error) { } catch (error) {
console.error("[storeRegistry] readScopeIndex read failed:", error); console.error(accessLogPrefix() + " readScopeIndex failed:", error);
} }
logStage("readScopeIndex(" + shortNuri(indexDoc) + ") → " + out.length + " entities");
return out; return out;
} }
+12 -15
View File
@@ -6,9 +6,10 @@
* (a) OFF by default: reads + writes via sparqlQuery / sparqlUpdate / docCreate * (a) OFF by default: reads + writes via sparqlQuery / sparqlUpdate / docCreate
* emit nothing to console.log. * emit nothing to console.log.
* (b) ON via configure({ debugAccessLog: true }): each read/write emits a line * (b) ON via configure({ debugAccessLog: true }): each read/write emits a line
* matching `[polyfill] [<identity>] READ/WRITE <shortNuri> (<label>)` plus * matching `[<identity>][polyfill] READ/WRITE <shortNuri> (<label>)` (identity
* row-count suffix on READs. The NURI is shortened by shortNuri (did:ng:o: * FIRST, `[polyfill]` glued right after) plus row-count suffix on READs. The
* prefix + :v: suffix stripped, RepoID truncated to 8 chars + ellipsis). * 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 * (c) ON via env var NG_EVENTUALLY_ACCESS_LOG=1: same behavior without changing
* calling code. * calling code.
* (d) Identity follows setCurrentUser: after setCurrentUser the prefix changes. * (d) Identity follows setCurrentUser: after setCurrentUser the prefix changes.
@@ -160,8 +161,7 @@ describe("access-log: ON via configure({ debugAccessLog: true })", () => {
restore(); restore();
} }
expect(lines.length).toBe(1); expect(lines.length).toBe(1);
expect(lines[0]).toMatch(/^\[polyfill\] /); // SDK-layer access-log prefix expect(lines[0]).toMatch(/^\[alice\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
expect(lines[0]).toMatch(/\[alice\]/);
expect(lines[0]).toMatch(/READ/); expect(lines[0]).toMatch(/READ/);
expect(lines[0]).toContain(shortNuri("did:ng:o:q")); // NURI shortened 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]).not.toContain("did:ng:o:"); // full prefix stripped
@@ -179,8 +179,7 @@ describe("access-log: ON via configure({ debugAccessLog: true })", () => {
restore(); restore();
} }
expect(lines.length).toBe(1); expect(lines.length).toBe(1);
expect(lines[0]).toMatch(/^\[polyfill\] /); expect(lines[0]).toMatch(/^\[alice\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
expect(lines[0]).toMatch(/\[alice\]/);
expect(lines[0]).toMatch(/WRITE/); expect(lines[0]).toMatch(/WRITE/);
expect(lines[0]).toContain(shortNuri("did:ng:o:w")); expect(lines[0]).toContain(shortNuri("did:ng:o:w"));
expect(lines[0]).toMatch(/writeLabel/); expect(lines[0]).toMatch(/writeLabel/);
@@ -196,8 +195,7 @@ describe("access-log: ON via configure({ debugAccessLog: true })", () => {
restore(); restore();
} }
expect(lines.length).toBe(1); expect(lines.length).toBe(1);
expect(lines[0]).toMatch(/^\[polyfill\] /); expect(lines[0]).toMatch(/^\[alice\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
expect(lines[0]).toMatch(/\[alice\]/);
expect(lines[0]).toMatch(/WRITE/); expect(lines[0]).toMatch(/WRITE/);
// The nuri is the value returned by ng.doc_create, shortened by shortNuri. // The nuri is the value returned by ng.doc_create, shortened by shortNuri.
expect(lines[0]).toContain(shortNuri("did:ng:o:log-doc")); expect(lines[0]).toContain(shortNuri("did:ng:o:log-doc"));
@@ -223,8 +221,7 @@ describe("access-log: ON via env var NG_EVENTUALLY_ACCESS_LOG=1", () => {
restore(); restore();
} }
expect(lines.length).toBe(1); expect(lines.length).toBe(1);
expect(lines[0]).toMatch(/^\[polyfill\] /); expect(lines[0]).toMatch(/^\[bob\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
expect(lines[0]).toMatch(/\[bob\]/);
expect(lines[0]).toMatch(/READ/); expect(lines[0]).toMatch(/READ/);
expect(lines[0]).toContain(shortNuri("did:ng:o:env-q")); expect(lines[0]).toContain(shortNuri("did:ng:o:env-q"));
}); });
@@ -241,7 +238,7 @@ describe("access-log: ON via env var NG_EVENTUALLY_ACCESS_LOG=1", () => {
restore(); restore();
} }
expect(lines.length).toBe(1); expect(lines.length).toBe(1);
expect(lines[0]).toMatch(/\[charlie\]/); expect(lines[0]).toMatch(/^\[charlie\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
expect(lines[0]).toMatch(/WRITE/); expect(lines[0]).toMatch(/WRITE/);
}); });
@@ -266,8 +263,8 @@ describe("access-log: identity follows setCurrentUser", () => {
restore(); restore();
} }
expect(lines.length).toBe(2); expect(lines.length).toBe(2);
expect(lines[0]).toMatch(/\[first-user\]/); expect(lines[0]).toMatch(/^\[first-user\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
expect(lines[1]).toMatch(/\[second-user\]/); expect(lines[1]).toMatch(/^\[second-user\]\[polyfill\] /);
}); });
it("prefix is (none) when no identity is set", async () => { it("prefix is (none) when no identity is set", async () => {
@@ -280,6 +277,6 @@ describe("access-log: identity follows setCurrentUser", () => {
restore(); restore();
} }
expect(lines.length).toBe(1); expect(lines.length).toBe(1);
expect(lines[0]).toMatch(/\[\(none\)\]/); expect(lines[0]).toMatch(/^\[\(none\)\]\[polyfill\] /); // identity-first, glued [polyfill] prefix
}); });
}); });