const test = require('node:test'); const assert = require('node:assert/strict'); const MeasurementHandlers = require('../../src/measurement/measurementHandlers'); function makeChainable(sink) { const builder = { _path: {}, type(t) { this._path.type = t; return this; }, variant(v) { this._path.variant = v; return this; }, position(p){ this._path.position = p; return this; }, child(id) { this._path.child = id; return this; }, value(v, ts, unit) { sink.push({ ...this._path, value: v, ts, unit }); this._path = {}; }, getCurrentValue(unit) { return sink._currentValue != null ? sink._currentValue : 0; }, }; return builder; } 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({ operational = true } = {}) { const writes = []; const logger = makeLogger(); const host = { logger, writes, measurementUnits: { flow: 'm3/h', power: 'kW', temperature: 'C', pressure: 'mbar' }, unitPolicy: { canonical: { flow: 'm3/s', power: 'W', temperature: 'K', pressure: 'Pa' }, output: { flow: 'm3/h', power: 'kW', temperature: 'C', pressure: 'mbar' }, }, predictFlow: { outputY: 7 }, predictPower: { outputY: 1234 }, measurements: makeChainable(writes), _isOperationalState: () => operational, _resolveMeasurementUnit: (type, unit) => { if (!unit) throw new Error(`Missing unit for ${type} measurement.`); return unit; }, _updateMetricDrift: (...args) => { host.driftCalls.push(args); }, _updatePredictionHealth: () => { host.healthCalls++; }, driftCalls: [], healthCalls: 0, updateMeasuredPressure: (...args) => { host.pressureCalls.push(args); }, pressureCalls: [], updatePosition: () => { host.positionCalls++; }, positionCalls: 0, }; return host; } test('dispatch("flow", …) routes to updateMeasuredFlow', () => { const host = makeHost(); const mh = new MeasurementHandlers({ host }); mh.dispatch('flow', 5, 'downstream', { unit: 'm3/h', childId: 'c1', childName: 'FT-1' }); const flowWrite = host.writes.find((w) => w.type === 'flow' && w.variant === 'measured'); assert.ok(flowWrite, 'expected measured flow write'); assert.equal(flowWrite.value, 5); assert.equal(flowWrite.position, 'downstream'); assert.equal(flowWrite.child, 'c1'); const predictedWrites = host.writes.filter((w) => w.type === 'flow' && w.variant === 'predicted'); assert.equal(predictedWrites.length, 2, 'two predicted writes (downstream+atEquipment)'); assert.equal(host.driftCalls.length, 1); assert.equal(host.driftCalls[0][0], 'flow'); assert.equal(host.healthCalls, 1); }); test('dispatch("temperature", …) writes to measurements (works in non-operational state too)', () => { const host = makeHost({ operational: false }); const mh = new MeasurementHandlers({ host }); mh.dispatch('temperature', 22.5, 'atEquipment', { unit: 'C', childId: 'tc', childName: 'TT-1', timestamp: 111 }); const write = host.writes.find((w) => w.type === 'temperature'); assert.ok(write); assert.equal(write.value, 22.5); assert.equal(write.unit, 'C'); assert.equal(write.ts, 111); }); test('dispatch("power", …) routes to updateMeasuredPower and respects unit', () => { const host = makeHost(); const mh = new MeasurementHandlers({ host }); mh.dispatch('power', 1500, 'atEquipment', { unit: 'kW', childId: 'pwr', childName: 'P-1' }); const measured = host.writes.find((w) => w.type === 'power' && w.variant === 'measured'); assert.ok(measured); assert.equal(measured.unit, 'kW'); const predicted = host.writes.find((w) => w.type === 'power' && w.variant === 'predicted'); assert.ok(predicted); assert.equal(host.driftCalls.length, 1); assert.equal(host.driftCalls[0][0], 'power'); }); test('flow/power updates are skipped when machine is not operational', () => { const host = makeHost({ operational: false }); const mh = new MeasurementHandlers({ host }); mh.dispatch('flow', 5, 'downstream', { unit: 'm3/h' }); mh.dispatch('power', 99, 'atEquipment', { unit: 'kW' }); assert.equal(host.writes.length, 0); assert.equal(host.driftCalls.length, 0); assert.ok(host.logger.calls.warn.some((m) => /Machine not operational/.test(m))); }); test('dispatch("pressure", …) delegates to host.updateMeasuredPressure (pressureRouter)', () => { const host = makeHost(); const mh = new MeasurementHandlers({ host }); mh.dispatch('pressure', 1013, 'upstream', { unit: 'mbar', childId: 'PT-1' }); assert.equal(host.pressureCalls.length, 1); assert.deepEqual(host.pressureCalls[0][0], 1013); }); test('dispatch(unknown, …) logs warn and falls back to updatePosition', () => { const host = makeHost(); const mh = new MeasurementHandlers({ host }); mh.dispatch('vibration', 1, 'atEquipment', {}); assert.equal(host.positionCalls, 1); assert.ok(host.logger.calls.warn.some((m) => /No handler for measurement type/.test(m))); }); test('handler rejects update when unit resolution throws', () => { const host = makeHost(); const mh = new MeasurementHandlers({ host }); mh.dispatch('flow', 5, 'downstream', { /* no unit */ }); assert.equal(host.writes.length, 0); assert.ok(host.logger.calls.warn.some((m) => /Rejected flow update/.test(m))); }); test('constructor validates host', () => { assert.throws(() => new MeasurementHandlers({}), /ctx\.host is required/); });