Files
EVOLV/test/realistic-startup-timing.integration.test.js
Rene De Ren 3c7d54e9c3
Some checks failed
CI / lint-and-test (push) Has been cancelled
Add 4 cross-node tests closing PS↔MGC integration gaps
- 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>
2026-05-08 18:07:11 +02:00

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();
}
});