Files
EVOLV/test/dead-zone-signal.integration.test.js

103 lines
4.3 KiB
JavaScript
Raw Permalink Normal View History

2026-05-08 18:07:11 +02:00
// 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();
}
});