Adds to scalar setters whose payloads are
plain numbers OR {value, unit}. Skipped where payload is compound or
mode-dependent (control-%, {F, C: [...]}, etc.) — documented inline.
Every command gains a description field for wikiGen consumption.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
186 lines
7.2 KiB
JavaScript
186 lines
7.2 KiB
JavaScript
// Basic tests for the pumpingStation 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 = 'manual' } = {}) {
|
|
const calls = {
|
|
changeMode: [],
|
|
calibratePredictedVolume: [],
|
|
calibratePredictedLevel: [],
|
|
setManualInflow: [],
|
|
forwardDemandToChildren: [],
|
|
registerChild: [],
|
|
};
|
|
const source = {
|
|
mode,
|
|
logger: makeLogger(),
|
|
changeMode: (m) => calls.changeMode.push(m),
|
|
calibratePredictedVolume: (v) => calls.calibratePredictedVolume.push(v),
|
|
calibratePredictedLevel: (v) => calls.calibratePredictedLevel.push(v),
|
|
setManualInflow: (v, ts, u) => calls.setManualInflow.push({ v, ts, u }),
|
|
forwardDemandToChildren: async (d) => { calls.forwardDemandToChildren.push(d); },
|
|
childRegistrationUtils: {
|
|
registerChild: (childSource, position) =>
|
|
calls.registerChild.push({ childSource, position }),
|
|
},
|
|
};
|
|
return { source, calls };
|
|
}
|
|
|
|
function makeCtx({ child = null, logger = makeLogger() } = {}) {
|
|
return {
|
|
logger,
|
|
RED: { nodes: { getNode: (id) => (child && child.id === id ? child : undefined) } },
|
|
node: {},
|
|
send: () => {},
|
|
};
|
|
}
|
|
|
|
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: 'levelbased' }, source, makeCtx());
|
|
assert.deepEqual(calls.changeMode, ['levelbased']);
|
|
|
|
await reg.dispatch({ topic: 'cmd.calibrate.volume', payload: '12.5' }, source, makeCtx());
|
|
assert.deepEqual(calls.calibratePredictedVolume, [12.5]);
|
|
|
|
await reg.dispatch({ topic: 'cmd.calibrate.level', payload: 1.25 }, source, makeCtx());
|
|
assert.deepEqual(calls.calibratePredictedLevel, [1.25]);
|
|
|
|
// Registry normalises to the descriptor's `units.default` (m3/h) before
|
|
// the handler runs. 0.5 m3/s -> 1800 m3/h.
|
|
await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s' }, source, makeCtx());
|
|
assert.equal(calls.setManualInflow.length, 1);
|
|
assert.equal(calls.setManualInflow[0].v, 1800);
|
|
assert.equal(calls.setManualInflow[0].u, 'm3/h');
|
|
|
|
await reg.dispatch({ topic: 'set.demand', payload: 100 }, source, makeCtx());
|
|
assert.deepEqual(calls.forwardDemandToChildren, [100]);
|
|
});
|
|
|
|
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: 'changemode', payload: 'manual' }, source, makeCtx({ logger: ctxLogger }));
|
|
await reg.dispatch({ topic: 'changemode', payload: 'manual' }, source, makeCtx({ logger: ctxLogger }));
|
|
|
|
assert.deepEqual(calls.changeMode, ['manual', 'manual']);
|
|
const deprecWarns = ctxLogger.calls.warn.filter((m) => m.includes("'changemode' is deprecated"));
|
|
assert.equal(deprecWarns.length, 1, 'deprecation warning should log exactly once');
|
|
assert.equal(reg.deprecationStats().changemode, 2);
|
|
|
|
// q_in alias also routes to setInflow.
|
|
await reg.dispatch({ topic: 'q_in', payload: 0.25, unit: 'm3/s' }, source, makeCtx({ logger: ctxLogger }));
|
|
assert.equal(calls.setManualInflow.length, 1);
|
|
});
|
|
|
|
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)}`
|
|
);
|
|
});
|
|
|
|
test('set.inflow accepts number payload and { value, unit, timestamp } object payload', async () => {
|
|
const { source, calls } = makeSource();
|
|
const reg = makeRegistry(makeLogger());
|
|
|
|
// After registry units-normalisation the handler always sees a number in
|
|
// the descriptor's default unit (m3/h). 0.5 m3/s -> 1800 m3/h.
|
|
await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s', timestamp: 1000 }, source, makeCtx());
|
|
assert.deepEqual(calls.setManualInflow[0], { v: 1800, ts: 1000, u: 'm3/h' });
|
|
|
|
// Object payload `{ value, unit }` is flattened to a number; 2 m3/h stays
|
|
// 2 m3/h. The timestamp travels on the msg envelope after normalisation
|
|
// (the per-payload `timestamp` field is not preserved by the flatten).
|
|
await reg.dispatch(
|
|
{ topic: 'set.inflow', payload: { value: 2, unit: 'm3/h' }, timestamp: 2000 },
|
|
source,
|
|
makeCtx()
|
|
);
|
|
assert.deepEqual(calls.setManualInflow[1], { v: 2, ts: 2000, u: 'm3/h' });
|
|
});
|
|
|
|
test('set.demand in non-manual mode logs debug and does not call forwardDemandToChildren', async () => {
|
|
const { source, calls } = makeSource({ mode: 'levelbased' });
|
|
const ctxLogger = makeLogger();
|
|
const reg = makeRegistry(makeLogger());
|
|
|
|
await reg.dispatch({ topic: 'set.demand', payload: 50 }, source, makeCtx({ logger: ctxLogger }));
|
|
assert.equal(calls.forwardDemandToChildren.length, 0);
|
|
assert.ok(
|
|
ctxLogger.calls.debug.some((m) => m.includes('set.demand') && m.includes('levelbased')),
|
|
`expected debug about ignoring demand, got: ${JSON.stringify(ctxLogger.calls.debug)}`
|
|
);
|
|
});
|
|
|
|
test('set.demand with non-numeric payload logs warn and does not call', async () => {
|
|
const { source, calls } = makeSource({ mode: 'manual' });
|
|
const ctxLogger = makeLogger();
|
|
const reg = makeRegistry(makeLogger());
|
|
|
|
await reg.dispatch({ topic: 'set.demand', payload: 'oops' }, source, makeCtx({ logger: ctxLogger }));
|
|
assert.equal(calls.forwardDemandToChildren.length, 0);
|
|
assert.ok(
|
|
ctxLogger.calls.warn.some((m) => m.includes('set.demand') && m.includes('oops')),
|
|
`expected warn about invalid Qd, got: ${JSON.stringify(ctxLogger.calls.warn)}`
|
|
);
|
|
});
|