specificClass.js: 1760 → 400 lines.
Machine extends BaseDomain. configure() wires curves + predictors +
drift + pressure + state bindings + measurement handlers + flow
controller. ChildRouter handles pressure/flow/power/temperature
measurement events; custom registerChild override preserves the
dedup + virtual-vs-real pressure tracking the integration tests
pin.
Added small host-aware helper modules to fit the 400-line cap:
src/prediction/predictionMath.js (calcFlow/Power/Ctrl)
src/prediction/efficiencyMath.js (calcCog/EfficiencyCurve/etc.)
src/pressure/pressureSelector.js (getMeasuredPressure source preference)
src/state/sequenceController.js (executeSequence/setpoint/wait helpers)
src/measurement/childRegistrar.js (custom registerChild path)
src/drift/healthRefresh.js (drift status update wrappers)
src/io/output.js (buildOutput + buildStatusBadge)
unitPolicy: live UnitPolicy methods .canonical()/.output()/.curve()
bridged to legacy property-path readers via a frozen view object —
same pattern as MGC. See OPEN_QUESTIONS.md.
nodeClass.js: 433 → 61 lines.
Extends BaseNodeAdapter. tickInterval=null (event-driven on state +
measurement events). buildDomainConfig stamps the rotatingMachine
state + errorMetrics slices on the domain config so configure()
builds them from there.
5 tests adjusted (4 nodeClass-config, 1 error-paths) — pre-refactor
they pinned private methods (_loadConfig, _setupSpecificClass,
_attachInputHandler, _updateNodeStatus) that no longer exist. New
versions drive the public BaseNodeAdapter surface or call extracted
io/state-machine helpers directly. See OPEN_QUESTIONS.md 2026-05-10
"private nodeClass tests" for the deferred rewrite plan.
196 / 196 tests pass (basic 110 + integration ~80 + edge ~6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
7.8 KiB
JavaScript
151 lines
7.8 KiB
JavaScript
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');
|
|
});
|