Files
Alex B 893773338e chore(tests): move root test-*.js into tests/unit and tests/e2e (#2036)
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.

Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.

Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.

Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.

Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
2026-09-17 19:06:07 +02:00

108 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Unit tests for perf.js anomaly detection — 5-minute rolling baseline.
*
* Issue #1120 acceptance criterion: "Per-component write rate > 10× steady-state
* baseline" flagged with ⚠️. The baseline must be a 5-minute rolling window,
* not a single sample-to-sample comparison (which gives false negatives during
* a slow ramp and false positives during natural bursts).
*
* This file exercises window.detectPerfAnomalies(history, current, opts).
*/
'use strict';
const vm = require('vm');
const fs = require('fs');
const code = fs.readFileSync('public/perf.js', 'utf8');
const ctx = {
window: {},
document: { addEventListener() {}, getElementById() { return null; }, hidden: true },
console,
fetch: () => Promise.resolve({ json: () => Promise.resolve(null) }),
setInterval: () => 0,
clearInterval: () => {},
registerPage: () => {},
};
vm.createContext(ctx);
vm.runInContext(code, ctx);
const detect = ctx.window.detectPerfAnomalies;
if (typeof detect !== 'function') {
console.log('FAIL: window.detectPerfAnomalies is not a function (got ' + typeof detect + ')');
process.exit(1);
}
let pass = 0, fail = 0;
function test(name, fn) {
try { fn(); pass++; console.log(' ✅ ' + name); }
catch (e) { fail++; console.log(' ❌ ' + name + ': ' + e.message); }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || 'assertion failed'); }
// Build a 5-minute history where backfill_path_json increments at a steady
// 1/sec baseline (300 samples over 300s), tx_inserted at 5/sec.
function buildHistory(startMs, durSec, perSec) {
const h = [];
let cum = {};
for (const k of Object.keys(perSec)) cum[k] = 0;
for (let i = 0; i <= durSec; i++) {
const ts = new Date(startMs + i * 1000).toISOString();
const snap = { sampleAt: ts, sources: {} };
for (const k of Object.keys(perSec)) {
cum[k] += perSec[k];
snap.sources[k] = cum[k];
}
h.push(snap);
}
return h;
}
test('⚠️ fires when backfill rate hits 11× the 5-minute baseline', () => {
const t0 = Date.UTC(2026, 5, 5, 0, 0, 0);
const history = buildHistory(t0, 300, { backfill_path_json: 1, tx_inserted: 5 });
// Now a fresh sample at t0+301s where backfill_path_json jumped from 300→311
// (11/sec over 1s), tx_inserted continues at 5/sec.
const last = history[history.length - 1];
const current = {
sampleAt: new Date(t0 + 301 * 1000).toISOString(),
sources: {
backfill_path_json: last.sources.backfill_path_json + 11,
tx_inserted: last.sources.tx_inserted + 5,
},
};
const r = detect(history, current, { windowMs: 5 * 60 * 1000, factor: 10 });
assert(r && r.flags, 'expected result with flags map');
assert(r.flags.backfill_path_json === true,
'expected backfill_path_json flagged at 11× baseline, got flags=' + JSON.stringify(r.flags) +
' rates=' + JSON.stringify(r.rates) + ' baselines=' + JSON.stringify(r.baselineRates));
});
test('no flag at 5× baseline (under threshold)', () => {
const t0 = Date.UTC(2026, 5, 5, 0, 0, 0);
const history = buildHistory(t0, 300, { backfill_path_json: 2, tx_inserted: 5 });
const last = history[history.length - 1];
const current = {
sampleAt: new Date(t0 + 301 * 1000).toISOString(),
sources: {
backfill_path_json: last.sources.backfill_path_json + 10, // 10/sec vs 2/sec baseline = 5×
tx_inserted: last.sources.tx_inserted + 5,
},
};
const r = detect(history, current, { windowMs: 5 * 60 * 1000, factor: 10 });
assert(!r.flags.backfill_path_json,
'expected no flag at 5× baseline, got ' + JSON.stringify(r.flags));
});
test('no flag without enough history (< 30s of samples)', () => {
const t0 = Date.UTC(2026, 5, 5, 0, 0, 0);
const history = buildHistory(t0, 5, { backfill_path_json: 1 });
const last = history[history.length - 1];
const current = {
sampleAt: new Date(t0 + 6 * 1000).toISOString(),
sources: { backfill_path_json: last.sources.backfill_path_json + 100 },
};
const r = detect(history, current, { windowMs: 5 * 60 * 1000, factor: 10, minHistorySec: 30 });
assert(!r.flags.backfill_path_json, 'expected no flag with insufficient history');
});
console.log('\n' + pass + ' passed, ' + fail + ' failed');
process.exit(fail === 0 ? 0 : 1);