mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-10 18:17:31 +00:00
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.
108 lines
4.2 KiB
JavaScript
108 lines
4.2 KiB
JavaScript
/* 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);
|