#!/usr/bin/env node // Deterministic browser-coordinator regression without a production browser or network. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { runInNewContext } from "node:vm"; import { fileURLToPath } from "node:url"; import { resolve, dirname } from "node:path"; const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const scheduler = readFileSync(resolve(root, "src/admin/web/app/refresh.js"), "utf8"); const core = readFileSync(resolve(root, "src/admin/web/app/core.js"), "utf8"); const bridge = core.slice(core.indexOf("function renderLiveStatus(value)"), core.indexOf("async function refreshCertificateInventory()")); assert.match(bridge, /function startLive\(/); function fixture({ withoutSse = false } = {}) { let time = 0, nextId = 1, renderCount = 0, logCount = 0, diagCount = 0; const timers = new Map(), listeners = new Map(), windowListeners = new Map(), streams = [], requests = [], rendered = [], failures = []; let response = async () => ({}); class FakeEventSource { constructor(url) { this.url = url; this.closed = false; this.handlers = new Map(); streams.push(this); } addEventListener(type, callback) { this.handlers.set(type, callback); } close() { this.closed = true; } emit(type, value) { this.handlers.get(type)?.({ data: JSON.stringify(value) }); } fail() { this.onerror?.({ type: "error" }); } } const nodes = { "connection-label": { textContent: "" } }; const document = { hidden: false, addEventListener(type, callback) { assert(!listeners.has(type)); listeners.set(type, callback); }, removeEventListener(type, callback) { assert.equal(listeners.get(type), callback); listeners.delete(type); } }; const ctx = { document, AbortController, URLSearchParams, EventSource: withoutSse ? undefined : FakeEventSource, setTimeout(fn, ms) { const id = nextId++; timers.set(id, { at: time + ms, fn }); return id; }, clearTimeout(id) { timers.delete(id); }, console: { warn(...args) { failures.push(args); } }, authenticated: true, authEpoch: 1, currentPanel: "overview", liveConnected: false, liveFallback: false, liveEvents: null, liveInterval: null, latestStatus: null, window: { rustProxySettings: { current: () => "listeners", render: () => undefined }, addEventListener(type, callback) { assert(!windowListeners.has(type)); windowListeners.set(type, callback); }, removeEventListener(type, callback) { assert.equal(windowListeners.get(type), callback); windowListeners.delete(type); }, }, $: name => nodes[name] || null, t: name => name, api: (url, args = {}) => { requests.push({ url, args, time }); return response(url, args); }, renderStatus(value) { renderCount++; rendered.push(value); }, renderLogs() { logCount++; }, renderDiagnostics() { diagCount++; }, renderLiveStatus() { throw new Error("bridge not loaded"); }, revalidateSession: async () => { ctx.window.rustProxyRefresh.suspendForSessionValidation(); await Promise.resolve(); ctx.window.rustProxyRefresh.completeSessionValidation(true); }, loadTunnels: async () => undefined }; runInNewContext(bridge + "\n" + scheduler, ctx, { filename: "coordinator-browser-bundle.js" }); const refresh = ctx.window.rustProxyRefresh; const settled = async () => { for (let i = 0; i < 12; i++) await Promise.resolve(); }; async function advance(ms = 0) { const until = time + ms; for (let i = 0; i < 1000; i++) { const due = [...timers.entries()].filter(([, timer]) => timer.at <= until).sort((a, b) => a[1].at - b[1].at)[0]; if (!due) { time = until; await settled(); return; } const [id, timer] = due; time = timer.at; timers.delete(id); timer.fn(); await settled(); } throw new Error("unbounded polling timer loop"); } function switchPanel(name) { ctx.currentPanel = name; refresh.setPanel(name); } function changeVisibility(hidden) { document.hidden = hidden; listeners.get("visibilitychange")?.(); } function count(url) { return requests.filter(item => item.url.startsWith(url)).length; } function registerViews() { for (const name of ["traffic", "cache"]) { refresh.register(name, { interval: () => refresh.current()[`${name}_seconds`], active: () => ctx.currentPanel === name, run: async ({ signal, isCurrent }) => { const value = await ctx.api(`/api/${name}`, { signal }); if (isCurrent()) rendered.push({ name, value }); } }); } } return { ctx, refresh, requests, rendered, streams, listeners, windowListeners, timers, failures, advance, switchPanel, count, registerViews, changeVisibility, setResponse(fn) { response = fn; }, get renderCount() { return renderCount; }, get logCount() { return logCount; }, get diagCount() { return diagCount; }, }; } const h = fixture(); h.registerViews(); assert.equal(h.refresh.current().overview_seconds, 3); assert.equal(h.refresh.current().system_info_seconds, 60); assert.equal(h.refresh.current().traffic_seconds, 5); assert.equal(h.refresh.current().cache_seconds, 5); assert.equal(h.refresh.current().live_enabled, true); assert.equal(h.streams.length, 0); h.refresh.start(); await h.advance(); assert.equal(h.streams.length, 1, "one shared SSE connection"); assert.equal(h.streams[0].url, "/api/events?interval=3"); assert.equal(h.count("/api/traffic"), 0, "no Traffic on Overview"); assert.equal(h.count("/api/cache"), 0, "no Cache on Overview"); assert.equal(h.count("/api/system-info"), 1, "Overview uses slow diagnostics"); assert.equal(h.count("/api/status"), 0, "no duplicate status polling while SSE connects"); assert.equal(h.count("/api/logs"), 0); await h.advance(10_000); assert.equal(h.count("/api/system-info"), 1, "diagnostics not collected at live interval"); h.switchPanel("traffic"); await h.advance(); assert.equal(h.count("/api/traffic"), 1); assert.equal(h.count("/api/cache"), 0); for (let i = 0; i < 20; i++) { h.switchPanel("overview"); h.switchPanel("traffic"); } assert.equal(h.refresh.diagnostics().filter(row => row.id === "traffic").length, 1); await h.advance(0); assert.equal(h.count("/api/traffic"), 2, "tab switches do not multiply immediate requests"); await h.advance(15_000); assert.equal(h.count("/api/traffic"), 5, "one request each 5 seconds"); assert.equal(h.count("/api/cache"), 0); h.refresh.update({ traffic_seconds: 3, cache_seconds: 0, system_info_seconds: 0, overview_seconds: 5 }); assert(h.streams[0].closed, "SSE is replaced when the administrator updates their interval"); assert.equal(h.streams.at(-1).url, "/api/events?interval=5"); await h.advance(); assert.equal(h.count("/api/traffic"), 6); await h.advance(6_000); assert.equal(h.count("/api/traffic"), 8); h.switchPanel("cache"); await h.advance(20_000); assert.equal(h.count("/api/cache"), 0, "Cache manual mode cancels polling"); await h.refresh.manual("cache"); assert.equal(h.count("/api/cache"), 1, "manual Cache works with interval=0"); await h.advance(40_000); assert.equal(h.count("/api/cache"), 1); assert.equal(h.count("/api/traffic"), 8, "Traffic is stopped in Cache view"); // SSE failures activate both fallbacks and a recovered snapshot switches them off. h.streams.at(-1).fail(); await h.advance(); assert.equal(h.count("/api/status?view=live"), 1); assert.equal(h.count("/api/logs"), 1); await h.advance(5_000); assert.equal(h.count("/api/status?view=live"), 2); h.streams.at(-1).emit("snapshot", { status: { metrics: { requests: 5 } }, logs: { entries: [] } }); assert.equal(h.ctx.liveConnected, true); await h.advance(20_000); assert.equal(h.count("/api/status?view=live"), 2, "SSE recovery stops fallback polling"); assert.equal(h.count("/api/logs"), 2); assert.equal(h.renderCount, 3, "two fallback status renders followed by recovered SSE"); // Hidden browser tabs pause even previously active sources, then resume cleanly. h.refresh.update({ traffic_seconds: 3, cache_seconds: 3, system_info_seconds: 15 }); h.switchPanel("traffic"); await h.advance(); const beforeHidden = h.count("/api/traffic"); h.changeVisibility(true); assert(h.streams.at(-1).closed, "SSE closes when hidden"); assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0); await h.advance(120_000); assert.equal(h.count("/api/traffic"), beforeHidden); h.changeVisibility(false); await h.advance(); await h.advance(); assert.equal(h.count("/api/traffic"), beforeHidden + 1); assert.equal(h.streams.filter(stream => !stream.closed).length, 1); // Off pauses all scheduled work, but retains explicit manual requests. h.refresh.update({ live_enabled: false, traffic_seconds: 3 }); const requestsBeforeOff = h.requests.length; await h.advance(120_000); assert.equal(h.requests.length, requestsBeforeOff); await h.refresh.manual("traffic"); assert.equal(h.count("/api/traffic"), beforeHidden + 2); assert.equal(h.streams.filter(stream => !stream.closed).length, 0); h.refresh.update({ live_enabled: true, traffic_seconds: 3 }); await h.advance(); assert.equal(h.streams.filter(stream => !stream.closed).length, 1); // Aborting an old view prevents the obsolete response from modifying UI. let resolveDelayed; h.setResponse(url => url === "/api/traffic" ? new Promise(resolve => { resolveDelayed = resolve; }) : Promise.resolve({ ok: true })); await h.advance(3_000); const staleCount = h.rendered.length; const request = h.requests.at(-1); assert.equal(request.url, "/api/traffic"); h.switchPanel("cache"); assert.equal(request.args.signal.aborted, true); resolveDelayed({ stale: true }); await h.advance(); assert.equal(h.rendered.filter(v => v.name === "traffic").length, h.rendered.slice(0, staleCount).filter(v => v.name === "traffic").length); assert.equal(h.count("/api/cache"), 2); // Manual diagnostics still work when their own automatic interval is zero. h.refresh.update({ live_enabled: false, system_info_seconds: 0 }); const diagnosticRequests = h.count("/api/system-info"); await h.refresh.manual("system-info"); assert.equal(h.count("/api/system-info"), diagnosticRequests + 1); h.refresh.update({ live_enabled: true }); // A failed source does not stop other source registrations. const e = fixture({ withoutSse: true }); e.registerViews(); e.setResponse(url => url === "/api/traffic" ? Promise.reject(new Error("Traffic disconnected")) : Promise.resolve({ ok: true })); e.refresh.start(); e.switchPanel("traffic"); await e.advance(); assert.equal(e.count("/api/traffic"), 1); assert.equal(e.count("/api/status?view=live"), 1, "browsers without EventSource use status polling"); assert.equal(e.count("/api/logs"), 1, "browsers without EventSource use logs polling"); e.switchPanel("cache"); await e.advance(); assert.equal(e.count("/api/cache"), 1); assert.equal(e.failures.length, 1); await e.advance(5_000); assert.equal(e.count("/api/cache"), 2); // Multiple registrations with identical IDs must be rejected. assert.throws(() => h.refresh.register("traffic", { interval: () => 5, run: async () => {} }), /Duplicate refresh task/); // Logout clears timers, active SSE streams, observer and pending fetches. h.refresh.stop(); assert.equal(h.listeners.has("visibilitychange"), false); assert.equal(h.windowListeners.size, 0, "pagehide/pageshow listeners are removed on logout"); assert.equal(h.streams.filter(stream => !stream.closed).length, 0); assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0); const afterLogout = h.requests.length; h.ctx.authenticated = false; await h.advance(600_000); assert.equal(h.requests.length, afterLogout); await h.refresh.manual("traffic"); assert.equal(h.requests.length, afterLogout); // Validate incoming preferences in the browser, including malformed partial records. const invalid = runInNewContext(scheduler + ";normalizeRefreshPreferences({ live_enabled: 1, overview_seconds: 0, traffic_seconds: 17, cache_seconds: 0, system_info_seconds: 300 });", { document: { hidden: true, addEventListener() {}, removeEventListener() {} }, AbortController, window: {}, authenticated: false, authEpoch: 0, currentPanel: "overview", liveConnected: false, liveFallback: false, stopLive() {}, startLive() {}, $: () => null }); assert.equal(invalid.live_enabled, true); assert.equal(invalid.overview_seconds, 3); assert.equal(invalid.traffic_seconds, 5); assert.equal(invalid.cache_seconds, 0); assert.equal(invalid.system_info_seconds, 300); console.log("PASS: per-account refresh validation, SSE/fallback, 5/3/0-s scheduling, tab/visibility/logout lifecycle, isolation, no duplicate/inflight/stale requests, manual mode.");