2026-05-27 17:47:50 +02:00
|
|
|
// Unit tests for the MGC movement state + rendezvous-lock helpers
|
|
|
|
|
// (getMovementState / _isEmergencyDemand / _pressureEmergency). Exercised via
|
|
|
|
|
// prototype.call with a
|
2026-05-27 16:09:18 +02:00
|
|
|
// 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');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:47:50 +02:00
|
|
|
// 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({
|
2026-05-27 16:09:18 +02:00
|
|
|
_lastDemand: last == null ? null : { canonical: last },
|
2026-05-27 17:47:50 +02:00
|
|
|
}, demandQ, { emergency });
|
2026-05-27 16:09:18 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 17:47:50 +02:00
|
|
|
test('emergency: a stop (≤0) always pre-empts', () => {
|
|
|
|
|
assert.equal(emergency(0), true);
|
|
|
|
|
assert.equal(emergency(-5), true);
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|
2026-05-27 17:47:50 +02:00
|
|
|
test('emergency: the first demand (no prior) dispatches immediately', () => {
|
|
|
|
|
assert.equal(emergency(50, { last: null }), true);
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|
2026-05-27 17:47:50 +02:00
|
|
|
test('emergency: an explicit emergency flag pre-empts', () => {
|
|
|
|
|
assert.equal(emergency(60, { last: 10, emergency: true }), true);
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|
2026-05-27 17:47:50 +02:00
|
|
|
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
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|
2026-05-27 17:47:50 +02:00
|
|
|
|
|
|
|
|
// 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);
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|
2026-05-27 17:47:50 +02:00
|
|
|
test('pressureEmergency: true when header breaches the configured threshold', () => {
|
|
|
|
|
assert.equal(pressureEmergency({ thr: 200000, headerPa: 210000 }), true);
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|
2026-05-27 17:47:50 +02:00
|
|
|
test('pressureEmergency: false when header pressure is unknown', () => {
|
|
|
|
|
assert.equal(pressureEmergency({ thr: 200000, headerPa: undefined }), false);
|
2026-05-27 16:09:18 +02:00
|
|
|
});
|