// Basic tests for the machineGroupControl 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 = 'mgc-1', handleInputResult = undefined } = {}) { const calls = { setMode: [], setScaling: [], handleInput: [], registerChild: [], }; const source = { logger: makeLogger(), config: { general: { name } }, setMode: (m) => calls.setMode.push(m), setScaling: (s) => calls.setScaling.push(s), handleInput: async (src, demand) => { calls.handleInput.push({ src, demand }); if (handleInputResult instanceof Error) throw handleInputResult; return handleInputResult; }, 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: 'prioritycontrol' }, source, makeCtx()); assert.deepEqual(calls.setMode, ['prioritycontrol']); await reg.dispatch({ topic: 'set.scaling', payload: 'normalized' }, source, makeCtx()); assert.deepEqual(calls.setScaling, ['normalized']); await reg.dispatch({ topic: 'set.demand', payload: '12.5' }, source, makeCtx()); assert.equal(calls.handleInput.length, 1); assert.deepEqual(calls.handleInput[0], { src: 'parent', demand: 12.5 }); }); test('child.register canonical resolves child via RED.nodes.getNode', async () => { const { source, calls } = makeSource(); const child = { id: 'child-1', source: { tag: 'child-domain' } }; const reg = makeRegistry(makeLogger()); await reg.dispatch( { topic: 'child.register', payload: 'child-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('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: 'prioritycontrol' }, source, makeCtx({ logger: ctxLogger })); await reg.dispatch({ topic: 'setMode', payload: 'optimalcontrol' }, source, makeCtx({ logger: ctxLogger })); assert.deepEqual(calls.setMode, ['prioritycontrol', 'optimalcontrol']); let warns = ctxLogger.calls.warn.filter((m) => m.includes("'setMode' is deprecated")); assert.equal(warns.length, 1, 'setMode deprecation warning should log exactly once'); await reg.dispatch({ topic: 'setScaling', payload: 'absolute' }, source, makeCtx({ logger: ctxLogger })); warns = ctxLogger.calls.warn.filter((m) => m.includes("'setScaling' is deprecated")); assert.equal(warns.length, 1); assert.deepEqual(calls.setScaling, ['absolute']); await reg.dispatch({ topic: 'Qd', payload: 5 }, source, makeCtx({ logger: ctxLogger })); warns = ctxLogger.calls.warn.filter((m) => m.includes("'Qd' is deprecated")); assert.equal(warns.length, 1); assert.equal(calls.handleInput.length, 1); const child = { id: 'child-x', source: { tag: 'child-domain' } }; await reg.dispatch( { topic: 'registerChild', payload: 'child-x', positionVsParent: 'atEquipment' }, source, makeCtx({ child, logger: ctxLogger }) ); warns = ctxLogger.calls.warn.filter((m) => m.includes("'registerChild' is deprecated")); assert.equal(warns.length, 1); assert.equal(calls.registerChild.length, 1); }); test('set.demand with non-numeric payload logs error and does not call handleInput', async () => { const { source, calls } = makeSource(); const ctxLogger = makeLogger(); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'set.demand', payload: 'oops' }, source, makeCtx({ logger: ctxLogger })); assert.equal(calls.handleInput.length, 0); assert.ok( ctxLogger.calls.error.some((m) => m.includes('set.demand') && m.includes('oops')), `expected error about invalid Qd, got: ${JSON.stringify(ctxLogger.calls.error)}` ); }); test('set.demand on success calls ctx.send with reply { topic: config.general.name, payload: "done" }', async () => { const { source, calls } = makeSource({ name: 'mgc-A' }); const sent = []; const ctx = makeCtx({ sendSpy: (m) => sent.push(m) }); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'set.demand', payload: 7.5 }, source, ctx); assert.equal(calls.handleInput.length, 1); assert.deepEqual(calls.handleInput[0], { src: 'parent', demand: 7.5 }); assert.equal(sent.length, 1); assert.equal(sent[0].topic, 'mgc-A'); assert.equal(sent[0].payload, 'done'); }); test('child.register with unknown child 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)}` ); });