Files
rotatingMachine/test/basic/flowController.basic.test.js
znetsixe c5bb375dd0 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>
2026-05-10 21:38:45 +02:00

133 lines
5.0 KiB
JavaScript

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