const test = require('node:test'); const assert = require('node:assert/strict'); // Local stub for groupCurves — replace once ../groupOps/groupCurves lands. const groupCurves = { groupFlow: (m) => m.predictFlow, groupPower: (m) => m.predictPower, groupNCog: (m) => m.NCog ?? 0, groupCalcPower: (m, f) => m.inputFlowCalcPower(f), }; const { validPumpCombinations, checkSpecialCases } = require('../../src/combinatorics/pumpCombinations'); function makeMachine({ id, state = 'off', mode = 'auto', fMin = 0, fMax = 100, pMax = 100, NCog = 0.5, validAction = true } = {}) { return { config: { general: { id } }, state: { getCurrentState: () => state }, currentMode: mode, NCog, predictFlow: { currentFxyYMin: fMin, currentFxyYMax: fMax }, predictPower: { currentFxyYMin: 0, currentFxyYMax: pMax }, inputFlowCalcPower: (flow) => flow * 0.5, isValidActionForMode: () => validAction, }; } const POSITIONS = { DOWNSTREAM: 'downstream' }; const baseCtx = (extra = {}) => ({ groupCurves, logger: { warn: () => {}, debug: () => {}, error: () => {} }, readChildMeasurement: () => undefined, POSITIONS, unitPolicy: { canonical: { flow: 'm3/s' } }, ...extra, }); test('validPumpCombinations: 3 idle machines + Qd in range returns subsets that can deliver', () => { const machines = { a: makeMachine({ id: 'a', state: 'idle', fMin: 10, fMax: 50 }), b: makeMachine({ id: 'b', state: 'idle', fMin: 10, fMax: 50 }), c: makeMachine({ id: 'c', state: 'idle', fMin: 10, fMax: 50 }), }; const combos = validPumpCombinations(machines, 40, baseCtx()); assert.ok(combos.length > 0, 'expected at least one combination'); // every combination must be able to deliver Qd for (const subset of combos) { const maxF = subset.reduce((s, id) => s + machines[id].predictFlow.currentFxyYMax, 0); const minF = subset.reduce((s, id) => s + machines[id].predictFlow.currentFxyYMin, 0); assert.ok(maxF >= 40); assert.ok(minF <= 40); } }); test('validPumpCombinations: excludes machines in off/coolingdown/stopping/emergencystop', () => { const machines = { a: makeMachine({ id: 'a', state: 'off', fMin: 10, fMax: 50 }), b: makeMachine({ id: 'b', state: 'coolingdown', fMin: 10, fMax: 50 }), c: makeMachine({ id: 'c', state: 'stopping', fMin: 10, fMax: 50 }), d: makeMachine({ id: 'd', state: 'emergencystop', fMin: 10, fMax: 50 }), e: makeMachine({ id: 'e', state: 'idle', fMin: 10, fMax: 50 }), }; const combos = validPumpCombinations(machines, 30, baseCtx()); // Only "e" can be in a combination for (const subset of combos) { for (const id of subset) assert.equal(id, 'e'); } }); test('checkSpecialCases: reduces Qd by flow of manually controlled operational machines', () => { const machines = { a: makeMachine({ id: 'a', state: 'operational', mode: 'virtualControl' }), b: makeMachine({ id: 'b', state: 'idle' }), }; const ctx = baseCtx({ readChildMeasurement: (m, type, variant) => { if (m.config.general.id === 'a' && variant === 'measured') return 12; return undefined; }, }); const adjusted = checkSpecialCases(machines, 50, ctx); assert.equal(adjusted, 38); }); test('validPumpCombinations: no machines returns empty array', () => { const combos = validPumpCombinations({}, 10, baseCtx()); assert.deepEqual(combos, []); });