const test = require('node:test'); const assert = require('node:assert/strict'); const NodeClass = require('../../src/nodeClass'); const commands = require('../../src/commands'); const { createRegistry } = require('generalFunctions'); const { makeNodeStub, makeREDStub } = require('../helpers/factories'); // Post-BaseNodeAdapter, dispatch is the commands-registry. These tests // drive the same surface from a prototype-derived nodeClass instance to // keep the routing covered without booting Node-RED. function makeSourceStub() { const calls = []; return { calls, logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} }, childRegistrationUtils: { registerChild(childSource, pos) { calls.push(['registerChild', childSource, pos]); } }, setMode(mode) { calls.push(['setMode', mode]); }, handleInput(source, action, parameter) { calls.push(['handleInput', source, action, parameter]); return Promise.resolve(); }, showWorkingCurves() { return { ok: true }; }, showCoG() { return { cog: 1 }; }, updateSimulatedMeasurement(type, position, value) { calls.push(['updateSimulatedMeasurement', type, position, value]); }, updateMeasuredPressure(value, position) { calls.push(['updateMeasuredPressure', value, position]); }, updateMeasuredFlow(value, position) { calls.push(['updateMeasuredFlow', value, position]); }, updateMeasuredPower(value, position) { calls.push(['updateMeasuredPower', value, position]); }, updateMeasuredTemperature(value, position) { calls.push(['updateMeasuredTemperature', value, position]); }, isUnitValidForType() { return true; }, }; } test('input handler routes topics to source methods via commands registry', async () => { const inst = Object.create(NodeClass.prototype); const node = makeNodeStub(); const source = makeSourceStub(); inst.node = node; inst.RED = makeREDStub({ child1: { source: { id: 'child-source' } } }); inst.source = source; inst._commands = createRegistry(commands, { logger: source.logger }); inst._attachInputHandler(); const onInput = node._handlers.input; await onInput({ topic: 'setMode', payload: 'auto' }, () => {}, () => {}); await onInput({ topic: 'execSequence', payload: { source: 'GUI', action: 'startup' } }, () => {}, () => {}); await onInput({ topic: 'flowMovement', payload: { source: 'GUI', action: 'flowMovement', setpoint: 123 } }, () => {}, () => {}); await onInput({ topic: 'emergencystop', payload: { source: 'GUI', action: 'emergencystop' } }, () => {}, () => {}); await onInput({ topic: 'registerChild', payload: 'child1', positionVsParent: 'downstream' }, () => {}, () => {}); await onInput({ topic: 'simulateMeasurement', payload: { type: 'pressure', position: 'upstream', value: 250, unit: 'mbar' } }, () => {}, () => {}); await onInput({ topic: 'simulateMeasurement', payload: { type: 'power', position: 'atEquipment', value: 7.5, unit: 'kW' } }, () => {}, () => {}); assert.deepEqual(source.calls[0], ['setMode', 'auto']); assert.deepEqual(source.calls[1], ['handleInput', 'GUI', 'execSequence', 'startup']); assert.deepEqual(source.calls[2], ['handleInput', 'GUI', 'flowMovement', 123]); // estop handler defaults action to 'emergencystop' even without one // supplied, so the trailing arg is undefined — passed as positional. assert.deepEqual(source.calls[3].slice(0, 3), ['handleInput', 'GUI', 'emergencystop']); assert.deepEqual(source.calls[4], ['registerChild', { id: 'child-source' }, 'downstream']); assert.deepEqual(source.calls[5], ['updateSimulatedMeasurement', 'pressure', 'upstream', 250]); assert.deepEqual(source.calls[6], ['updateMeasuredPower', 7.5, 'atEquipment']); }); test('simulateMeasurement warns and ignores invalid payloads', async () => { const warns = []; const inst = Object.create(NodeClass.prototype); const node = makeNodeStub(); const calls = []; inst.node = node; inst.RED = makeREDStub(); inst.source = { logger: { warn: (m) => warns.push(m), info: () => {}, debug: () => {}, error: () => {} }, childRegistrationUtils: { registerChild() {} }, setMode() {}, handleInput() { return Promise.resolve(); }, showWorkingCurves() { return {}; }, showCoG() { return {}; }, updateSimulatedMeasurement() { calls.push('updateSimulatedMeasurement'); }, updateMeasuredPressure() { calls.push('updateMeasuredPressure'); }, updateMeasuredFlow() { calls.push('updateMeasuredFlow'); }, updateMeasuredPower() { calls.push('updateMeasuredPower'); }, updateMeasuredTemperature() { calls.push('updateMeasuredTemperature'); }, isUnitValidForType() { return true; }, }; inst._commands = createRegistry(commands, { logger: inst.source.logger }); inst._attachInputHandler(); const onInput = node._handlers.input; await onInput({ topic: 'simulateMeasurement', payload: { type: 'pressure', position: 'upstream', value: 'not-a-number' } }, () => {}, () => {}); await onInput({ topic: 'simulateMeasurement', payload: { type: 'flow', position: 'upstream', value: 12 } }, () => {}, () => {}); await onInput({ topic: 'simulateMeasurement', payload: { type: 'unknown', position: 'upstream', value: 12, unit: 'm3/h' } }, () => {}, () => {}); assert.equal(calls.length, 0); // Filter out the one-time deprecation warning for the legacy // 'simulateMeasurement' alias — only the three invalid-payload warns // matter for this assertion. const payloadWarns = warns.filter((w) => !/deprecated/i.test(String(w))); assert.equal(payloadWarns.length, 3); assert.match(String(payloadWarns[0]), /finite number/i); assert.match(String(payloadWarns[1]), /payload\.unit is required/i); assert.match(String(payloadWarns[2]), /unsupported simulatemeasurement type/i); }); test('source.getStatusBadge shows warning when pressure inputs are not initialized', () => { // Status badge now lives on the domain (Machine). Build a tiny stub. const source = { currentMode: 'virtualControl', state: { getCurrentState: () => 'operational', getCurrentPosition: () => 50 }, pressureInit: { getStatus: () => ({ initialized: false, hasUpstream: false, hasDownstream: false, hasDifferential: false }) }, measurements: { type() { return { variant() { return { position() { return { getCurrentValue() { return 0; } }; } }; } }; } }, unitPolicyView: { output: { flow: 'm3/h' } }, logger: { error: () => {} }, }; // Import the buildStatusBadge helper directly — it's the same code the // domain's getStatusBadge() invokes. const { buildStatusBadge } = require('../../src/io/output'); const status = buildStatusBadge(source); assert.equal(status.fill, 'yellow'); assert.equal(status.shape, 'ring'); assert.match(status.text, /pressure not initialized/i); }); test('showWorkingCurves and CoG route reply messages to process output index', async () => { const inst = Object.create(NodeClass.prototype); const node = makeNodeStub(); const source = { logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} }, childRegistrationUtils: { registerChild() {} }, setMode() {}, handleInput() { return Promise.resolve(); }, showWorkingCurves() { return { curve: [1, 2, 3] }; }, showCoG() { return { cog: 0.77 }; }, }; inst.node = node; inst.RED = makeREDStub(); inst.source = source; inst._commands = createRegistry(commands, { logger: source.logger }); inst._attachInputHandler(); const onInput = node._handlers.input; const sent = []; const send = (out) => sent.push(out); await onInput({ topic: 'showWorkingCurves', payload: { request: true } }, send, () => {}); await onInput({ topic: 'CoG', payload: { request: true } }, send, () => {}); assert.equal(sent.length, 2); assert.equal(Array.isArray(sent[0]), true); assert.equal(sent[0].length, 3); assert.equal(sent[0][0].topic, 'showWorkingCurves'); assert.equal(sent[0][1], null); assert.equal(sent[0][2], null); assert.equal(sent[1][0].topic, 'showCoG'); });