// Unit tests for the level-based control strategy. // Run with: node --test test/basic/control-levelBased.basic.test.js const test = require('node:test'); const assert = require('node:assert/strict'); const levelBased = require('../../src/control/levelBased'); function makeMeasurements(levelMeters) { // Minimal MeasurementContainer stand-in. The strategy only calls // getUnit('level') and a chain ending in getCurrentValue(unit). const chain = { type() { return chain; }, variant() { return chain; }, position() { return chain; }, getCurrentValue() { return Number.isFinite(levelMeters) ? levelMeters : null; }, }; return { getUnit: () => 'm', type: () => chain, }; } function makeGroup(name) { const calls = { handleInput: [], turnOff: 0 }; return { config: { general: { name } }, handleInput: async (...args) => { calls.handleInput.push(args); }, turnOffAllMachines: () => { calls.turnOff += 1; }, _calls: calls, }; } function makeCtx(levelMeters, opts = {}) { const groups = { a: makeGroup('A'), b: makeGroup('B'), c: makeGroup('C'), }; return { measurements: makeMeasurements(levelMeters), config: { control: { levelbased: { minLevel: 1, startLevel: 2, maxLevel: 4, ...(opts.levelbased || {}) } }, }, logger: { warn: () => {}, debug: () => {}, info: () => {}, error: () => {} }, machineGroups: groups, machines: {}, levelVariants: ['measured', 'predicted'], }; } test('level < minLevel → STOP: turnOffAllMachines on every group, percControl = 0', async () => { const ctx = makeCtx(0.5); const state = { percControl: 42 }; await levelBased.run(ctx, state); assert.equal(state.percControl, 0); for (const g of Object.values(ctx.machineGroups)) { assert.equal(g._calls.turnOff, 1, 'turnOffAllMachines called once per group'); assert.equal(g._calls.handleInput.length, 0, 'no demand sent in stop zone'); } }); // basin-docs behavior: between minLevel and the active ramp foot, demand // is commanded to 0 % (not "unchanged"). MGC still receives the command; // only the explicit minLevel hard-stop path skips handleInput. test('minLevel ≤ level < ramp foot → commands 0 % without shutdown', async () => { const ctx = makeCtx(1.5); const state = { percControl: 17 }; await levelBased.run(ctx, state); assert.equal(state.percControl, 0, 'percControl driven to 0 in the hold zone'); for (const g of Object.values(ctx.machineGroups)) { assert.equal(g._calls.turnOff, 0); assert.equal(g._calls.handleInput.length, 1, 'one demand=0 forward per group'); assert.deepEqual(g._calls.handleInput[0], ['parent', 0]); } }); test('level == startLevel → percControl == 0 (lower edge of ramp)', async () => { const ctx = makeCtx(2); const state = { percControl: null }; await levelBased.run(ctx, state); assert.equal(state.percControl, 0); }); test('level == maxLevel → percControl == 100 (upper edge of ramp)', async () => { const ctx = makeCtx(4); const state = { percControl: null }; await levelBased.run(ctx, state); assert.equal(state.percControl, 100); }); test('level above maxLevel → percControl clamped at 100 (interpolation limit_input behaviour)', async () => { const ctx = makeCtx(10); const state = { percControl: null }; await levelBased.run(ctx, state); // interpolate_lin_single_point clamps via limit_input(o_min, o_max). assert.equal(state.percControl, 100); }); test('percControl forwarded to every group via handleInput("parent", percControl)', async () => { const ctx = makeCtx(3); // halfway between startLevel=2 and maxLevel=4 → 50% const state = { percControl: null }; await levelBased.run(ctx, state); assert.equal(state.percControl, 50); for (const g of Object.values(ctx.machineGroups)) { assert.equal(g._calls.handleInput.length, 1, 'one forward per group'); assert.deepEqual(g._calls.handleInput[0], ['parent', 50]); assert.equal(g._calls.turnOff, 0); } }); test('no valid level → warns and returns without mutating percControl or calling groups', async () => { const ctx = makeCtx(NaN); let warned = false; ctx.logger.warn = () => { warned = true; }; const state = { percControl: 7 }; await levelBased.run(ctx, state); assert.equal(warned, true); assert.equal(state.percControl, 7); for (const g of Object.values(ctx.machineGroups)) { assert.equal(g._calls.turnOff, 0); assert.equal(g._calls.handleInput.length, 0); } });