Files
RustProxy/tools/checks/overview_extra_metrics_0323_ui_test.mjs
T
2026-10-14 11:51:37 +02:00

97 lines
5.3 KiB
JavaScript

// Existing status snapshots only: no API calls, bounded histories and honest no-data.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const source = fs.readFileSync('src/admin/web/app/overview-extra-metrics.js', 'utf8');
const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8'));
const ids = registry.filter(widget => widget.category === 'performance').map(widget => widget.id);
assert.equal(ids.length, 18);
const widgets = new Map(ids.map(id => [id, { hidden: false }]));
const fields = new Map();
const field = id => {
if (!fields.has(id)) fields.set(id, { textContent: '-', points: '', setAttribute(name, value) { this[name] = value; } });
return fields.get(id);
};
let now = 1000;
const context = {
window: {}, currentPanel: 'overview', performance: { now: () => now },
t: key => key, fmt: (v, places = 0) => Number(v).toFixed(places),
fmtBytes: v => `${v} B`, $: field,
document: { querySelector: selector => widgets.get(selector.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null },
};
vm.createContext(context);
vm.runInContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'), context);
vm.runInContext(source, context);
const render = context.window.rustProxyExtraOverview.render;
const text = id => field(`overview-extra-${id}`).textContent;
const points = id => field(`overview-extra-${id}-line`).points;
const start = {
metrics: { requests: 100, http1_requests: 70, http2_requests: 30, http3_requests: 0, tls_handshake_errors: 3, errors: 10, active_connections: 5, active: 2, websocket_active: 1, mean_duration_ms: 25 },
system: { process_cpu_percent: 12, process_rss_bytes: 1000, open_fds: 10, max_fds: 500 },
cache: { hits: 10, misses: 10, resident_bytes: 10, hot_limit_bytes: 100 },
backend_pools: [{ members: [{status: 'up'}, {status: 'down'}] }],
};
render(start);
assert.equal(text('extra_process_cpu'), '12.0%');
assert.equal(text('extra_process_rss'), '1000 B');
assert.equal(text('extra_backend_unhealthy'), '1');
for (const id of ['extra_tls_error_rate','extra_request_error_rate','extra_cache_hit_rate','extra_h2_share'])
assert.equal(text(id), '-', `initial ${id} delta must not be invented`);
now = 3000;
const next = structuredClone(start);
next.metrics = { ...start.metrics, requests: 130, http1_requests: 84, http2_requests: 45, http3_requests: 1, tls_handshake_errors: 5, errors: 16, active_connections: 6, active: 4, websocket_active: 3, mean_duration_ms: 40 };
next.system = { ...start.system, process_cpu_percent: 22, process_rss_bytes: 2000, open_fds: 20 };
next.cache = { ...start.cache, hits: 20, misses: 15, resident_bytes: 25 };
next.backend_pools[0].members[0].status = 'down';
render(next);
assert.equal(text('extra_tls_error_rate'), '1.0/s');
assert.equal(text('extra_request_error_rate'), '3.0/s');
assert.equal(text('extra_cache_hit_rate'), '66.7%');
assert.equal(text('extra_h2_share'), '50.0%');
assert.equal(text('extra_cache_memory'), '25.0%');
assert.equal(text('extra_response_latency'), '40.0 ms');
assert.equal(text('extra_backend_unhealthy'), '2');
assert.equal(text('extra_total_rps'), '15.0/s');
assert.equal(text('extra_active_exchanges'), '4');
assert.equal(text('extra_h1_share'), '46.7%');
assert.equal(text('extra_h3_share'), '3.3%');
assert.equal(text('extra_cache_miss_rate'), '2.5/s');
assert.equal(text('extra_backend_active'), '0');
assert.equal(points('extra_process_cpu').trim().split(/\s+/).length, 2);
// Hiding a chart stops sampling; stale cumulative counters are never used as an instant rate.
widgets.get('extra_tls_error_rate').hidden = true;
now = 5000;
render({ ...next, metrics: { ...next.metrics, tls_handshake_errors: 50 } });
assert.equal(text('extra_tls_error_rate'), '1.0/s');
widgets.get('extra_tls_error_rate').hidden = false;
now = 6000;
render({ ...next, metrics: { ...next.metrics, tls_handshake_errors: 60 } });
assert.equal(text('extra_tls_error_rate'), '-', 'first fresh visible sample establishes a new baseline');
// After a reset, don't return a negative rate or denominator percentage.
now = 7000;
render({ ...next, metrics: { ...next.metrics, requests: 1, http2_requests: 0, tls_handshake_errors: 0, errors: 0 }, cache: { ...next.cache, hits: 0, misses: 0 } });
assert.equal(text('extra_h2_share'), '-');
assert.equal(text('extra_request_error_rate'), '-');
assert.equal(text('extra_tls_error_rate'), '-');
// Explicit unavailability is not represented as a measured zero.
now = 8000;
render({ metrics: {}, system: {}, cache: {} });
assert.equal(text('extra_process_cpu'), '-');
assert.equal(text('extra_websocket_active'), '-');
assert.equal(text('extra_backend_unhealthy'), '-');
context.window.rustProxyExtraOverview.reset();
now = 9000;
for (let index = 0; index < 50; index++) {
now += 1000;
render({ ...next, system: { ...next.system, process_cpu_percent: index } });
}
assert.equal(points('extra_process_cpu').trim().split(/\s+/).length, 30, 'bounded history');
context.currentPanel = 'vhosts';
const pointBefore = points('extra_process_cpu');
now += 1000;
render(next);
assert.equal(points('extra_process_cpu'), pointBefore, 'no extra sampling outside Overview');
assert.match(source, /if \(!tile \|\| tile.hidden\)/);
console.log('PASS: 18 extra metric series, time-based deltas, no-data, counter resets, hidden opt-ins and 30-sample bounds');