This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 20:56:36 +02:00
1 parent f213d853e2
commit 76b8bb7ee2
78 files changed
+7750 -5204

No files matched your search

+229
View File
@@ -0,0 +1,229 @@
#!/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(), 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 } },
$: 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 () => undefined,
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, 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, 2);
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=2");
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();
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.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, 2);
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.");