94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
|
||
|
|
const Machine = require('../../src/specificClass');
|
||
|
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Regression tests for the FSM interruptible-movement fix (2026-04-13).
|
||
|
|
*
|
||
|
|
* Before the fix, `executeSequence("shutdown")` was silently rejected by the
|
||
|
|
* state manager if the machine was mid-move (accelerating/decelerating),
|
||
|
|
* because allowedTransitions for those states only permits returning to
|
||
|
|
* `operational` or `emergencystop`. Operators pressing Stop during a ramp
|
||
|
|
* would see the transition error-logged but no actual stop.
|
||
|
|
*
|
||
|
|
* The fix aborts the active movement, waits for the FSM to return to
|
||
|
|
* `operational`, then runs the normal shutdown / emergency-stop sequence.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
|
|
||
|
|
function makeSlowMoveMachine() {
|
||
|
|
// Slow movement so the test can reliably interrupt during accelerating.
|
||
|
|
// speed=20%/s, interval=10ms -> 80% setpoint takes ~4s of real movement.
|
||
|
|
return new Machine(
|
||
|
|
makeMachineConfig(),
|
||
|
|
makeStateConfig({
|
||
|
|
movement: { mode: 'staticspeed', speed: 20, maxSpeed: 1000, interval: 10 },
|
||
|
|
time: { starting: 0, warmingup: 0, stopping: 0, coolingdown: 0 },
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
test('shutdown during accelerating aborts the move and reaches idle', async () => {
|
||
|
|
const machine = makeSlowMoveMachine();
|
||
|
|
|
||
|
|
await machine.handleInput('parent', 'execSequence', 'startup');
|
||
|
|
assert.equal(machine.state.getCurrentState(), 'operational');
|
||
|
|
machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
|
||
|
|
machine.updateMeasuredPressure(200, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' });
|
||
|
|
|
||
|
|
// Fire a setpoint that needs ~4 seconds. Do NOT await it.
|
||
|
|
const movePromise = machine.handleInput('parent', 'execMovement', 80);
|
||
|
|
|
||
|
|
// Wait a moment for the FSM to enter accelerating.
|
||
|
|
await sleep(100);
|
||
|
|
assert.equal(machine.state.getCurrentState(), 'accelerating');
|
||
|
|
|
||
|
|
// Issue shutdown while the move is still accelerating.
|
||
|
|
await machine.handleInput('GUI', 'execSequence', 'shutdown');
|
||
|
|
|
||
|
|
// Let the aborted move unwind.
|
||
|
|
await movePromise.catch(() => {});
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
machine.state.getCurrentState(),
|
||
|
|
'idle',
|
||
|
|
'shutdown issued mid-ramp must still drive FSM back to idle',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('emergency stop during accelerating reaches off', async () => {
|
||
|
|
const machine = makeSlowMoveMachine();
|
||
|
|
|
||
|
|
await machine.handleInput('parent', 'execSequence', 'startup');
|
||
|
|
machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
|
||
|
|
|
||
|
|
const movePromise = machine.handleInput('parent', 'execMovement', 80);
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
assert.equal(machine.state.getCurrentState(), 'accelerating');
|
||
|
|
|
||
|
|
await machine.handleInput('GUI', 'emergencystop');
|
||
|
|
await movePromise.catch(() => {});
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
machine.state.getCurrentState(),
|
||
|
|
'off',
|
||
|
|
'emergency stop issued mid-ramp must still drive FSM to off',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('executeSequence accepts mixed-case sequence names', async () => {
|
||
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||
|
|
await machine.handleInput('parent', 'execSequence', 'startup');
|
||
|
|
assert.equal(machine.state.getCurrentState(), 'operational');
|
||
|
|
|
||
|
|
// Parent orchestrators (e.g. machineGroupControl) use "emergencyStop" with
|
||
|
|
// a capital S in their configs. The sequence key in rotatingMachine.json
|
||
|
|
// is lowercase. Normalization must bridge that gap without a warn.
|
||
|
|
await machine.executeSequence('EmergencyStop');
|
||
|
|
assert.equal(machine.state.getCurrentState(), 'off');
|
||
|
|
});
|