// Unit tests for the MGC movement state + rendezvous-lock helpers // (getMovementState / _isEmergencyDemand / _pressureEmergency). Exercised via // prototype.call with a // minimal fake `this` so no Node-RED runtime or full MachineGroup boot is // needed. See project rule .claude/rules/testing.md (basic = pure logic). const test = require('node:test'); const assert = require('node:assert/strict'); const MachineGroup = require('../../src/specificClass'); function machine(state, { delayedMove = null, moveTimeLeft = 0 } = {}) { return { state: { getCurrentState: () => state, delayedMove, getMoveTimeLeft: () => moveTimeLeft } }; } function movementStateOf(machines, pending = 0) { return MachineGroup.prototype.getMovementState.call({ machines, movementExecutor: { pending: () => pending }, }); } test('movementState: ready when no machines are registered', () => { assert.equal(movementStateOf({}), 'ready'); }); test('movementState: ready when every machine is settled and nothing is pending', () => { assert.equal(movementStateOf({ a: machine('operational'), b: machine('idle') }), 'ready'); }); test('movementState: working while a machine is mid-ramp', () => { assert.equal(movementStateOf({ a: machine('operational'), b: machine('accelerating') }), 'working'); }); test('movementState: working during a start/stop sequence step', () => { assert.equal(movementStateOf({ a: machine('warmingup') }), 'working'); }); test('movementState: working when a setpoint is queued (delayedMove)', () => { assert.equal(movementStateOf({ a: machine('operational', { delayedMove: 50 }) }), 'working'); }); test('movementState: working while move time remains', () => { assert.equal(movementStateOf({ a: machine('operational', { moveTimeLeft: 1.2 }) }), 'working'); }); test('movementState: working when the executor still has scheduled commands', () => { assert.equal(movementStateOf({ a: machine('operational') }, 2), 'working'); }); // Rendezvous lock: only an EMERGENCY pre-empts an in-flight rendezvous; every // ordinary setpoint (any size, mode/priority change included) defers. function emergency(demandQ, { last = 10, emergency = false } = {}) { return MachineGroup.prototype._isEmergencyDemand.call({ _lastDemand: last == null ? null : { canonical: last }, }, demandQ, { emergency }); } test('emergency: a stop (≤0) always pre-empts', () => { assert.equal(emergency(0), true); assert.equal(emergency(-5), true); }); test('emergency: the first demand (no prior) dispatches immediately', () => { assert.equal(emergency(50, { last: null }), true); }); test('emergency: an explicit emergency flag pre-empts', () => { assert.equal(emergency(60, { last: 10, emergency: true }), true); }); test('emergency: an ordinary same-mode step defers (large or small)', () => { assert.equal(emergency(12, { last: 10 }), false); // small nudge — defer assert.equal(emergency(60, { last: 10 }), false); // large step — also defers now }); // Pressure-excursion detector — inert until planner.emergencyPressurePa is set. function pressureEmergency({ thr, headerPa } = {}) { return MachineGroup.prototype._pressureEmergency.call({ config: { planner: thr == null ? {} : { emergencyPressurePa: thr } }, operatingPoint: { headerDiffPa: headerPa }, }); } test('pressureEmergency: inert (false) when no threshold is configured', () => { assert.equal(pressureEmergency({ headerPa: 999999 }), false); }); test('pressureEmergency: false when header is below the configured threshold', () => { assert.equal(pressureEmergency({ thr: 200000, headerPa: 150000 }), false); }); test('pressureEmergency: true when header breaches the configured threshold', () => { assert.equal(pressureEmergency({ thr: 200000, headerPa: 210000 }), true); }); test('pressureEmergency: false when header pressure is unknown', () => { assert.equal(pressureEmergency({ thr: 200000, headerPa: undefined }), false); });