|
| 1 | +/** |
| 2 | + * Sentinel Node Tester — "Scanning…" ETA label regression tests |
| 3 | + * |
| 4 | + * Root cause this guards (2026-06): during the Phase-2 parallel online scan the |
| 5 | + * ETA is genuinely incomputable (no node-test completions to measure yet). The |
| 6 | + * dashboards showed a frozen-looking "Calculating…" (admin) / "ETA —" (live) for |
| 7 | + * the whole scan. The first fix keyed the label on totalNodes<=0 — which works |
| 8 | + * for a FRESH run but NOT a RESUME, where recomputeCounters() leaves totalNodes |
| 9 | + * at the restored positive value, so the scan was never detected and the label |
| 10 | + * stayed "Calculating…". The durable fix is an explicit state.scanning flag set |
| 11 | + * across the scan in pipeline.js, whitelisted into PUBLIC_STATE_KEYS, and read by |
| 12 | + * both dashboards' ETA branch. |
| 13 | + * |
| 14 | + * These are static source assertions (no native imports, no server, no DB). |
| 15 | + * |
| 16 | + * Run: node test/scanning-eta-label.test.js |
| 17 | + */ |
| 18 | + |
| 19 | +import { readFileSync } from 'fs'; |
| 20 | +import { fileURLToPath } from 'url'; |
| 21 | +import path from 'path'; |
| 22 | + |
| 23 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 24 | +const ROOT = path.join(__dirname, '..'); |
| 25 | +const pipeline = readFileSync(path.join(ROOT, 'audit/pipeline.js'), 'utf8'); |
| 26 | +const server = readFileSync(path.join(ROOT, 'server.js'), 'utf8'); |
| 27 | +const admin = readFileSync(path.join(ROOT, 'admin.html'), 'utf8'); |
| 28 | +const live = readFileSync(path.join(ROOT, 'live.html'), 'utf8'); |
| 29 | + |
| 30 | +const results = { pass: 0, fail: 0 }; |
| 31 | +function ok(cond, name) { |
| 32 | + if (cond) { results.pass++; console.log(` PASS ${name}`); } |
| 33 | + else { results.fail++; console.error(` FAIL ${name}`); } |
| 34 | +} |
| 35 | + |
| 36 | +console.log('"Scanning…" ETA label — regression tests\n'); |
| 37 | + |
| 38 | +// ─── 1. pipeline.js sets state.scanning across the Phase-2 scan ─────────────── |
| 39 | +console.log('1. pipeline.js drives the scanning flag'); |
| 40 | +ok(/state\.scanning\s*=\s*true\s*;/.test(pipeline), |
| 41 | + 'pipeline sets state.scanning = true before the scan'); |
| 42 | +// The clear must be in a finally so a scan throw can't leave it stuck true. |
| 43 | +ok(/finally\s*{\s*[\s\S]*?state\.scanning\s*=\s*false\s*;[\s\S]*?}/.test(pipeline), |
| 44 | + 'pipeline clears state.scanning = false in a finally (throw-safe)'); |
| 45 | +// Ordering: the `= true` assignment precedes the scanNodesParallel await it guards. |
| 46 | +{ |
| 47 | + const trueIdx = pipeline.indexOf('state.scanning = true'); |
| 48 | + const scanIdx = pipeline.indexOf('scanNodesParallel(nodesToTest'); |
| 49 | + ok(trueIdx !== -1 && scanIdx !== -1 && trueIdx < scanIdx, |
| 50 | + 'scanning=true is set before the runAudit scanNodesParallel call'); |
| 51 | +} |
| 52 | +// Reset at run start so a prior errored run can't leak a stale true. |
| 53 | +ok(/state\.scanning\s*=\s*false;\s*\/\/[^\n]*Phase-2/.test(pipeline), |
| 54 | + 'runAudit initialises state.scanning = false at the top'); |
| 55 | + |
| 56 | +// ─── 2. server.js ships scanning to the public/live surface ─────────────────── |
| 57 | +console.log('\n2. server.js whitelists scanning for public SSE'); |
| 58 | +ok(/PUBLIC_STATE_KEYS\s*=\s*\[[\s\S]*?'scanning'[\s\S]*?\]/.test(server), |
| 59 | + "PUBLIC_STATE_KEYS includes 'scanning' (else /live never sees it)"); |
| 60 | + |
| 61 | +// ─── 3. both dashboards key the "Scanning…" label on the flag ───────────────── |
| 62 | +console.log('\n3. dashboards read the flag in the ETA branch'); |
| 63 | +ok(/state\.scanning\s*\|\|/.test(admin) && /Scanning…/.test(admin), |
| 64 | + "admin.html ETA branch tests state.scanning and renders 'Scanning…'"); |
| 65 | +ok(/_liveState\.scanning\s*\|\|/.test(live) && /Scanning…/.test(live), |
| 66 | + "live.html ETA branch tests _liveState.scanning and renders 'Scanning…'"); |
| 67 | + |
| 68 | +// ─── Summary ───────────────────────────────────────────────────────────────── |
| 69 | +console.log(`\n============================================================`); |
| 70 | +console.log(`RESULTS: ${results.pass} passed, ${results.fail} failed (${results.pass + results.fail} total)`); |
| 71 | +console.log(`============================================================`); |
| 72 | +process.exit(results.fail === 0 ? 0 : 1); |
0 commit comments