const test = require('node:test'); const assert = require('node:assert/strict'); const FlowController = require('../../src/flow/flowController'); 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({ mode = 'auto', allowedActions = new Set(['execsequence', 'execmovement', 'flowmovement', 'emergencystop', 'statuscheck', 'entermaintenance', 'exitmaintenance']), allowedSources = true, setpointError, } = {}) { const logger = makeLogger(); const host = { logger, currentMode: mode, unitPolicy: { canonical: { flow: 'm3/s' }, output: { flow: 'm3/h' }, convert: (val, from, to, label) => { host.calls.convertUnit.push({ val, from, to, label }); return val * 1000; // pretend m3/h -> m3/s factor }, }, isValidActionForMode: (action) => allowedActions.has(action), isValidSourceForMode: () => allowedSources, calls: { executeSequence: [], setpoint: [], calcCtrl: [], convertUnit: [] }, async executeSequence(seq) { host.calls.executeSequence.push(seq); return { ran: seq }; }, async setpoint(sp) { host.calls.setpoint.push(sp); if (setpointError) throw setpointError; return { moved: sp }; }, calcCtrl: (canonicalFlow) => { host.calls.calcCtrl.push(canonicalFlow); return canonicalFlow / 2; }, }; return host; } test('handle("parent","execSequence","startup") triggers executeSequence', async () => { const host = makeHost(); const fc = new FlowController({ host }); const result = await fc.handle('parent', 'execSequence', 'startup'); assert.deepEqual(host.calls.executeSequence, ['startup']); assert.deepEqual(result, { ran: 'startup' }); }); test('handle("parent","execMovement",50) invokes setpoint(50)', async () => { const host = makeHost(); const fc = new FlowController({ host }); const result = await fc.handle('parent', 'execMovement', 50); assert.deepEqual(host.calls.setpoint, [50]); assert.deepEqual(result, { moved: 50 }); }); test('handle("parent","flowMovement",X) converts unit -> calcCtrl -> setpoint', async () => { const host = makeHost(); const fc = new FlowController({ host }); await fc.handle('parent', 'flowMovement', 36); assert.equal(host.calls.convertUnit.length, 1); assert.equal(host.calls.convertUnit[0].from, 'm3/h'); assert.equal(host.calls.convertUnit[0].to, 'm3/s'); assert.deepEqual(host.calls.calcCtrl, [36 * 1000]); assert.deepEqual(host.calls.setpoint, [(36 * 1000) / 2]); }); test('handle("parent","emergencyStop") fires executeSequence("emergencystop") and logs warn', async () => { const host = makeHost(); const fc = new FlowController({ host }); await fc.handle('parent', 'emergencyStop'); assert.deepEqual(host.calls.executeSequence, ['emergencystop']); assert.ok(host.logger.calls.warn.some((m) => /Emergency stop activated/.test(m))); }); test('handle rejects non-string action', async () => { const host = makeHost(); const fc = new FlowController({ host }); await fc.handle('parent', 123, 'x'); assert.deepEqual(host.calls.executeSequence, []); assert.deepEqual(host.calls.setpoint, []); assert.ok(host.logger.calls.error.some((m) => /Action must be string/.test(m))); }); test('handle bails out when action not allowed for mode', async () => { const host = makeHost({ allowedActions: new Set(['statuscheck']) }); const fc = new FlowController({ host }); await fc.handle('parent', 'execSequence', 'startup'); assert.deepEqual(host.calls.executeSequence, []); }); test('handle bails out when source not allowed for mode', async () => { const host = makeHost({ allowedSources: false }); const fc = new FlowController({ host }); await fc.handle('externalApi', 'execSequence', 'startup'); assert.deepEqual(host.calls.executeSequence, []); }); test('handle catches downstream errors and logs them (does not propagate)', async () => { const host = makeHost({ setpointError: new Error('boom') }); const fc = new FlowController({ host }); const result = await fc.handle('parent', 'execMovement', 12); assert.equal(result, undefined); assert.ok(host.logger.calls.error.some((m) => /Error handling input/.test(m))); }); test('handle returns a success envelope for statuscheck', async () => { const host = makeHost(); const fc = new FlowController({ host }); const out = await fc.handle('parent', 'statusCheck'); assert.equal(out.status, true); assert.ok(out.feedback.includes('statuscheck')); }); test('handle warns on unimplemented action', async () => { const host = makeHost({ allowedActions: new Set(['weirdaction']) }); const fc = new FlowController({ host }); await fc.handle('parent', 'weirdAction'); assert.ok(host.logger.calls.warn.some((m) => /is not implemented/.test(m))); }); test('constructor validates host', () => { assert.throws(() => new FlowController({}), /ctx\.host is required/); });