/** * SHADES — tiny test harness for selftest.html. Lane A owns this file. * * Deliberately not a framework. The only rules that matter: * - No rAF. requestAnimationFrame is throttled to a crawl in a hidden tab, * and a selftest that only passes while you watch it is not a selftest. * Drive time with fixedLoop() below. * - No Date.now(), no Math.random() in anything under test. * * Each lane owns js/tests/.test.js and nobody edits selftest.html. */ export function assert(cond, msg = 'assertion failed') { if (!cond) throw new Error(msg); } export function assertEq(actual, expected, msg = '') { if (!Object.is(actual, expected)) { throw new Error(`${msg || 'not equal'}: got ${format(actual)}, want ${format(expected)}`); } } export function assertClose(actual, expected, eps = 1e-6, msg = '') { if (!(Math.abs(actual - expected) <= eps)) { throw new Error(`${msg || 'not close'}: got ${actual}, want ${expected} ±${eps}`); } } export function assertLess(a, b, msg = '') { if (!(a < b)) throw new Error(`${msg || 'not less'}: ${a} is not < ${b}`); } function format(v) { if (typeof v === 'number') return String(v); try { return JSON.stringify(v); } catch { return String(v); } } /** * Drive a sim forward in fixed steps. This is how you fast-forward a storm. * * @param {number} seconds Sim seconds to advance. * @param {number} dt Step size — pass contracts.FIXED_DT. * @param {(dt:number, t:number) => void} fn Called with the time at step start. */ export function fixedLoop(seconds, dt, fn) { const steps = Math.round(seconds / dt); let t = 0; for (let i = 0; i < steps; i++) { fn(dt, t); t += dt; } return t; } /** Collects results for one lane. */ export class Suite { constructor(lane) { this.lane = lane; /** @type {{lane:string, label:string, status:'pass'|'fail'|'skip', err?:string, ms:number}[]} */ this.results = []; } /** * A test passes by returning, fails by throwing, and SKIPS by returning a * string that starts with 'SKIPPED'. * * That last clause is not sugar — it is a repair. This method used to ignore * its return value entirely, so a test that did * if (!ok) return `SKIPPED — harness dispute: ...`; * was recorded as a PASS. Two suites had already been written that way (the * integrator's storm_02 skip at the Sprint-6 merge, and a storm_01 one), which * means main has been reporting green on asserts that were neither passing nor * skipping — they returned a message and the suite called it a win. `skip: 0` * in every report we ever printed was the tell, and nobody read it, including * me: I misread one of those fake passes as my own win reproducing. * * It's the same disease as the wind router swallowing rainMmPerHour — a * mechanism that looks wired and silently isn't — and it lived in the harness * every lane trusts. A skip must be visible or it is a lie with good manners. */ test(label, fn) { const t0 = performance.now(); try { const out = fn(); if (typeof out === 'string' && out.startsWith('SKIPPED')) { this.results.push({ lane: this.lane, label, status: 'skip', err: out.replace(/^SKIPPED\s*[—-]?\s*/, ''), ms: performance.now() - t0, }); return; } if (out && typeof out.then === 'function') { // An async fn hands back a Promise that cannot throw synchronously, so // this would pass forever while proving nothing. Lane B called this out // in balance.test.js's header; make it impossible rather than a warning. throw new Error(`test "${label}" is async — Suite.test() cannot await it. ` + `Do the awaiting in run() and assert synchronously.`); } this.results.push({ lane: this.lane, label, status: 'pass', ms: performance.now() - t0 }); } catch (err) { this.results.push({ lane: this.lane, label, status: 'fail', err: err?.message ?? String(err), ms: performance.now() - t0, }); } } /** For a lane that hasn't landed yet. Shows up in the report, doesn't fail it. */ skip(label) { this.results.push({ lane: this.lane, label, status: 'skip', ms: 0 }); } } /** * @param {[string, (t: Suite) => void|Promise][]} lanes * @returns {Promise<{pass:number, fail:number, skip:number, ok:boolean, results:object[]}>} */ export async function runAll(lanes) { const results = []; for (const [lane, run] of lanes) { const suite = new Suite(lane); try { await run(suite); } catch (err) { // A lane whose module fails to import or throws at setup must not take // the whole report down with it — that's how you lose the signal from // every other lane during a merge. suite.results.push({ lane, label: '(suite threw during setup)', status: 'fail', err: err?.message ?? String(err), ms: 0, }); } results.push(...suite.results); } const pass = results.filter((r) => r.status === 'pass').length; const fail = results.filter((r) => r.status === 'fail').length; const skip = results.filter((r) => r.status === 'skip').length; return { pass, fail, skip, ok: fail === 0, results }; }