// Basic tests for the rotatingMachine commands registry. // Run with: node --test test/basic/commands.basic.test.js 'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { createRegistry } = require('generalFunctions'); const commands = require('../../src/commands'); // --- helpers --------------------------------------------------------------- function makeLogger() { const calls = { warn: [], error: [], info: [], debug: [] }; return { calls, warn: (m) => calls.warn.push(String(m)), error: (m) => calls.error.push(String(m)), info: (m) => calls.info.push(String(m)), debug: (m) => calls.debug.push(String(m)), }; } function makeSource({ name = 'rm-1', unitValid = true } = {}) { const calls = { setMode: [], handleInput: [], registerChild: [], sim: [], updatePressure: [], updateFlow: [], updateTemp: [], updatePower: [], showWorkingCurves: 0, showCoG: 0, }; const source = { logger: makeLogger(), config: { general: { name } }, setMode: (m) => calls.setMode.push(m), handleInput: async (src, action, parameter) => { calls.handleInput.push({ src, action, parameter }); }, isUnitValidForType: () => unitValid, updateSimulatedMeasurement: (type, position, value, ctx) => calls.sim.push({ type, position, value, ctx }), updateMeasuredPressure: (v, p, c) => calls.updatePressure.push({ v, p, c }), updateMeasuredFlow: (v, p, c) => calls.updateFlow.push({ v, p, c }), updateMeasuredTemperature: (v, p, c) => calls.updateTemp.push({ v, p, c }), updateMeasuredPower: (v, p, c) => calls.updatePower.push({ v, p, c }), showWorkingCurves: () => { calls.showWorkingCurves++; return { curves: 'mock' }; }, showCoG: () => { calls.showCoG++; return { cog: 'mock' }; }, childRegistrationUtils: { registerChild: (childSource, position) => calls.registerChild.push({ childSource, position }), }, }; return { source, calls }; } function makeCtx({ child = null, logger = makeLogger(), sendSpy = null } = {}) { return { logger, RED: { nodes: { getNode: (id) => (child && child.id === id ? child : undefined) } }, node: {}, send: sendSpy || (() => {}), }; } function makeRegistry(logger) { return createRegistry(commands, { logger }); } // --- tests ----------------------------------------------------------------- test('canonical topics dispatch to their handlers', async () => { const { source, calls } = makeSource(); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'set.mode', payload: 'GUI' }, source, makeCtx()); assert.deepEqual(calls.setMode, ['GUI']); await reg.dispatch( { topic: 'cmd.startup', payload: { source: 'GUI' } }, source, makeCtx()); assert.deepEqual(calls.handleInput.at(-1), { src: 'GUI', action: 'execSequence', parameter: 'startup' }); await reg.dispatch( { topic: 'cmd.shutdown', payload: { source: 'GUI' } }, source, makeCtx()); assert.deepEqual(calls.handleInput.at(-1), { src: 'GUI', action: 'execSequence', parameter: 'shutdown' }); await reg.dispatch( { topic: 'cmd.estop', payload: { source: 'GUI', action: 'emergencystop' } }, source, makeCtx()); assert.deepEqual(calls.handleInput.at(-1), { src: 'GUI', action: 'emergencystop', parameter: undefined }); await reg.dispatch( { topic: 'set.setpoint', payload: { source: 'GUI', action: 'execMovement', setpoint: '75' } }, source, makeCtx()); assert.deepEqual(calls.handleInput.at(-1), { src: 'GUI', action: 'execMovement', parameter: 75 }); await reg.dispatch( { topic: 'set.flow-setpoint', payload: { source: 'GUI', action: 'flowMovement', setpoint: '12' } }, source, makeCtx()); assert.deepEqual(calls.handleInput.at(-1), { src: 'GUI', action: 'flowMovement', parameter: 12 }); }); test('aliases dispatch to the same handler and log a one-time deprecation', async () => { const { source, calls } = makeSource(); const ctxLogger = makeLogger(); const reg = makeRegistry(ctxLogger); await reg.dispatch({ topic: 'setMode', payload: 'GUI' }, source, makeCtx({ logger: ctxLogger })); await reg.dispatch({ topic: 'setMode', payload: 'virtualControl' }, source, makeCtx({ logger: ctxLogger })); assert.deepEqual(calls.setMode, ['GUI', 'virtualControl']); let warns = ctxLogger.calls.warn.filter((m) => m.includes("'setMode' is deprecated")); assert.equal(warns.length, 1); await reg.dispatch({ topic: 'emergencystop', payload: { source: 'GUI', action: 'emergencystop' } }, source, makeCtx({ logger: ctxLogger })); warns = ctxLogger.calls.warn.filter((m) => m.includes("'emergencystop' is deprecated")); assert.equal(warns.length, 1); await reg.dispatch({ topic: 'execMovement', payload: { source: 'GUI', action: 'execMovement', setpoint: 50 } }, source, makeCtx({ logger: ctxLogger })); warns = ctxLogger.calls.warn.filter((m) => m.includes("'execMovement' is deprecated")); assert.equal(warns.length, 1); await reg.dispatch({ topic: 'flowMovement', payload: { source: 'GUI', action: 'flowMovement', setpoint: 5 } }, source, makeCtx({ logger: ctxLogger })); warns = ctxLogger.calls.warn.filter((m) => m.includes("'flowMovement' is deprecated")); assert.equal(warns.length, 1); }); test('execSequence with payload.action=startup reaches cmd.startup handler', async () => { const { source, calls } = makeSource(); const ctxLogger = makeLogger(); const reg = makeRegistry(ctxLogger); await reg.dispatch( { topic: 'execSequence', payload: { source: 'GUI', action: 'startup' } }, source, makeCtx({ logger: ctxLogger })); assert.equal(calls.handleInput.length, 1); assert.deepEqual(calls.handleInput[0], { src: 'GUI', action: 'execSequence', parameter: 'startup' }); // Registry logs the legacy-topic deprecation (no canonical alias, but // the demux handler accepts both startup/shutdown actions). }); test('execSequence with payload.action=shutdown reaches cmd.shutdown handler', async () => { const { source, calls } = makeSource(); const reg = makeRegistry(makeLogger()); await reg.dispatch( { topic: 'execSequence', payload: { source: 'GUI', action: 'shutdown' } }, source, makeCtx()); assert.equal(calls.handleInput.length, 1); assert.deepEqual(calls.handleInput[0], { src: 'GUI', action: 'execSequence', parameter: 'shutdown' }); }); test('execSequence with unknown action logs warn and does not call handleInput', async () => { const { source, calls } = makeSource(); const ctxLogger = makeLogger(); const reg = makeRegistry(makeLogger()); await reg.dispatch( { topic: 'execSequence', payload: { source: 'GUI', action: 'frobnicate' } }, source, makeCtx({ logger: ctxLogger })); assert.equal(calls.handleInput.length, 0); assert.ok(ctxLogger.calls.warn.some((m) => m.includes('execSequence') && m.includes('frobnicate')), `expected warn, got: ${JSON.stringify(ctxLogger.calls.warn)}`); }); test('data.simulate-measurement happy path dispatches to the right updater', async () => { const { source, calls } = makeSource(); const reg = makeRegistry(makeLogger()); await reg.dispatch( { topic: 'data.simulate-measurement', payload: { type: 'pressure', position: 'upstream', value: 1013, unit: 'mbar' } }, source, makeCtx()); assert.equal(calls.sim.length, 1); assert.equal(calls.sim[0].type, 'pressure'); assert.equal(calls.sim[0].value, 1013); await reg.dispatch( { topic: 'data.simulate-measurement', payload: { type: 'flow', value: 30, unit: 'm3/h' } }, source, makeCtx()); assert.equal(calls.updateFlow.length, 1); }); test('data.simulate-measurement validation: bad type / missing unit / non-finite value', async () => { const { source, calls } = makeSource(); const ctxLogger = makeLogger(); const reg = makeRegistry(makeLogger()); // unsupported type await reg.dispatch( { topic: 'data.simulate-measurement', payload: { type: 'voltage', value: 1, unit: 'V' } }, source, makeCtx({ logger: ctxLogger })); assert.ok(ctxLogger.calls.warn.some((m) => m.includes('Unsupported simulateMeasurement type: voltage'))); // missing unit await reg.dispatch( { topic: 'data.simulate-measurement', payload: { type: 'pressure', value: 1013 } }, source, makeCtx({ logger: ctxLogger })); assert.ok(ctxLogger.calls.warn.some((m) => m.includes('unit is required'))); // non-finite value await reg.dispatch( { topic: 'data.simulate-measurement', payload: { type: 'pressure', value: 'abc', unit: 'mbar' } }, source, makeCtx({ logger: ctxLogger })); assert.ok(ctxLogger.calls.warn.some((m) => m.includes('must be a finite number'))); // nothing was forwarded to the source assert.equal(calls.sim.length, 0); assert.equal(calls.updateFlow.length, 0); assert.equal(calls.updatePressure.length, 0); }); test('query.curves and query.cog reply on Port 0 via ctx.send', async () => { const { source, calls } = makeSource(); const sent = []; const ctx = makeCtx({ sendSpy: (ports) => sent.push(ports) }); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'query.curves' }, source, ctx); await reg.dispatch({ topic: 'query.cog' }, source, ctx); assert.equal(calls.showWorkingCurves, 1); assert.equal(calls.showCoG, 1); assert.equal(sent.length, 2); // First port carries the reply; Ports 1 & 2 are null. assert.equal(sent[0][0].topic, 'showWorkingCurves'); assert.deepEqual(sent[0][0].payload, { curves: 'mock' }); assert.equal(sent[0][1], null); assert.equal(sent[0][2], null); assert.equal(sent[1][0].topic, 'showCoG'); assert.deepEqual(sent[1][0].payload, { cog: 'mock' }); }); test('child.register canonical resolves child via RED.nodes.getNode', async () => { const { source, calls } = makeSource(); const child = { id: 'm-1', source: { tag: 'm-domain' } }; const reg = makeRegistry(makeLogger()); await reg.dispatch( { topic: 'child.register', payload: 'm-1', positionVsParent: 'upstream' }, source, makeCtx({ child }) ); assert.equal(calls.registerChild.length, 1); assert.equal(calls.registerChild[0].childSource, child.source); assert.equal(calls.registerChild[0].position, 'upstream'); }); test('child.register with unknown id logs warn and does not throw', async () => { const { source, calls } = makeSource(); const ctxLogger = makeLogger(); const reg = makeRegistry(makeLogger()); await assert.doesNotReject(() => reg.dispatch( { topic: 'child.register', payload: 'missing-id', positionVsParent: 'atEquipment' }, source, makeCtx({ logger: ctxLogger }) ) ); assert.equal(calls.registerChild.length, 0); assert.ok( ctxLogger.calls.warn.some((m) => m.includes('registerChild') && m.includes('missing-id')), `expected warn about missing child, got: ${JSON.stringify(ctxLogger.calls.warn)}` ); });