Add 4 cross-node tests closing PS↔MGC integration gaps
Some checks failed
CI / lint-and-test (push) Has been cancelled
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>
This commit is contained in:
102
test/dead-zone-signal.integration.test.js
Normal file
102
test/dead-zone-signal.integration.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// Dead-zone signal contract: PS must emit the right percControl as level
|
||||
// crosses startLevel↓ → stopLevel↓. Schmitt-trigger semantics:
|
||||
//
|
||||
// - level > startLevel → percControl scales 0..100 % across
|
||||
// [startLevel, maxLevel] (engaged=true)
|
||||
// - stopLevel ≤ level ≤ start → percControl = deadZoneKeepAlivePercent
|
||||
// (engaged stays true on the way down)
|
||||
// - level < stopLevel → percControl = 0, MGC turnOffAllMachines
|
||||
// (engaged=false; rising edge re-arms
|
||||
// only at startLevel)
|
||||
//
|
||||
// Without this test, refactors of `_applyLevelbasedControl` could
|
||||
// silently break the hysteresis transitions and the demo would oscillate
|
||||
// or never stop pumping.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildPlant } = require('./lib/wiring');
|
||||
|
||||
const TICK_MS = 1000;
|
||||
|
||||
function readPercControl(ps) {
|
||||
return Number(ps.percControl) || 0;
|
||||
}
|
||||
|
||||
function readEngaged(ps) {
|
||||
return Boolean(ps._stopHystRunning);
|
||||
}
|
||||
|
||||
async function settle(plant, qIn_m3s, ms) {
|
||||
const { ps, advance } = plant;
|
||||
const ticks = Math.ceil(ms / TICK_MS);
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
ps.setManualInflow(qIn_m3s, Date.now(), 'm3/s');
|
||||
advance(TICK_MS);
|
||||
ps.tick();
|
||||
await new Promise((r) => setImmediate(r));
|
||||
}
|
||||
}
|
||||
|
||||
test('dead-zone Schmitt: percControl 100→1→0 across startLevel↓ stopLevel↓', async () => {
|
||||
// Start ABOVE startLevel so the rising edge has already fired (engaged
|
||||
// becomes true via the startup tick).
|
||||
const plant = buildPlant({ initialBasinLevel: 3.0 });
|
||||
const { ps, restore } = plant;
|
||||
try {
|
||||
// Tick once at zero inflow to let the controller register engaged.
|
||||
await settle(plant, 0, 1000);
|
||||
assert.ok(readEngaged(ps),
|
||||
`precondition: engaged should be true at level=3.0 above startLevel=2.5; got ${readEngaged(ps)}`);
|
||||
|
||||
// ---- Region A: above startLevel ----
|
||||
// level=3.0 → upPct = 50 % (linear over [2.5, 3.5]).
|
||||
// (We don't lock the exact value — just assert it's well above the
|
||||
// keep-alive 1 % to confirm we're on the "engaged + above start" path.)
|
||||
await settle(plant, 0, 2000);
|
||||
const pcAbove = readPercControl(ps);
|
||||
assert.ok(pcAbove > 10,
|
||||
`Region A: at level≈3.0 m, percControl should be the ramp value (>>1 %); got ${pcAbove.toFixed(2)} %`);
|
||||
|
||||
// Manually drop level into the dead band [stopLevel=2.0, startLevel=2.5]
|
||||
// by calibrating instead of waiting for physical drain (this isolates
|
||||
// the Schmitt-trigger logic from physics).
|
||||
ps.calibratePredictedLevel(2.3);
|
||||
await settle(plant, 0, 1000);
|
||||
const pcDead = readPercControl(ps);
|
||||
const engagedDead = readEngaged(ps);
|
||||
assert.ok(engagedDead,
|
||||
`Region B: engaged should remain true while in dead band [stopLevel, startLevel]; got false`);
|
||||
// Keep-alive default in psConfig is 1 %.
|
||||
assert.ok(pcDead >= 0.5 && pcDead <= 5,
|
||||
`Region B: at level=2.3 in dead band, percControl should be the keep-alive value (~1 %); got ${pcDead.toFixed(2)} %`);
|
||||
|
||||
// Drop below stopLevel — falling-edge disengage.
|
||||
ps.calibratePredictedLevel(1.9);
|
||||
await settle(plant, 0, 1000);
|
||||
const pcOff = readPercControl(ps);
|
||||
const engagedOff = readEngaged(ps);
|
||||
assert.equal(pcOff, 0,
|
||||
`Region C: below stopLevel=2.0, percControl must be 0; got ${pcOff}`);
|
||||
assert.equal(engagedOff, false,
|
||||
`Region C: below stopLevel, engaged must flip to false; got ${engagedOff}`);
|
||||
|
||||
// Refill into the dead band — engaged should stay false (no rising
|
||||
// edge yet — needs to cross startLevel).
|
||||
ps.calibratePredictedLevel(2.3);
|
||||
await settle(plant, 0, 1000);
|
||||
const pcDeadAgain = readPercControl(ps);
|
||||
assert.equal(readEngaged(ps), false,
|
||||
`Region D: re-entered dead band from below stopLevel — engaged must stay false until level crosses startLevel`);
|
||||
assert.equal(pcDeadAgain, 0,
|
||||
`Region D: in dead band but not engaged → percControl must be 0; got ${pcDeadAgain}`);
|
||||
|
||||
// Cross startLevel → engaged re-arms.
|
||||
ps.calibratePredictedLevel(2.6);
|
||||
await settle(plant, 0, 1000);
|
||||
assert.equal(readEngaged(ps), true,
|
||||
`Region E: rising edge at startLevel must set engaged=true`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user