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>
This commit is contained in:
132
test/edge/negative-zero-guards.edge.test.js
Normal file
132
test/edge/negative-zero-guards.edge.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user