Some checks failed
CI / lint-and-test (push) Has been cancelled
- ps-mgc-flow-contract: asserts PS's view of MGC outflow equals the live per-pump aggregate at every tick. Currently FAILS — exposes that MGC's flow.predicted.downstream reverts to optimalControl's bestFlow target after handlePressureChange writes the correct flow.act, leaving PS with stale outflow values. The mirror added in dc27a56 is necessary but not sufficient. - dead-zone-signal: asserts the Schmitt-trigger transitions (engaged 100% → keep-alive 1% → off 0%) across startLevel↓/stopLevel↓ with proper rising-edge re-arm. Currently PASSES. - inflow-overcapacity-stability: 45 s sim at 2× station capacity; asserts pumps don't thrash or park in accelerating residue. Currently FAILS — pumps end up at ctrl=0 in 'accelerating' state, suggesting the residue-unpark fix doesn't fully cover steady-state over-capacity. - realistic-startup-timing: re-runs the varying-demand-during-startup scenario with PRODUCTION-default state.time (starting=10s, warm=5s) instead of the 1-2 s used elsewhere. Currently PASSES — confirms the dispatch-reorder fix holds under realistic transition windows. Honest summary: 2 pass, 2 fail. The two failures expose genuine remaining defects in the PS↔MGC measurement contract and the residue-unpark policy. They're committed FAILING so the bugs are captured under version control until the underlying fixes land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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();
|
|
}
|
|
});
|