Files
rotatingMachine/test/edge/negative-zero-guards.edge.test.js
znetsixe 07af7cef40 fix: production hardening — safety fixes, prediction accuracy, test coverage
Safety:
- Async input handler: await all handleInput() calls, prevents unhandled rejections
- Fix emergencyStop case mismatch: "emergencyStop" → "emergencystop" matching config
- Implement showCoG() method (was routing to undefined)
- Null guards on 6 methods for missing curve data
- Editor menu polling timeout (5s max)
- Listener cleanup on node close (child measurements + state emitter)
- Tick loop race condition: track startup timeout, clear on close

Prediction accuracy:
- Remove efficiency rounding that destroyed signal in canonical units
- Fix calcEfficiency variant: hydraulic power reads from correct variant
- Guard efficiency calculations against negative/zero values
- Division-by-zero protection in calcRelativeDistanceFromPeak
- Curve data anomaly detection (cross-pressure median-y ratio check)
- calcEfficiencyCurve O(n²) → O(n) with running min
- updateCurve bootstraps predictors when they were null

Tests: 43 new tests (76 total) covering emergency stop, shutdown/maintenance
sequences, efficiency/CoG, movement lifecycle, output format, null guards,
and listener cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:41:00 +02:00

133 lines
4.8 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const Machine = require('../../src/specificClass');
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
test('calcEfficiency with zero power and flow does not produce efficiency value', () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
machine.measurements.type('pressure').variant('measured').position('downstream').value(1000, Date.now(), 'mbar');
machine.measurements.type('pressure').variant('measured').position('upstream').value(800, Date.now(), 'mbar');
machine.measurements.type('flow').variant('predicted').position('atEquipment').value(0, Date.now(), 'm3/h');
machine.measurements.type('power').variant('predicted').position('atEquipment').value(0, Date.now(), 'kW');
// Should not throw
assert.doesNotThrow(() => machine.calcEfficiency(0, 0, 'predicted'));
});
test('calcEfficiency with negative power does not produce corrupt efficiency', () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
machine.measurements.type('pressure').variant('measured').position('downstream').value(1000, Date.now(), 'mbar');
machine.measurements.type('pressure').variant('measured').position('upstream').value(800, Date.now(), 'mbar');
machine.measurements.type('flow').variant('predicted').position('atEquipment').value(100, Date.now(), 'm3/h');
machine.measurements.type('power').variant('predicted').position('atEquipment').value(-5, Date.now(), 'kW');
// Should not crash or produce negative efficiency
assert.doesNotThrow(() => machine.calcEfficiency(-5, 100, 'predicted'));
const eff = machine.measurements.type('efficiency').variant('predicted').position('atEquipment').getCurrentValue();
// Efficiency should not have been updated with negative power (guard: power > 0)
assert.ok(eff === undefined || eff === null || eff >= 0, 'Efficiency should not be negative');
});
test('calcCog returns safe defaults when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig()
);
const result = machine.calcCog();
assert.equal(result.cog, 0);
assert.equal(result.cogIndex, 0);
assert.equal(result.NCog, 0);
assert.equal(result.minEfficiency, 0);
});
test('getCurrentCurves returns empty arrays when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig()
);
const { powerCurve, flowCurve } = machine.getCurrentCurves();
assert.deepEqual(powerCurve, { x: [], y: [] });
assert.deepEqual(flowCurve, { x: [], y: [] });
});
test('getCompleteCurve returns null when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig()
);
const { powerCurve, flowCurve } = machine.getCompleteCurve();
assert.equal(powerCurve, null);
assert.equal(flowCurve, null);
});
test('calcFlow returns 0 when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig({ state: { current: 'operational' } })
);
const flow = machine.calcFlow(50);
assert.equal(flow, 0);
});
test('calcPower returns 0 when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig({ state: { current: 'operational' } })
);
const power = machine.calcPower(50);
assert.equal(power, 0);
});
test('inputFlowCalcPower returns 0 when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig({ state: { current: 'operational' } })
);
const power = machine.inputFlowCalcPower(100);
assert.equal(power, 0);
});
test('getMeasuredPressure returns 0 when no curve data available', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig()
);
const pressure = machine.getMeasuredPressure();
assert.equal(pressure, 0);
});
test('updateCurve bootstraps predictors when they were null', () => {
const machine = new Machine(
makeMachineConfig({ asset: { model: null } }),
makeStateConfig()
);
assert.equal(machine.hasCurve, false);
assert.equal(machine.predictFlow, null);
// Load a real curve into a machine that started without one
const { loadCurve } = require('generalFunctions');
const realCurve = loadCurve('hidrostal-H05K-S03R');
assert.doesNotThrow(() => machine.updateCurve(realCurve));
assert.equal(machine.hasCurve, true);
assert.ok(machine.predictFlow !== null);
assert.ok(machine.predictPower !== null);
assert.ok(machine.predictCtrl !== null);
});