|
| 1 | +/** |
| 2 | + * publicRunActive() — idle /live must not load the last run's data |
| 3 | + * |
| 4 | + * Bug: the public live surfaces (SSE init, /api/public/logs, /api/public/live-state) |
| 5 | + * shipped the log backlog + per-node results whenever broadcastLive was on — |
| 6 | + * even with NO run in flight. Right after a server boot, logBuffer is hydrated |
| 7 | + * from results/audit-*.log, so an idle /live page LOADED that backlog (and the |
| 8 | + * last run's results) behind the opaque "Testing Has Been Paused" overlay. |
| 9 | + * Removing the overlay in devtools revealed a live log + TESTED count but no |
| 10 | + * rows — the page was loading data it should not. |
| 11 | + * |
| 12 | + * Fix: gate every public work payload on publicRunActive() — true only when a |
| 13 | + * run is genuinely in flight (continuous loop running, OR an active batch row, |
| 14 | + * OR state.status running/paused*). This test runs the REAL publicRunActive |
| 15 | + * extracted from server.js against stubbed continuous/getActiveBatch/state. |
| 16 | + * |
| 17 | + * Run: node test/public-run-active.test.js |
| 18 | + */ |
| 19 | + |
| 20 | +import { readFileSync } from 'node:fs'; |
| 21 | +import { fileURLToPath } from 'node:url'; |
| 22 | +import { dirname, join } from 'node:path'; |
| 23 | +import vm from 'node:vm'; |
| 24 | + |
| 25 | +const out = { pass: 0, fail: 0, errors: [] }; |
| 26 | +function ok(cond, name) { |
| 27 | + if (cond) { out.pass++; console.log(` PASS ${name}`); } |
| 28 | + else { out.fail++; out.errors.push(name); console.log(` FAIL ${name}`); } |
| 29 | +} |
| 30 | + |
| 31 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 32 | +const src = readFileSync(join(__dirname, '..', 'server.js'), 'utf8'); |
| 33 | + |
| 34 | +function extractFn(s, name) { |
| 35 | + const m = new RegExp(`function\\s+${name}\\s*\\(`).exec(s); |
| 36 | + if (!m) throw new Error(`function ${name} not found in server.js`); |
| 37 | + let depth = 0, started = false, j = m.index; |
| 38 | + for (; j < s.length; j++) { |
| 39 | + const c = s[j]; |
| 40 | + if (c === '{') { depth++; started = true; } |
| 41 | + else if (c === '}') { depth--; if (started && depth === 0) { j++; break; } } |
| 42 | + } |
| 43 | + return s.slice(m.index, j); |
| 44 | +} |
| 45 | + |
| 46 | +const extracted = extractFn(src, 'publicRunActive'); |
| 47 | + |
| 48 | +function evalActive({ running = false, activeBatch = null, status = 'idle' } = {}) { |
| 49 | + const sandbox = { |
| 50 | + continuous: { status: () => ({ running }) }, |
| 51 | + getActiveBatch: () => activeBatch, |
| 52 | + state: { status }, |
| 53 | + console, |
| 54 | + }; |
| 55 | + vm.createContext(sandbox); |
| 56 | + vm.runInContext(extracted, sandbox); |
| 57 | + return vm.runInContext('publicRunActive()', sandbox); |
| 58 | +} |
| 59 | + |
| 60 | +console.log('\npublicRunActive() — idle /live must not load data\n'); |
| 61 | + |
| 62 | +// ─── Active signals ────────────────────────────────────────────────────────── |
| 63 | +console.log('[1] any active signal → true'); |
| 64 | +ok(evalActive({ running: true, status: 'idle' }) === true, |
| 65 | + 'continuous loop running → active'); |
| 66 | +ok(evalActive({ activeBatch: { batch: { id: 9 } }, status: 'idle' }) === true, |
| 67 | + 'an in-flight batch row → active (covers direct p2p / sub-plan / test runs)'); |
| 68 | +ok(evalActive({ status: 'running' }) === true, |
| 69 | + 'state.status running → active'); |
| 70 | +ok(evalActive({ status: 'paused_balance' }) === true, |
| 71 | + 'state.status paused_balance (mid-run pause) → active'); |
| 72 | +ok(evalActive({ status: 'paused_internet' }) === true, |
| 73 | + 'state.status paused_internet (mid-run pause) → active'); |
| 74 | + |
| 75 | +// ─── Idle / terminal → NOT active ──────────────────────────────────────────── |
| 76 | +console.log('[2] idle / terminal → false (paused page loads nothing)'); |
| 77 | +ok(evalActive({ status: 'idle' }) === false, 'fresh boot, idle → not active'); |
| 78 | +ok(evalActive({ status: 'done' }) === false, 'finished run (done) → not active'); |
| 79 | +ok(evalActive({ status: 'stopped' }) === false, 'stopped run → not active'); |
| 80 | +ok(evalActive({ status: 'error' }) === false, 'errored run → not active'); |
| 81 | +ok(evalActive({ running: false, activeBatch: null, status: undefined }) === false, |
| 82 | + 'no signals at all → not active'); |
| 83 | + |
| 84 | +// ─── Robust to throwing dependencies ───────────────────────────────────────── |
| 85 | +console.log('[3] throwing continuous/getActiveBatch does not crash the gate'); |
| 86 | +{ |
| 87 | + const sandbox = { |
| 88 | + continuous: { status: () => { throw new Error('boom'); } }, |
| 89 | + getActiveBatch: () => { throw new Error('boom'); }, |
| 90 | + state: { status: 'idle' }, |
| 91 | + console: { error() {} }, |
| 92 | + }; |
| 93 | + vm.createContext(sandbox); |
| 94 | + vm.runInContext(extracted, sandbox); |
| 95 | + ok(vm.runInContext('publicRunActive()', sandbox) === false, |
| 96 | + 'both deps throw + idle status → false, no throw'); |
| 97 | +} |
| 98 | + |
| 99 | +console.log(`\n${'='.repeat(60)}\nRESULTS: ${out.pass} passed, ${out.fail} failed (${out.pass + out.fail} total)`); |
| 100 | +if (out.errors.length) for (const e of out.errors) console.log(` FAIL: ${e}`); |
| 101 | +console.log('='.repeat(60)); |
| 102 | +process.exit(out.fail ? 1 : 0); |
0 commit comments