const test = require('node:test'); const assert = require('node:assert/strict'); const Machine = require('../../src/specificClass'); const { makeMachineConfig, makeStateConfig } = require('../helpers/factories'); test('getOutput contains all required fields in idle state', () => { const machine = new Machine(makeMachineConfig(), makeStateConfig()); const output = machine.getOutput(); // Core state fields assert.equal(output.state, 'idle'); assert.ok('runtime' in output); assert.ok('ctrl' in output); assert.ok('moveTimeleft' in output); assert.ok('mode' in output); assert.ok('maintenanceTime' in output); // Efficiency fields assert.ok('cog' in output); assert.ok('NCog' in output); assert.ok('NCogPercent' in output); assert.ok('effDistFromPeak' in output); assert.ok('effRelDistFromPeak' in output); // Prediction health fields assert.ok('predictionQuality' in output); assert.ok('predictionConfidence' in output); assert.ok('predictionPressureSource' in output); assert.ok('predictionFlags' in output); // Pressure drift fields assert.ok('pressureDriftLevel' in output); assert.ok('pressureDriftSource' in output); assert.ok('pressureDriftFlags' in output); }); test('getOutput seeds operating-point flow/power telemetry at boot (idle = 0, not absent)', () => { // Regression: an idle-from-boot machine must still emit the operating-point // series so dashboards can show the off/0 state. These keys are otherwise // only written once the pump runs (calcFlow/calcPower) or on a state // transition, leaving them absent in telemetry for a pump that never starts. const machine = new Machine(makeMachineConfig(), makeStateConfig()); const output = machine.getOutput(); const hasPrefix = (p) => Object.keys(output).some((k) => k.startsWith(p)); const valueFor = (p) => output[Object.keys(output).find((k) => k.startsWith(p))]; for (const prefix of [ 'flow.predicted.downstream', 'flow.predicted.atequipment', 'power.predicted.atequipment', ]) { assert.ok(hasPrefix(prefix), `${prefix}.* must be present at boot (idle)`); assert.equal(valueFor(prefix), 0, `${prefix}.* should be 0 while idle`); } // The envelope keys remain present too. assert.ok(hasPrefix('flow.predicted.max')); assert.ok(hasPrefix('flow.predicted.min')); }); test('getOutput flow drift fields appear after sufficient measured flow samples', async () => { const machine = new Machine(makeMachineConfig(), makeStateConfig()); await machine.handleInput('parent', 'execSequence', 'startup'); machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt' }); await machine.handleInput('parent', 'execMovement', 50); // Provide multiple measured flow samples to trigger valid drift assessment const baseTime = Date.now(); for (let i = 0; i < 12; i++) { machine.updateMeasuredFlow(100 + i, 'downstream', { timestamp: baseTime + (i * 1000), unit: 'm3/h', childId: 'flow-sensor', childName: 'FT-1', }); } const output = machine.getOutput(); // Drift fields should appear once enough samples provide a valid assessment if ('flowNrmse' in output) { assert.ok(typeof output.flowNrmse === 'number'); assert.ok('flowDriftValid' in output); } // At minimum, prediction health fields should always be present assert.ok('predictionQuality' in output); assert.ok('predictionConfidence' in output); }); test('getOutput prediction confidence is 0 in non-operational state', () => { const machine = new Machine(makeMachineConfig(), makeStateConfig()); const output = machine.getOutput(); assert.equal(output.predictionConfidence, 0); }); test('getOutput prediction confidence reflects differential pressure', () => { const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } })); // Differential pressure → high confidence machine.updateMeasuredPressure(800, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' }); machine.updateMeasuredPressure(1200, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' }); const output = machine.getOutput(); assert.ok(output.predictionConfidence >= 0.8, `Confidence ${output.predictionConfidence} should be >= 0.8 with differential pressure`); assert.equal(output.predictionPressureSource, 'differential'); }); test('getOutput values are in configured output units not canonical', () => { const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } })); machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt' }); machine.updatePosition(); const output = machine.getOutput(); // Flow keys should contain values in m3/h (configured), not m3/s (canonical) // Predicted flow at minimum pressure should be in a reasonable m3/h range, not ~0.003 m3/s const flowKey = Object.keys(output).find(k => k.startsWith('flow.predicted.downstream')); if (flowKey) { const flowVal = output[flowKey]; assert.ok(typeof flowVal === 'number', 'Flow output should be a number'); // m3/h values are typically 0-300, m3/s values are 0-0.08 // If in canonical units it would be very small if (flowVal > 0) { assert.ok(flowVal > 0.1, `Flow value ${flowVal} looks like canonical m3/s, should be m3/h`); } } }); test('getOutput NCogPercent is correctly derived from NCog', () => { const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } })); machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt' }); machine.updatePosition(); const output = machine.getOutput(); const expected = Math.round(output.NCog * 100 * 100) / 100; assert.equal(output.NCogPercent, expected, 'NCogPercent should be NCog * 100, rounded to 2 decimals'); });