P2 wave 1: extract concerns from pumpingStation specificClass
Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.
src/basin/ BasinGeometry + thresholdValidator (pure)
src/measurement/ flowAggregator + measurementRouter + calibration
src/control/ levelBased + flowBased(stub) + manual + index dispatcher
src/safety/ safetyController split into dryRun + overfill rules
src/commands/ registry array + handlers (canonical names from start)
src/editor.js 260 lines of SVG basin-diagram redraw, was inline in .html
examples/standalone-demo.js was if(require.main===module) at bottom of specificClass.js
CONTRACT.md canonical inputs + outputs + emitted events
Modified:
src/specificClass.js removed the 170-line standalone demo block
pumpingStation.html oneditprepare/oneditsave delegate to editor.{init,save}
pumpingStation.js added admin endpoint serving src/editor.js
102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
test/basic/control-levelBased.basic.test.js
Normal file
126
test/basic/control-levelBased.basic.test.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
test('minLevel ≤ level < startLevel → DEAD ZONE: no calls, percControl unchanged', async () => {
|
||||
const ctx = makeCtx(1.5);
|
||||
const state = { percControl: 17 };
|
||||
await levelBased.run(ctx, state);
|
||||
|
||||
assert.equal(state.percControl, 17, 'percControl untouched in dead zone');
|
||||
for (const g of Object.values(ctx.machineGroups)) {
|
||||
assert.equal(g._calls.turnOff, 0);
|
||||
assert.equal(g._calls.handleInput.length, 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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user