Files
rotatingMachine/test/edge/nodeClass-routing.edge.test.js
znetsixe e058fe9245 P5 wave 2: convert rotatingMachine to BaseDomain + extract helper modules
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>
2026-05-10 22:00:34 +02:00

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');
});