// Basic tests for the measurement 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({ mode = 'analog', simulator = false, outlier = false } = {}) { const calls = { toggleSimulation: 0, toggleOutlierDetection: 0, calibrate: 0, handleDigitalPayload: [], inputValueSets: [], }; const state = { simulator, outlier, _inputValue: 0 }; const source = { mode, logger: makeLogger(), toggleSimulation: () => { state.simulator = !state.simulator; calls.toggleSimulation += 1; }, toggleOutlierDetection: () => { state.outlier = !state.outlier; calls.toggleOutlierDetection += 1; }, calibrate: () => { calls.calibrate += 1; }, handleDigitalPayload: (p) => { calls.handleDigitalPayload.push(p); return { ok: true }; }, get inputValue() { return state._inputValue; }, set inputValue(v) { state._inputValue = v; calls.inputValueSets.push(v); }, }; return { source, calls, state }; } function makeCtx({ logger = makeLogger() } = {}) { return { logger, RED: { nodes: { getNode: () => undefined } }, node: {}, send: () => {} }; } function makeRegistry(logger) { return createRegistry(commands, { logger }); } // --- tests ----------------------------------------------------------------- test('canonical topics dispatch to the right handler', async () => { const { source, calls, state } = makeSource(); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'set.simulator' }, source, makeCtx()); assert.equal(calls.toggleSimulation, 1); assert.equal(state.simulator, true); await reg.dispatch({ topic: 'set.outlier-detection' }, source, makeCtx()); assert.equal(calls.toggleOutlierDetection, 1); assert.equal(state.outlier, true); await reg.dispatch({ topic: 'cmd.calibrate' }, source, makeCtx()); assert.equal(calls.calibrate, 1); }); 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); for (const alias of ['simulator', 'outlierDetection', 'calibrate', 'measurement']) { await reg.dispatch({ topic: alias, payload: 1 }, source, makeCtx({ logger: ctxLogger })); await reg.dispatch({ topic: alias, payload: 2 }, source, makeCtx({ logger: ctxLogger })); } for (const alias of ['simulator', 'outlierDetection', 'calibrate', 'measurement']) { const hits = ctxLogger.calls.warn.filter((m) => m.includes(`'${alias}' is deprecated`)); assert.equal(hits.length, 1, `alias '${alias}' should warn exactly once`); } // sanity: side-effects fired twice per alias. assert.equal(calls.toggleSimulation, 2); assert.equal(calls.toggleOutlierDetection, 2); assert.equal(calls.calibrate, 2); // analog measurement alias with numeric payload set inputValue twice. assert.deepEqual(calls.inputValueSets, [1, 2]); }); test('data.measurement analog with numeric payload sets source.inputValue', async () => { const { source, calls } = makeSource({ mode: 'analog' }); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'data.measurement', payload: 42 }, source, makeCtx()); await reg.dispatch({ topic: 'data.measurement', payload: '3.5' }, source, makeCtx()); assert.deepEqual(calls.inputValueSets, [42, 3.5]); }); test('data.measurement analog with object payload logs helpful switch-mode warn', async () => { const { source, calls } = makeSource({ mode: 'analog' }); const ctxLogger = makeLogger(); const reg = makeRegistry(ctxLogger); await reg.dispatch( { topic: 'data.measurement', payload: { temperature: 21.5, humidity: 45 } }, source, makeCtx({ logger: ctxLogger }) ); assert.equal(calls.inputValueSets.length, 0); assert.equal(calls.handleDigitalPayload.length, 0); assert.ok( ctxLogger.calls.warn.some((m) => m.includes('analog mode') && m.includes('digital')), `expected helpful switch-to-digital warn, got: ${JSON.stringify(ctxLogger.calls.warn)}` ); }); test('data.measurement digital with object payload calls handleDigitalPayload', async () => { const { source, calls } = makeSource({ mode: 'digital' }); const reg = makeRegistry(makeLogger()); const payload = { tempA: 21.5, tempB: 19.8 }; await reg.dispatch({ topic: 'data.measurement', payload }, source, makeCtx()); assert.equal(calls.handleDigitalPayload.length, 1); assert.deepEqual(calls.handleDigitalPayload[0], payload); assert.equal(calls.inputValueSets.length, 0); }); test('data.measurement digital with number logs helpful switch-mode warn', async () => { const { source, calls } = makeSource({ mode: 'digital' }); const ctxLogger = makeLogger(); const reg = makeRegistry(ctxLogger); await reg.dispatch( { topic: 'data.measurement', payload: 7 }, source, makeCtx({ logger: ctxLogger }) ); assert.equal(calls.handleDigitalPayload.length, 0); assert.equal(calls.inputValueSets.length, 0); assert.ok( ctxLogger.calls.warn.some((m) => m.includes('digital mode') && m.includes('analog')), `expected helpful switch-to-analog warn, got: ${JSON.stringify(ctxLogger.calls.warn)}` ); }); test('set.simulator toggles even with no payload (idempotent flip)', async () => { const { source, calls, state } = makeSource({ simulator: false }); const reg = makeRegistry(makeLogger()); await reg.dispatch({ topic: 'set.simulator' }, source, makeCtx()); assert.equal(state.simulator, true); await reg.dispatch({ topic: 'set.simulator' }, source, makeCtx()); assert.equal(state.simulator, false); await reg.dispatch({ topic: 'set.simulator' }, source, makeCtx()); assert.equal(state.simulator, true); assert.equal(calls.toggleSimulation, 3); });