// Race-window guard with PRODUCTION-default state.time: // starting: 10 s, warmingup: 5 s, stopping: 5 s, coolingdown: 10 s // // All previous deadlock tests use 1-2 s timing for speed. The race that // actually killed the live demo is about ordering during a long startup // window where many MGC.handleInput calls land while pumps are still // transitioning. This test re-runs the load-bearing demand-cycle scenario // against schema defaults so the test wall time matches the failure mode. const test = require('node:test'); const assert = require('node:assert/strict'); const { buildPlant, injectPumpPressure } = require('./lib/wiring'); const TICK_MS = 1000; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); test('realistic startup (start=10s, warm=5s) — varying demand during 15-second startup window', async () => { const plant = buildPlant({ initialBasinLevel: 2.6 }); const { ps, mgc, pumps, restore } = plant; try { // Apply production-default times. for (const p of pumps) { p.state.config.time = { starting: 10, warmingup: 5, stopping: 5, coolingdown: 10 }; } // Inject realistic pressures so predicts have a head. for (const p of pumps) injectPumpPressure(p, 19620, 117720); // Drive demand sequence at 1 Hz (mirroring PS tick rate). The first // 15 calls land during pump startup window; the last 15 land after. const sequence = [25, 75, 50, 100, 30, 90, 60, 100, 50, 80, 40, 100, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100]; for (const pct of sequence) { mgc.handleInput('parent', pct).catch((e) => console.log(`call ${pct}% rejected: ${e.message}`)); await sleep(1000); } // Drain: give the slowest pump time to finish its startup + ramp. await sleep(6000); const states = pumps.map((p) => p.state.getCurrentState()); const ctrls = pumps.map((p) => Number(p.state.getCurrentPosition?.()) || 0); console.log(` states=[${states.join(', ')}] ctrls=[${ctrls.map((c) => c.toFixed(1)).join(', ')}]`); console.log(` delayedMove=[${pumps.map((p) => String(p.state.delayedMove)).join(', ')}]`); // After settling, the LAST demand was 100 % so all 3 pumps must be // high. This is the same invariant idle-startup-deadlock Scenario 4 // checks, but with production timing. for (let i = 0; i < pumps.length; i++) { const id = pumps[i].config.general.id; assert.equal(states[i], 'operational', `${id}: expected operational, got '${states[i]}' (delayedMove=${pumps[i].state.delayedMove})`); assert.ok(ctrls[i] > 70, `${id}: expected ctrl > 70 % at final demand 100 %, got ${ctrls[i].toFixed(1)} % — startup race regression with production timing`); } } finally { restore(); } });