Compare commits
1 Commits
9916527790
...
8e684203a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e684203a8 |
@@ -211,6 +211,113 @@ test('Scenario 3 — pumps with NO pressure measurements injected', async () =>
|
|||||||
console.log(' (Scenario 3 is exploratory — no asserts; review the snapshot above.)');
|
console.log(' (Scenario 3 is exploratory — no asserts; review the snapshot above.)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
test('Scenario 5 — full up/down/up cycle through shutdown', async () => {
|
||||||
|
// Hypothesis E: when demand goes 100% → 0% → 100% (basin fills, drains
|
||||||
|
// past stopLevel, then refills), pumps pass through stopping →
|
||||||
|
// coolingdown → idle. If a fresh flow>0 demand arrives while a pump is
|
||||||
|
// mid-shutdown, the current MGC dispatch saves flowmovement to
|
||||||
|
// delayedMove (good) but doesn't issue execsequence-startup because
|
||||||
|
// state !== 'idle' (bug). The pump completes shutdown, reaches 'idle',
|
||||||
|
// and stays there because transitionToState('idle') doesn't fire
|
||||||
|
// delayedMove — only the transition INTO 'operational' does. Pump is
|
||||||
|
// stuck with delayedMove orphaned.
|
||||||
|
|
||||||
|
const { mgc, pumps } = buildGroup();
|
||||||
|
console.log('\n[Scenario 5] cycle: 100% → 0% → 100% with mid-shutdown re-engage');
|
||||||
|
printSnapshots('before any handleInput', pumps);
|
||||||
|
|
||||||
|
// Phase 1: drive up to 100% from idle.
|
||||||
|
await mgc.handleInput('parent', 100);
|
||||||
|
await sleep(5000); // full startup + ramp
|
||||||
|
printSnapshots('after settle at 100%', pumps);
|
||||||
|
for (const p of pumps) {
|
||||||
|
assert.equal(p.state.getCurrentState(), 'operational',
|
||||||
|
`Phase 1: pump ${p.config.general.id} not operational at 100% (got ${p.state.getCurrentState()})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: demand drops to 0% — pumps begin shutdown sequence.
|
||||||
|
// FIRE-AND-FORGET: handleInput(0) awaits turnOffAllMachines which
|
||||||
|
// awaits the full per-pump shutdown sequence. We need the next 100%
|
||||||
|
// demand to arrive WHILE pumps are still in stopping/coolingdown,
|
||||||
|
// not after they've reached idle.
|
||||||
|
mgc.handleInput('parent', 0).catch(e => console.log(`0% rejected: ${e.message}`));
|
||||||
|
// Wait briefly so the shutdown sequence enters but does NOT complete.
|
||||||
|
// shutdown=['stopping','coolingdown','idle'] with stopping=1s,
|
||||||
|
// coolingdown=2s. 500ms puts us solidly inside 'stopping'.
|
||||||
|
await sleep(500);
|
||||||
|
printSnapshots('mid-shutdown (pumps should be in stopping/coolingdown)', pumps);
|
||||||
|
|
||||||
|
const midShutdownStates = pumps.map(p => p.state.getCurrentState());
|
||||||
|
console.log(` states mid-shutdown: ${midShutdownStates.join(', ')}`);
|
||||||
|
|
||||||
|
// Phase 3: demand returns to 100% while pumps are mid-shutdown.
|
||||||
|
await mgc.handleInput('parent', 100);
|
||||||
|
// Generous: full coolingdown remaining + full startup + ramp.
|
||||||
|
await sleep(8000);
|
||||||
|
printSnapshots('after re-engage to 100%', pumps);
|
||||||
|
|
||||||
|
expectAllRunningAt100(pumps, 'Scenario 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
test('Scenario 6 — full up sweep then full down sweep', async () => {
|
||||||
|
// Hypothesis F: the user observed "going up stuck ~60%, going down
|
||||||
|
// not reacting". Mirror that with an explicit up-then-down monotonic
|
||||||
|
// sweep, every step holding 600 ms (slightly longer than DWELL on
|
||||||
|
// production basin model). After the sweep, we expect the LATEST
|
||||||
|
// demand (the final value of the down-sweep, which is 10%) to be
|
||||||
|
// honoured: pumps either at 1-pump combo's split or all idle if that
|
||||||
|
// demand falls below the per-pump minimum.
|
||||||
|
|
||||||
|
const { mgc, pumps } = buildGroup();
|
||||||
|
console.log('\n[Scenario 6] up-sweep 10%→100% then down-sweep 100%→10%, each step 600 ms');
|
||||||
|
printSnapshots('before any handleInput', pumps);
|
||||||
|
|
||||||
|
const upSteps = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
||||||
|
const downSteps = [90, 80, 70, 60, 50, 40, 30, 20, 10];
|
||||||
|
|
||||||
|
console.log(' --- up sweep ---');
|
||||||
|
for (const pct of upSteps) {
|
||||||
|
mgc.handleInput('parent', pct).catch(e => console.log(`up ${pct}% rejected: ${e.message}`));
|
||||||
|
await sleep(600);
|
||||||
|
const snaps = pumps.map(snapshot);
|
||||||
|
const totalQ = snaps.reduce((s, x) => s + x.flow, 0);
|
||||||
|
console.log(` cmd=${pct.toFixed(0).padStart(3)}% states=[${snaps.map(s=>s.state.padEnd(13)).join(', ')}] ctrl=[${snaps.map(s=>s.ctrl.toFixed(1).padStart(5)).join(', ')}] ΣQ=${totalQ.toFixed(1)}`);
|
||||||
|
}
|
||||||
|
printSnapshots('top of up-sweep (cmd=100%) after full settle', pumps);
|
||||||
|
await sleep(2000);
|
||||||
|
printSnapshots('top of up-sweep + 2s drain', pumps);
|
||||||
|
|
||||||
|
console.log(' --- down sweep ---');
|
||||||
|
for (const pct of downSteps) {
|
||||||
|
mgc.handleInput('parent', pct).catch(e => console.log(`down ${pct}% rejected: ${e.message}`));
|
||||||
|
await sleep(600);
|
||||||
|
const snaps = pumps.map(snapshot);
|
||||||
|
const totalQ = snaps.reduce((s, x) => s + x.flow, 0);
|
||||||
|
console.log(` cmd=${pct.toFixed(0).padStart(3)}% states=[${snaps.map(s=>s.state.padEnd(13)).join(', ')}] ctrl=[${snaps.map(s=>s.ctrl.toFixed(1).padStart(5)).join(', ')}] ΣQ=${totalQ.toFixed(1)}`);
|
||||||
|
}
|
||||||
|
printSnapshots('bottom of down-sweep (cmd=10%) after sequence', pumps);
|
||||||
|
await sleep(3000);
|
||||||
|
printSnapshots('bottom of down-sweep + 3s drain', pumps);
|
||||||
|
|
||||||
|
// Final demand was 10% (≈ 148 m³/h). At head 1100 mbar with per-pump
|
||||||
|
// min ≈ 89.5, this is solvable by a 1-pump combo near 148 m³/h.
|
||||||
|
// Optimizer typically picks the 1-pump combo. Either way, pumps are
|
||||||
|
// NOT supposed to be stuck at the prior up-sweep's 100% setpoint.
|
||||||
|
const flowMin_m3h = mgc.calcDynamicTotals().flow.min * 3600;
|
||||||
|
const flowMax_m3h = mgc.calcDynamicTotals().flow.max * 3600;
|
||||||
|
const expectedQ_m3h = flowMin_m3h + (flowMax_m3h - flowMin_m3h) * 0.10; // 10% scaled
|
||||||
|
console.log(` expected total flow at 10%: ~${expectedQ_m3h.toFixed(1)} m³/h`);
|
||||||
|
|
||||||
|
const snaps = pumps.map(snapshot);
|
||||||
|
const totalQ = snaps.reduce((s, x) => s + x.flow, 0);
|
||||||
|
// Loose: total within 30 m³/h of expectation. Catches the obvious
|
||||||
|
// stuck-at-old-position regression.
|
||||||
|
assert.ok(Math.abs(totalQ - expectedQ_m3h) < 30,
|
||||||
|
`Scenario 6: total flow ${totalQ.toFixed(1)} m³/h diverged from expected ${expectedQ_m3h.toFixed(1)} after down-sweep — pumps did not adopt latest demand. Per-pump: ${snaps.map(s => `${s.state}@${s.ctrl.toFixed(0)}%`).join(', ')}`);
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
test('Scenario 4 — varying demand during startup (combo flips)', async () => {
|
test('Scenario 4 — varying demand during startup (combo flips)', async () => {
|
||||||
// Hypothesis D: in production the demand is NOT constant — as basin
|
// Hypothesis D: in production the demand is NOT constant — as basin
|
||||||
|
|||||||
Reference in New Issue
Block a user