src/groupOps/ groupOperatingPoint + groupCurves (pure functions)
src/totals/ totalsCalculator (dynamic + absolute + active)
src/combinatorics/ pumpCombinations (validPumpCombinations + checkSpecialCases)
src/optimizer/ bestCombination (CoG) + bepGravitation (BEP-G + marginal-cost)
src/efficiency/ groupEfficiency (calc + distance helpers)
src/dispatch/ demandDispatcher (LatestWinsGate-based; replaces
_dispatchInFlight + _delayedCall)
src/commands/ canonical names from start (set.mode/scaling/demand,
child.register) + legacy aliases
CONTRACT.md inputs/outputs/events surface
53 basic tests pass (52 new + 1 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P4 wave 2.
Findings flagged via agents (TODO append to OPEN_QUESTIONS.md):
- calcGroupEfficiency.maxEfficiency is actually the mean (misleading name)
- checkSpecialCases has a no-op `return false` inside forEach
- MGC doesn't route cmd.startup/shutdown/estop — confirm if station broadcasts need it
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.4 KiB
JavaScript
67 lines
2.4 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { groupFlow, groupPower, groupNCog, groupCalcPower } = require('../../src/groupOps/groupCurves');
|
|
|
|
function predictView(min, max, current = (min + max) / 2) {
|
|
return {
|
|
currentF: current,
|
|
currentFxyYMin: min,
|
|
currentFxyYMax: max,
|
|
};
|
|
}
|
|
|
|
test('groupFlow returns the same shape as the original _groupFlow (groupPredictFlow preferred)', () => {
|
|
const machine = {
|
|
predictFlow: predictView(0, 1, 0.5),
|
|
groupPredictFlow: predictView(0.1, 0.9, 0.4),
|
|
};
|
|
const v = groupFlow(machine);
|
|
assert.equal(v, machine.groupPredictFlow);
|
|
assert.equal(v.currentFxyYMin, 0.1);
|
|
assert.equal(v.currentFxyYMax, 0.9);
|
|
assert.equal(v.currentF, 0.4);
|
|
});
|
|
|
|
test('groupFlow falls back to predictFlow when groupPredictFlow is absent', () => {
|
|
const machine = { predictFlow: predictView(0, 1) };
|
|
assert.equal(groupFlow(machine), machine.predictFlow);
|
|
});
|
|
|
|
test('groupPower returns groupPredictPower when present, else predictPower', () => {
|
|
const m1 = { predictPower: predictView(0, 100), groupPredictPower: predictView(10, 90) };
|
|
assert.equal(groupPower(m1), m1.groupPredictPower);
|
|
|
|
const m2 = { predictPower: predictView(0, 100) };
|
|
assert.equal(groupPower(m2), m2.predictPower);
|
|
});
|
|
|
|
test('groupNCog returns the group value when groupPredictFlow is present', () => {
|
|
const m = { groupPredictFlow: predictView(0, 1), groupNCog: 0.42, NCog: 0.99, predictFlow: predictView(0, 1) };
|
|
assert.equal(groupNCog(m), 0.42);
|
|
});
|
|
|
|
test('groupNCog falls back to NCog when no groupPredictFlow', () => {
|
|
const m = { predictFlow: predictView(0, 1), NCog: 0.7 };
|
|
assert.equal(groupNCog(m), 0.7);
|
|
});
|
|
|
|
test('groupNCog defaults to 0 when neither is defined', () => {
|
|
const m = { predictFlow: predictView(0, 1) };
|
|
assert.equal(groupNCog(m), 0);
|
|
});
|
|
|
|
test('groupCalcPower prefers machine.groupCalcPower', () => {
|
|
let lastFlow = null;
|
|
const m = {
|
|
groupCalcPower(flow) { lastFlow = flow; return flow * 2; },
|
|
inputFlowCalcPower(flow) { return flow * 999; },
|
|
};
|
|
assert.equal(groupCalcPower(m, 0.3), 0.6);
|
|
assert.equal(lastFlow, 0.3);
|
|
});
|
|
|
|
test('groupCalcPower falls back to inputFlowCalcPower when groupCalcPower missing', () => {
|
|
const m = { inputFlowCalcPower(flow) { return flow + 1; } };
|
|
assert.equal(groupCalcPower(m, 5), 6);
|
|
});
|