P5 wave 1: extract rotatingMachine concerns into focused modules
src/curves/ loader + normalizer (with cross-pressure anomaly
detection) + reverseCurve helper
src/prediction/ predictors (predictFlow/Power/Ctrl) +
groupPredictors (lazy group-scope views) +
OperatingPoint (pressure-driven prediction setpoints)
src/drift/ DriftAssessor (per-metric drift) + PredictionHealth
(composes flow/power/pressure into HealthStatus +
confidence sibling — see OPEN_QUESTIONS 2026-05-10)
src/pressure/ VirtualPressureChildren (dashboard-sim) +
PressureInitialization (real-vs-virtual tracking) +
PressureRouter (dispatches by position)
src/state/ stateBindings (state.emitter listener helper) +
isOperationalState
src/measurement/ measurementHandlers (dispatcher for flow/power/temp/pressure)
src/flow/ flowController (handleInput body — execSequence,
execMovement, flowMovement, emergencystop)
src/display/ workingCurves (showWorkingCurves + showCoG admin)
src/commands/ canonical names: set.mode, cmd.startup/shutdown/estop,
set.setpoint, set.flow-setpoint,
data.simulate-measurement, query.curves, query.cog,
child.register. execSequence demuxes by payload.action
to canonical cmd.* handlers.
CONTRACT.md inputs/outputs/events/children surface
110 basic tests pass (100 new + 10 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P5 wave 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
132
test/basic/flowController.basic.test.js
Normal file
132
test/basic/flowController.basic.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const FlowController = require('../../src/flow/flowController');
|
||||
|
||||
function makeLogger() {
|
||||
const calls = { debug: [], info: [], warn: [], error: [] };
|
||||
return {
|
||||
calls,
|
||||
debug: (m) => calls.debug.push(m),
|
||||
info: (m) => calls.info.push(m),
|
||||
warn: (m) => calls.warn.push(m),
|
||||
error: (m) => calls.error.push(m),
|
||||
};
|
||||
}
|
||||
|
||||
function makeHost({
|
||||
mode = 'auto',
|
||||
allowedActions = new Set(['execsequence', 'execmovement', 'flowmovement', 'emergencystop', 'statuscheck', 'entermaintenance', 'exitmaintenance']),
|
||||
allowedSources = true,
|
||||
setpointError,
|
||||
} = {}) {
|
||||
const logger = makeLogger();
|
||||
const host = {
|
||||
logger,
|
||||
currentMode: mode,
|
||||
unitPolicy: {
|
||||
canonical: { flow: 'm3/s' },
|
||||
output: { flow: 'm3/h' },
|
||||
},
|
||||
isValidActionForMode: (action) => allowedActions.has(action),
|
||||
isValidSourceForMode: () => allowedSources,
|
||||
calls: { executeSequence: [], setpoint: [], calcCtrl: [], convertUnit: [] },
|
||||
async executeSequence(seq) { host.calls.executeSequence.push(seq); return { ran: seq }; },
|
||||
async setpoint(sp) {
|
||||
host.calls.setpoint.push(sp);
|
||||
if (setpointError) throw setpointError;
|
||||
return { moved: sp };
|
||||
},
|
||||
calcCtrl: (canonicalFlow) => { host.calls.calcCtrl.push(canonicalFlow); return canonicalFlow / 2; },
|
||||
_convertUnitValue: (val, from, to, label) => {
|
||||
host.calls.convertUnit.push({ val, from, to, label });
|
||||
return val * 1000; // pretend m3/h -> m3/s factor
|
||||
},
|
||||
};
|
||||
return host;
|
||||
}
|
||||
|
||||
test('handle("parent","execSequence","startup") triggers executeSequence', async () => {
|
||||
const host = makeHost();
|
||||
const fc = new FlowController({ host });
|
||||
const result = await fc.handle('parent', 'execSequence', 'startup');
|
||||
assert.deepEqual(host.calls.executeSequence, ['startup']);
|
||||
assert.deepEqual(result, { ran: 'startup' });
|
||||
});
|
||||
|
||||
test('handle("parent","execMovement",50) invokes setpoint(50)', async () => {
|
||||
const host = makeHost();
|
||||
const fc = new FlowController({ host });
|
||||
const result = await fc.handle('parent', 'execMovement', 50);
|
||||
assert.deepEqual(host.calls.setpoint, [50]);
|
||||
assert.deepEqual(result, { moved: 50 });
|
||||
});
|
||||
|
||||
test('handle("parent","flowMovement",X) converts unit -> calcCtrl -> setpoint', async () => {
|
||||
const host = makeHost();
|
||||
const fc = new FlowController({ host });
|
||||
await fc.handle('parent', 'flowMovement', 36);
|
||||
assert.equal(host.calls.convertUnit.length, 1);
|
||||
assert.equal(host.calls.convertUnit[0].from, 'm3/h');
|
||||
assert.equal(host.calls.convertUnit[0].to, 'm3/s');
|
||||
assert.deepEqual(host.calls.calcCtrl, [36 * 1000]);
|
||||
assert.deepEqual(host.calls.setpoint, [(36 * 1000) / 2]);
|
||||
});
|
||||
|
||||
test('handle("parent","emergencyStop") fires executeSequence("emergencystop") and logs warn', async () => {
|
||||
const host = makeHost();
|
||||
const fc = new FlowController({ host });
|
||||
await fc.handle('parent', 'emergencyStop');
|
||||
assert.deepEqual(host.calls.executeSequence, ['emergencystop']);
|
||||
assert.ok(host.logger.calls.warn.some((m) => /Emergency stop activated/.test(m)));
|
||||
});
|
||||
|
||||
test('handle rejects non-string action', async () => {
|
||||
const host = makeHost();
|
||||
const fc = new FlowController({ host });
|
||||
await fc.handle('parent', 123, 'x');
|
||||
assert.deepEqual(host.calls.executeSequence, []);
|
||||
assert.deepEqual(host.calls.setpoint, []);
|
||||
assert.ok(host.logger.calls.error.some((m) => /Action must be string/.test(m)));
|
||||
});
|
||||
|
||||
test('handle bails out when action not allowed for mode', async () => {
|
||||
const host = makeHost({ allowedActions: new Set(['statuscheck']) });
|
||||
const fc = new FlowController({ host });
|
||||
await fc.handle('parent', 'execSequence', 'startup');
|
||||
assert.deepEqual(host.calls.executeSequence, []);
|
||||
});
|
||||
|
||||
test('handle bails out when source not allowed for mode', async () => {
|
||||
const host = makeHost({ allowedSources: false });
|
||||
const fc = new FlowController({ host });
|
||||
await fc.handle('externalApi', 'execSequence', 'startup');
|
||||
assert.deepEqual(host.calls.executeSequence, []);
|
||||
});
|
||||
|
||||
test('handle catches downstream errors and logs them (does not propagate)', async () => {
|
||||
const host = makeHost({ setpointError: new Error('boom') });
|
||||
const fc = new FlowController({ host });
|
||||
const result = await fc.handle('parent', 'execMovement', 12);
|
||||
assert.equal(result, undefined);
|
||||
assert.ok(host.logger.calls.error.some((m) => /Error handling input/.test(m)));
|
||||
});
|
||||
|
||||
test('handle returns a success envelope for statuscheck', async () => {
|
||||
const host = makeHost();
|
||||
const fc = new FlowController({ host });
|
||||
const out = await fc.handle('parent', 'statusCheck');
|
||||
assert.equal(out.status, true);
|
||||
assert.ok(out.feedback.includes('statuscheck'));
|
||||
});
|
||||
|
||||
test('handle warns on unimplemented action', async () => {
|
||||
const host = makeHost({ allowedActions: new Set(['weirdaction']) });
|
||||
const fc = new FlowController({ host });
|
||||
await fc.handle('parent', 'weirdAction');
|
||||
assert.ok(host.logger.calls.warn.some((m) => /is not implemented/.test(m)));
|
||||
});
|
||||
|
||||
test('constructor validates host', () => {
|
||||
assert.throws(() => new FlowController({}), /ctx\.host is required/);
|
||||
});
|
||||
Reference in New Issue
Block a user