59 lines
2.8 KiB
JavaScript
59 lines
2.8 KiB
JavaScript
|
|
// 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();
|
||
|
|
}
|
||
|
|
});
|