src/simulation/simulator.js random-walk generator (was simulateInput inline)
src/calibration/calibrator.js calibrate + isStable + evaluateRepeatability,
using generalFunctions/stats. NB: isStable
tautology preserved verbatim — see
OPEN_QUESTIONS.md 2026-05-10 for the bug.
src/commands/ registry + handlers (canonical names from start)
CONTRACT.md inputs/outputs/events surface
77 basic tests pass (62 pre-refactor + 15 new across the three new files).
specificClass.js / nodeClass.js untouched — integration is P3 wave 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
6.2 KiB
JavaScript
169 lines
6.2 KiB
JavaScript
// 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);
|
|
});
|