"use strict"; const {test, afterEach} = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const vm = require("node:vm"); const source = fs.readFileSync(process.env.BACKGROUND_SOURCE || path.join(__dirname, "../../firefox-extension/session-v2/background.js"), "utf8"); const A = "11111111-1111-4111-8111-111111111111"; const B = "22222222-2222-4222-8222-222222222222"; const KEY = "labfy.session.window_uuid"; const fixtures = []; afterEach(() => { // Assertions thrown inside extension callbacks may be deliberately caught. // Record forbidden reads as well, so those catches cannot hide a privacy regression. for (const f of fixtures.splice(0)) assert.deepEqual(f.forbiddenReads, []); }); const plain = value => JSON.parse(JSON.stringify(value)); const flush = async () => { for (let i = 0; i < 80; ++i) await Promise.resolve(); }; function event() { const listeners = []; return {addListener: fn => listeners.push(fn), emit: (...args) => listeners.map(fn => fn(...args))}; } async function fixture(options = {}) { let now = 0, sequence = 0, random = 0; const timers = new Map(), values = options.values || new Map(); const state = {ids: options.ids || [1], values, forbiddenReads: [], writes: [], maps: [], updates: [], ports: [], reads: [], mode: "ok", creates: 0, read: options.read, write: options.write, enumerate: options.enumerate}; state.bootstrap = options.bootstrap; state.bootstraps = []; fixtures.push(state); const schedule = (fn, ms, interval = 0) => { const id = ++sequence; timers.set(id, {at: now + ms, fn, interval}); return id; }; // Poisoned properties make any navigation/content inspection fail the test. function window(id) { const w = {id, type: "normal"}; for (const name of ["title", "url", "tabs", "width", "height"]) Object.defineProperty(w, name, {get() { state.forbiddenReads.push(name); assert.fail(`Forbidden window property: ${name}`); }}); return w; } let nextWindow = Math.max(0, ...state.ids); const onMessage = event(); const runtime = { id: "session-v2@labfy.org", onMessage, onInstalled: event(), getURL: name => `moz-extension://synthetic/${name}`, connectNative(name) { assert.equal(name, "org.labfy.session_v2_identity"); const p = {onMessage: event(), onDisconnect: event(), disconnected: false, disconnect() { p.disconnected = true; p.onDisconnect.emit(); }, postMessage(message) { assert.equal(p.disconnected, false); const m = plain(message); if (m.op === "bootstrap-eligibility") { assert.deepEqual(m, {op: "bootstrap-eligibility"}); state.bootstraps.push({port: state.ports.indexOf(p)}); if (state.bootstrapHook) return state.bootstrapHook(p); if (state.bootstrap === "throw") throw new Error("bootstrap post failure"); if (state.bootstrap === "disconnect") { p.disconnect(); return; } if (state.bootstrap !== "hold") p.onMessage.emit(state.bootstrap === "invalid" ? {result: "OK", baseline_enrollment_allowed: "true"} : {result: "OK", baseline_enrollment_allowed: state.bootstrap === true}); return; } assert.deepEqual(Object.keys(m).sort(), ["op", "runtime_token", "uuid"]); state.maps.push({port: state.ports.indexOf(p), ...m}); if (state.mode === "throw") throw new Error("synthetic post failure"); if (state.mode === "disconnect") { p.disconnect(); return; } if (state.mode === "ok") p.onMessage.emit({result: "OK", uuid: m.uuid, con_id: 100 + state.maps.length}); }}; state.ports.push(p); return p; }, }; const browser = {runtime, windows: { onCreated: event(), onRemoved: event(), create: async query => { assert.deepEqual(plain(query), {type: "normal"}); ++state.creates; const id = ++nextWindow; state.ids.push(id); return window(id); }, getAll: async query => { if (state.enumerate) state.enumerate(state); assert.deepEqual(plain(query), {populate: false, windowTypes: ["normal"]}); return state.ids.map(window); }, update: async (id, change) => { assert.deepEqual(Object.keys(change), ["titlePreface"]); state.updates.push({id, ...plain(change)}); if (state.update) return state.update(id, change); }, }, sessions: { onChanged: event(), getWindowValue: async (id, key) => { assert.equal(key, KEY); state.reads.push({id, at: now}); return state.read ? state.read(id, now) : values.get(id); }, setWindowValue: async (id, key, value) => { assert.equal(key, KEY); state.writes.push({id, value, at: now}); if (state.write) return state.write(id, value); values.set(id, value); }, getTabValue: () => { state.forbiddenReads.push("getTabValue"); assert.fail("Tab fallback forbidden"); }, setTabValue: () => { state.forbiddenReads.push("setTabValue"); assert.fail("Tab witness writes forbidden"); }, }, tabs: new Proxy({}, {get() { state.forbiddenReads.push("tabs API"); assert.fail("Tab content API forbidden"); }})}; vm.runInNewContext(source, {browser, performance: {now: () => now}, crypto: {randomUUID: () => options.random ? options.random(++random) : `99999999-9999-4999-8999-${String(++random).padStart(12, "0")}`}, setTimeout: (fn, ms) => schedule(fn, ms), clearTimeout: id => timers.delete(id), setInterval: (fn, ms) => schedule(fn, ms, ms)}); state.tick = async ms => { const end = now + ms; await flush(); while (true) { const next = [...timers].filter(([, t]) => t.at <= end).sort((a, b) => a[1].at - b[1].at || a[0] - b[0])[0]; if (!next) break; const [id, t] = next; now = t.at; timers.delete(id); if (t.interval) timers.set(id, {...t, at: now + t.interval}); t.fn(); await flush(); } now = end; await flush(); }; state.open = (id = ++nextWindow, extra = {}) => { if (!state.ids.includes(id)) state.ids.push(id); const w = window(id); Object.assign(w, extra); browser.windows.onCreated.emit(w); return id; }; state.close = id => { state.ids = state.ids.filter(value => value !== id); browser.windows.onRemoved.emit(id); }; state.changed = () => browser.sessions.onChanged.emit(); state.install = (reason = "install") => runtime.onInstalled.emit({reason}); state.status = async () => plain(await onMessage.emit({op: "identity-status"}, {id: runtime.id})[0]); state.rawMessage = (message, sender) => onMessage.emit(message, sender)[0]; state.capture = async () => { const results = state.ports.at(-1).onMessage.emit({event: "capture"}); await flush(); state.captureResult = results[0]; }; if (options.install) state.install(); await flush(); return state; } const ready = async f => { await f.tick(33000); assert.equal((await f.status()).phase, "READY"); }; const capture = async f => { await f.capture(); await f.tick(2000); return plain(await f.captureResult); }; // Assertion ancienne remplacée : install seul n'est pas une preuve d'absence de checkpoint. test("installation sans autorité backend : baseline bloquée même si install arrive après start", async () => { const f = await fixture({ids: [1, 2]}); await f.tick(10000); f.install(); await ready(f); assert.equal(f.writes.length, 0); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.creates, 0); }); test("Ctrl+N après prêt et longtemps après settle : automatique unique et capturable", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(); await f.tick(999); assert.equal(f.writes.length, 0); await f.tick(3001); assert.equal(f.writes.length, 1); await f.tick(120000); f.open(); await f.tick(4000); assert.equal(f.writes.length, 2); assert.equal(new Set(f.writes.map(w => w.value)).size, 2); assert.equal((await capture(f)).result, "OK"); assert.equal(f.maps.length, 3); }); test("startup restore undefined au-delà du boot : aucune écriture puis valeur tardive préservée", async () => { const f = await fixture(); await f.tick(70000); assert.equal((await f.status()).phase, "READY"); assert.equal((await f.status()).result, "REFUSED"); f.values.set(1, A); assert.equal((await capture(f)).result, "OK"); f.open(); await f.tick(10000); assert.equal(f.writes.length, 1); }); test("onCreated durant startup : UUID reçu avant stabilisation préservé", async () => { const f = await fixture({values: new Map([[1, A]])}); await f.tick(10000); f.open(2); await f.tick(15000); assert.equal(f.writes.length, 0); f.values.set(2, B); await f.tick(12000); assert.equal((await f.status()).phase, "READY"); f.open(3); await f.tick(4000); assert.deepEqual(f.writes.map(w => w.id), [3]); }); test("installation baseline bloquée ; valeur du candidat boot reçue avant stabilisation préservée", async () => { const f = await fixture({install: true}); f.open(2); await f.tick(25000); assert.equal(f.writes.length, 0); f.values.set(2, B); await f.tick(14000); assert.deepEqual(f.writes.map(w => w.id), []); assert.equal(f.values.get(2), B); }); test("deux fenêtres mêmes contenus, créations simultanées : deux identités distinctes", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); f.open(3); await f.tick(4000); assert.deepEqual(f.writes.map(w => w.id), [2, 3]); assert.equal(new Set(f.writes.map(w => w.value)).size, 2); }); test("signal onChanged : UUID restauré reçu pendant guard préservé", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.changed(); f.open(2); await f.tick(4000); assert.equal(f.writes.length, 0); f.values.set(2, B); await f.tick(6000); assert.equal((await capture(f)).result, "OK"); assert.equal(f.values.get(2), B); }); test("onChanged pendant observation : passage révoqué puis candidat réobservé", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(1500); f.changed(); await f.tick(15000); assert.equal(f.writes.length, 1); assert.equal((await capture(f)).result, "OK"); }); test("sessionId positif protège ; absence de sessionId ne prouve rien", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2, {sessionId: "closed-session"}); await f.tick(10000); assert.equal(f.writes.length, 0); }); test("restauration tardive non signalée avec UUID pendant observation conservé", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(1500); f.values.set(2, B); await f.tick(5000); assert.equal(f.writes.length, 0); assert.equal((await capture(f)).result, "OK"); }); test("limite irréductible : restauration sans signal ni extData ressemble à Ctrl+N", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(4000); // Ce test expose le compromis, pas une preuve de classification parfaite. assert.equal(f.writes.length, 1); f.values.set(2, B); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.writes.length, 1); assert.equal(f.values.get(2), B); }); test("API rejection sur candidat : refus sans retry, capture peut lire valeur tardive", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.read = id => { if (id === 2) throw new Error("synthetic rejection"); return A; }; f.open(2); await f.tick(5000); assert.equal(f.writes.length, 0); f.read = id => id === 1 ? A : undefined; await f.tick(5000); assert.equal(f.writes.length, 0); f.values.set(2, B); f.read = null; assert.equal((await capture(f)).result, "OK"); }); test("provider reconnect, captures concurrentes et ancien port ne changent aucune identité", async () => { const f = await fixture({values: new Map([[1, A]])}); f.mode = "hold"; await f.capture(); await f.capture(); assert.equal(f.maps.length, 1); const old = f.ports[0]; old.disconnect(); await flush(); f.mode = "ok"; await f.tick(1000); assert.equal((await capture(f)).result, "OK"); old.onMessage.emit({result: "OK", uuid: B, con_id: 999}); assert.equal(f.writes.length, 0); assert.equal(f.updates.at(-1).titlePreface, ""); }); test("timeout natif et post failure restent récupérables sans vieux timer destructeur", async () => { const f = await fixture({values: new Map([[1, A]])}); f.mode = "hold"; await f.capture(); await f.tick(5000); assert.equal(f.ports[0].disconnected, true); await f.tick(1000); f.mode = "throw"; await f.capture(); await flush(); await f.tick(1000); f.mode = "ok"; assert.equal((await capture(f)).result, "OK"); assert.equal(f.ports.at(-1).disconnected, false); }); test("collision persistée refuse lot entier et collision générée refuse le mint", async () => { const f = await fixture({ids: [1, 2], values: new Map([[1, A], [2, A]])}); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.maps.length, 0); const g = await fixture({values: new Map([[1, A]]), random: () => A}); await ready(g); g.open(2); await g.tick(4000); assert.equal(g.writes.length, 0); }); test("tab move, ordre et fenêtre restante ne récupèrent aucune identité", async () => { const f = await fixture({ids: [9, 2], values: new Map([[9, A], [2, B]])}); assert.equal((await capture(f)).result, "OK"); f.ids.reverse(); assert.equal((await capture(f)).result, "OK"); assert.deepEqual(f.maps.map(m => m.uuid), [B, A, B, A]); f.values.delete(2); f.close(9); assert.equal((await capture(f)).result, "REFUSED"); await f.tick(35000); assert.equal(f.writes.length, 0); }); test("update/restart : jamais enrôler baseline absente, mêmes valeurs autoritaires", async () => { for (const reason of ["update", "browser_update", undefined]) { const f = await fixture(); if (reason) f.install(reason); await f.tick(70000); assert.equal(f.writes.length, 0); } const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(4000); const g = await fixture({ids: [8], values: new Map([[8, f.values.get(2)]])}); assert.equal((await capture(g)).result, "OK"); assert.equal(g.writes.length, 0); }); test("window close avant mint : seul le candidat remplaçant est réobservé", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(1000); f.close(2); f.open(3); await f.tick(10000); assert.deepEqual(f.writes.map(w => w.id), [3]); }); test("écriture appliquée mais rejetée : pas de retry ni capture concurrente partielle", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.write = (id, value) => { f.values.set(id, value); throw new Error("uncertain"); }; f.open(2); await f.tick(1000); await f.capture(); assert.equal((await f.captureResult).result, "BUSY"); await f.tick(8000); assert.equal(f.writes.length, 1); assert.equal((await capture(f)).result, "OK"); assert.equal(f.writes.length, 1); }); test("API suspendue : arrêt définitif, pas d'accumulation d'appels", async () => { const f = await fixture({read: () => new Promise(() => {})}); await f.capture(); await f.tick(40000); const count = f.reads.length; await f.capture(); await f.tick(10000); assert.equal(f.reads.length, count); assert.equal(f.writes.length, 0); assert.equal((await f.status()).result, "REFUSED"); }); test("valeurs invalides et limites locales refusent sans fausse réussite", async () => { for (const invalid of [null, "", "wrong", 42, {uuid: A}]) { const f = await fixture({values: new Map([[1, invalid]]), install: true}); await f.tick(35000); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.writes.length, 0); } const f = await fixture({ids: Array.from({length: 257}, (_, i) => i)}); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.reads.length, 0); }); test("capture seule reste non-mutante, absence tardive récupérée hors délai", async () => { const f = await fixture({read: (_, now) => now >= 7000 ? A : undefined}); assert.equal((await capture(f)).result, "REFUSED"); await f.tick(6000); assert.equal((await capture(f)).result, "OK"); assert.equal(f.writes.length, 0); }); test("une lecture rejetée ne libère pas l'opération tant que sa sœur est pendante", async () => { const f = await fixture({ids: [1, 2], read: id => { if (id === 1) throw new Error("reject"); return new Promise(() => {}); }}); await f.capture(); const count = f.reads.length; await f.capture(); assert.equal(f.reads.length, count); await f.tick(10000); assert.equal(f.reads.length, count); assert.equal(f.writes.length, 0); }); test("valeur tardive à la dernière relecture avant mint est préservée", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); let finalReads = 0; f.read = (id, now) => id === 1 ? A : now >= 36000 && ++finalReads >= 2 ? B : undefined; f.open(2); await f.tick(5000); assert.equal(f.writes.length, 0); assert.equal((await capture(f)).result, "OK"); }); test("immuabilité entre opérations : changement UUID refuse publication entière", async () => { const f = await fixture({values: new Map([[1, A]])}); assert.equal((await capture(f)).result, "OK"); f.values.set(1, B); const before = f.maps.length; assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.maps.length, before); await f.tick(35000); assert.equal(f.writes.length, 0); }); test("API suspendue pendant écriture : aucun retry, aucune capture d'identité partielle", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); let release; f.write = () => new Promise(resolve => { release = resolve; }); f.open(2); await f.tick(6000); assert.equal(f.writes.length, 1); assert.equal((await capture(f)).result, "REFUSED"); f.values.set(2, f.writes[0].value); release(); await f.tick(10000); assert.equal(f.writes.length, 1); assert.equal((await capture(f)).result, "REFUSED"); }); test("restart pendant observation perd toute autorisation de mint", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(1000); const g = await fixture({ids: [...f.ids], values: f.values}); await g.tick(70000); assert.equal(g.writes.length, 0); assert.equal((await capture(g)).result, "REFUSED"); }); test("rejeter une lecture baseline au premier install la protège définitivement", async () => { const f = await fixture({install: true, read: () => { throw new Error("read rejected"); }}); await f.tick(2000); f.read = null; await f.tick(40000); assert.equal(f.writes.length, 0); assert.equal((await capture(f)).result, "REFUSED"); }); test("fenêtre sessionId ambiguë bloque capture mais pas mint du Ctrl+N suivant", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.changed(); f.open(2, {sessionId: "restored"}); await f.tick(6000); f.open(3); await f.tick(10000); assert.deepEqual(f.writes.map(w => w.id), [3]); assert.equal((await f.status()).result, "REFUSED"); }); test("capture native avant fin d'installation n'attribue aucune identité", async () => { const f = await fixture({install: true}); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.writes.length, 0); await f.tick(31000); assert.equal(f.writes.length, 0); }); test("message historique create-window supprimé : aucune création et aucune attribution", async () => { const f = await fixture(); assert.equal(f.rawMessage({op: "create-window"}, {id: "session-v2@labfy.org"}), undefined); assert.equal(f.creates, 0); assert.equal(f.writes.length, 0); }); test("vérification divergente après écriture refuse aussi les publications suivantes", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.write = id => { f.values.set(id, B); }; f.open(2); await f.tick(5000); assert.equal(f.writes.length, 1); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.maps.length, 0); assert.equal(f.values.get(2), B); await f.tick(5000); assert.equal(f.writes.length, 1); }); test("listener synchrone : onCreated pendant le premier getAll reste en quarantaine", async () => { const f = await fixture({install: true, enumerate: state => { state.enumerate = null; state.open(2); }}); await f.tick(35000); assert.equal(f.writes.length, 0); f.values.set(2, B); await f.tick(5000); assert.deepEqual(f.writes.map(w => w.id), []); assert.equal(f.values.get(2), B); }); test("une preuve lue dans un lot partiellement rejeté ne peut devenir un mint", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); f.read = id => { if (id === 1) throw new Error("sibling rejection"); return B; }; await f.capture(); assert.equal((await f.captureResult).result, "REFUSED"); f.read = null; await f.tick(10000); assert.equal(f.writes.length, 0); }); test("une valeur invalide observée puis disparue ne permet jamais de mint", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); f.values.set(2, "corrupt"); await f.capture(); assert.equal((await f.captureResult).result, "REFUSED"); f.values.delete(2); await f.tick(10000); assert.equal(f.writes.length, 0); }); test("rejets non Error : refus explicite, sans exception dans le gestionnaire d'erreur", async () => { for (const rejected of [null, "read failed"]) { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.read = () => Promise.reject(rejected); f.open(2); await f.tick(2000); const status = await f.status(); assert.equal(status.result, "REFUSED"); assert.equal(status.reason, "API_REJECTED"); assert.equal((await capture(f)).result, "REFUSED"); assert.equal(f.writes.length, 0); } }); // Audit corrective : attentes produit, exécutables aussi contre la source pré-patch. test("audit BOOT : Ctrl+N à 10 secondes puis première capture automatique", async () => { const f = await fixture({values: new Map([[1, A]])}); await f.tick(10000); f.open(2); await f.tick(25000); assert.deepEqual(f.writes.map(w => w.id), [2]); assert.equal((await capture(f)).result, "OK"); }); test("audit historique absent : ne bloque pas les futurs Ctrl+N même après 60s", async () => { const f = await fixture(); f.install("update"); await f.tick(70000); assert.equal((await f.status()).result, "REFUSED"); f.open(2); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2]); assert.equal((await capture(f)).result, "REFUSED"); f.close(1); f.open(3); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2, 3]); assert.equal((await capture(f)).result, "OK"); }); test("audit fermeture onglet : observation retentée puis Ctrl+N suivant", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(1500); f.changed(); await f.tick(12000); assert.deepEqual(f.writes.map(w => w.id), [2]); f.open(3); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2, 3]); }); test("audit deux Ctrl+N espacés de 1s : mêmes candidats retentés", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2); await f.tick(1000); f.open(3); await f.tick(8000); assert.deepEqual(f.writes.map(w => w.id), [2, 3]); assert.equal((await capture(f)).result, "OK"); }); test("audit type facultatif : getAll normal confirme le candidat", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2, {type: undefined}); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2]); }); test("audit réinstallation avec checkpoint passé : install seul ne permet aucun mint baseline", async () => { // Le checkpoint externe est invisible à cette VM comme au background ; // aucune autorité backend d'absence n'a été reçue. const f = await fixture({install: true}); await f.tick(70000); assert.equal(f.writes.length, 0); assert.equal((await f.status()).result, "REFUSED"); }); test("timeout titlePreface : nettoyage unique après règlement tardif sans relancer le marquage", async () => { const f = await fixture({values: new Map([[1, A]])}); let release; f.update = (_, change) => change.titlePreface ? new Promise(resolve => { release = resolve; }) : undefined; await f.capture(); await f.tick(3000); assert.equal((await f.captureResult).result, "REFUSED"); assert.equal(f.updates.filter(u => u.titlePreface).length, 1); const before = f.updates.length; release(); await f.tick(10000); assert.equal(f.updates.length, before + 1); assert.equal(f.updates.at(-1).titlePreface, ""); assert.equal((await f.status()).result, "REFUSED"); assert.equal((await f.status()).prefixCleanupPending, false); await f.tick(10000); assert.equal(f.updates.length, before + 1); }); test("popup explicite et type absent hors getAll normal : aucun mint", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.open(2, {type: "popup"}); f.ids = [1]; f.open(3, {type: undefined}); f.ids = [1]; await f.tick(10000); assert.equal(f.writes.length, 0); }); test("update : UUID baseline reçu tardivement est lu sans réécriture", async () => { const f = await fixture(); f.install("update"); await f.tick(70000); f.open(2); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2]); f.values.set(1, A); await f.tick(3000); assert.equal((await capture(f)).result, "OK"); assert.deepEqual(f.writes.map(w => w.id), [2]); assert.equal((await f.status()).result, "READY"); }); test("activité après écriture : ne retente pas l'écriture, conserve les autres candidats", async () => { const f = await fixture({values: new Map([[1, A]])}); await ready(f); f.write = (id, value) => { f.values.set(id, value); if (id === 2) f.open(3); }; f.open(2); await f.tick(10000); assert.deepEqual(f.writes.map(w => w.id), [2, 3]); assert.equal((await capture(f)).result, "OK"); }); // P0 : doit échouer contre le background pré-protocole conservé en preuve. test("P0 premier install avec absence attestée : deux UUID stables sans clic", async () => { for (const early of [true, false]) { const f = await fixture({ids: [1, 2], bootstrap: true, install: early}); if (!early) { await f.tick(35000); f.install(); } await f.tick(45000); assert.deepEqual(f.writes.map(w => w.id), [1, 2]); assert.equal(new Set(f.writes.map(w => w.value)).size, 2); assert.equal(f.bootstraps.length, 2); // Recheck avant chacune des écritures. await f.tick(60000); assert.equal(f.writes.length, 2); assert.equal((await capture(f)).result, "OK"); } }); test("bootstrap refus/timeout/erreur : baseline isolée, Ctrl+N BOOT et futur automatiques", async () => { for (const bootstrap of [false, "hold", "throw", "disconnect", "invalid"]) { const f = await fixture({install: true, bootstrap}); await f.tick(10000); f.open(2); await f.tick(40000); assert.deepEqual(f.writes.map(w => w.id), [2], String(bootstrap)); assert.equal(f.bootstraps.length, 1); f.bootstrap = true; await f.tick(20000); // La reconnexion n'est pas une nouvelle autorité. f.open(3); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2, 3]); assert.equal(f.bootstraps.length, 1); assert.equal(f.values.has(1), false); } }); test("bootstrap : checkpoint créé entre deux écritures refuse la seconde", async () => { const f = await fixture({ids: [1, 2], install: true, bootstrap: true}); f.write = (id, value) => { f.values.set(id, value); f.bootstrap = false; }; await f.tick(45000); assert.deepEqual(f.writes.map(w => w.id), [1]); assert.equal(f.bootstraps.length, 2); assert.equal(f.values.has(2), false); await f.tick(60000); assert.equal(f.bootstraps.length, 2); }); test("bootstrap en vol : capture coalescée, pas de mélange avec map-window", async () => { const f = await fixture({install: true, bootstrap: "hold"}); await f.tick(33000); assert.equal(f.bootstraps.length, 1); await f.capture(); assert.equal((await f.captureResult).result, "BUSY"); assert.equal(f.maps.length, 0); f.ports.at(-1).onMessage.emit({result: "OK", baseline_enrollment_allowed: true}); await f.tick(1000); assert.equal(f.writes.length, 1); assert.equal((await capture(f)).result, "OK"); assert.equal(f.maps.length, 1); }); test("capture en vol retarde bootstrap ; onInstalled ne poste rien en concurrence", async () => { const f = await fixture({values: new Map([[1, A]]), bootstrap: true}); f.mode = "hold"; await f.capture(); f.install(); await f.tick(3000); assert.equal(f.bootstraps.length, 0); assert.equal(f.maps.length, 1); f.ports.at(-1).onMessage.emit({result: "OK", uuid: A, con_id: 42}); f.mode = "ok"; await f.tick(40000); assert.equal(f.writes.length, 0); assert.equal(f.bootstraps.length, 0); }); test("bootstrap réponse positive après onChanged : lot révoqué sans retry baseline", async () => { const f = await fixture({install: true, bootstrap: "hold"}); await f.tick(33000); assert.equal(f.bootstraps.length, 1); f.changed(); f.ports.at(-1).onMessage.emit({result: "OK", baseline_enrollment_allowed: true}); await f.tick(30000); assert.equal(f.writes.length, 0); assert.equal(f.bootstraps.length, 1); f.open(2); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2]); }); test("bootstrap : UUID arrivé pendant attestation préservé, aucun overwrite", async () => { const f = await fixture({install: true, bootstrap: "hold"}); await f.tick(33000); f.values.set(1, A); f.ports.at(-1).onMessage.emit({result: "OK", baseline_enrollment_allowed: true}); await f.tick(20000); assert.equal(f.writes.length, 0); assert.equal(f.values.get(1), A); assert.equal((await capture(f)).result, "OK"); }); test("bootstrap : rollback UUID connu et écriture incertaine jamais retentés", async () => { const f = await fixture({install: true, bootstrap: true}); f.write = (id, value) => { f.values.set(id, value); throw new Error("uncertain baseline"); }; await f.tick(45000); assert.equal(f.writes.length, 1); f.values.delete(1); await f.tick(20000); assert.equal(f.writes.length, 1); assert.equal(f.bootstraps.length, 1); assert.equal((await capture(f)).result, "REFUSED"); }); test("bootstrap : restart et update restent sans autorité malgré host positif", async () => { for (const reason of [undefined, "update", "browser_update"]) { const f = await fixture({bootstrap: true}); if (reason) f.install(reason); await f.tick(45000); assert.equal(f.writes.length, 0); assert.equal(f.bootstraps.length, 0); f.open(2); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2]); } }); test("bootstrap : événement pré-photographie/sessionId protège même avec host positif", async () => { const f = await fixture({install: true, bootstrap: true, enumerate: state => { state.enumerate = null; state.open(2); }}); f.open(1, {sessionId: "restored"}); await f.tick(45000); assert.equal(f.writes.length, 0); assert.equal(f.bootstraps.length, 0); }); test("bootstrap : timeout déconnecte, réponse ancien port ignorée, map reconnecté", async () => { const f = await fixture({install: true, bootstrap: "hold"}); await f.tick(33000); const old = f.ports.at(-1); await f.tick(10000); assert.equal(old.disconnected, true); old.onMessage.emit({result: "OK", baseline_enrollment_allowed: true}); f.values.set(1, A); f.bootstrap = true; assert.equal((await capture(f)).result, "OK"); assert.equal(f.writes.length, 0); assert.equal(f.bootstraps.length, 1); assert.notEqual(f.ports.at(-1), old); }); test("bootstrap : réponses natives non exactes refusées sans écriture baseline", async () => { for (const answer of [{result: "REFUSED"}, {result: "OK"}, {result: "OK", baseline_enrollment_allowed: true, extra: true}, {result: "OK", baseline_enrollment_allowed: 1}]) { const f = await fixture({install: true}); f.bootstrapHook = p => p.onMessage.emit(answer); await f.tick(45000); assert.equal(f.writes.length, 0); assert.equal(f.bootstraps.length, 1); f.open(2); await f.tick(6000); assert.deepEqual(f.writes.map(w => w.id), [2]); } });