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:
91
test/basic/stateBindings.basic.test.js
Normal file
91
test/basic/stateBindings.basic.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const { bindStateEvents, isOperationalState, OPERATIONAL_STATES } =
|
||||
require('../../src/state/stateBindings');
|
||||
|
||||
function makeFakeState() {
|
||||
const emitter = new EventEmitter();
|
||||
let current = 'idle';
|
||||
return {
|
||||
emitter,
|
||||
setState(s) { current = s; },
|
||||
getCurrentState() { return current; },
|
||||
};
|
||||
}
|
||||
|
||||
test('bindStateEvents attaches both listeners and they fire on emit', () => {
|
||||
const state = makeFakeState();
|
||||
let posCalls = 0;
|
||||
let stateCalls = 0;
|
||||
let lastStateArg = null;
|
||||
|
||||
bindStateEvents({
|
||||
state,
|
||||
onPositionChange: () => { posCalls++; },
|
||||
onStateChange: (newState) => { stateCalls++; lastStateArg = newState; },
|
||||
});
|
||||
|
||||
assert.equal(state.emitter.listenerCount('positionChange'), 1);
|
||||
assert.equal(state.emitter.listenerCount('stateChange'), 1);
|
||||
|
||||
state.emitter.emit('positionChange', 42);
|
||||
state.emitter.emit('stateChange', 'operational');
|
||||
|
||||
assert.equal(posCalls, 1);
|
||||
assert.equal(stateCalls, 1);
|
||||
assert.equal(lastStateArg, 'operational');
|
||||
});
|
||||
|
||||
test('bindStateEvents teardown removes both listeners and is idempotent', () => {
|
||||
const state = makeFakeState();
|
||||
const teardown = bindStateEvents({
|
||||
state,
|
||||
onPositionChange: () => {},
|
||||
onStateChange: () => {},
|
||||
});
|
||||
|
||||
assert.equal(state.emitter.listenerCount('positionChange'), 1);
|
||||
assert.equal(state.emitter.listenerCount('stateChange'), 1);
|
||||
|
||||
teardown();
|
||||
assert.equal(state.emitter.listenerCount('positionChange'), 0);
|
||||
assert.equal(state.emitter.listenerCount('stateChange'), 0);
|
||||
|
||||
teardown();
|
||||
assert.equal(state.emitter.listenerCount('positionChange'), 0);
|
||||
});
|
||||
|
||||
test('bindStateEvents validates context shape', () => {
|
||||
assert.throws(() => bindStateEvents(null), /ctx\.state\.emitter is required/);
|
||||
assert.throws(
|
||||
() => bindStateEvents({ state: makeFakeState() }),
|
||||
/handlers are required/,
|
||||
);
|
||||
});
|
||||
|
||||
test('isOperationalState returns true for operational/accelerating/decelerating/warmingup', () => {
|
||||
const state = makeFakeState();
|
||||
for (const s of ['operational', 'accelerating', 'decelerating', 'warmingup']) {
|
||||
state.setState(s);
|
||||
assert.equal(isOperationalState(state), true, `expected ${s} to be operational`);
|
||||
}
|
||||
});
|
||||
|
||||
test('isOperationalState returns false for non-operational states and bad input', () => {
|
||||
const state = makeFakeState();
|
||||
for (const s of ['idle', 'starting', 'stopping', 'coolingdown', 'emergencystopped']) {
|
||||
state.setState(s);
|
||||
assert.equal(isOperationalState(state), false, `expected ${s} not to be operational`);
|
||||
}
|
||||
assert.equal(isOperationalState(null), false);
|
||||
assert.equal(isOperationalState({}), false);
|
||||
});
|
||||
|
||||
test('OPERATIONAL_STATES list is exported and frozen-ish (no extras beyond contract)', () => {
|
||||
assert.deepEqual(
|
||||
[...OPERATIONAL_STATES].sort(),
|
||||
['accelerating', 'decelerating', 'operational', 'warmingup'],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user