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>
107 lines
3.6 KiB
JavaScript
107 lines
3.6 KiB
JavaScript
// Basic tests for the calibration helpers.
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { MeasurementContainer } = require('generalFunctions');
|
|
const {
|
|
calibratePredictedVolume,
|
|
calibratePredictedLevel,
|
|
setManualInflow,
|
|
} = require('../../src/measurement/calibration');
|
|
|
|
function makeBasin() {
|
|
return {
|
|
surfaceArea: 10,
|
|
minVol: 2,
|
|
maxVol: 50,
|
|
maxVolAtOverflow: 45,
|
|
overflowLevel: 4.5,
|
|
outflowLevel: 0.2,
|
|
inflowLevel: 3,
|
|
};
|
|
}
|
|
|
|
function makeCtx(seedVolume = null) {
|
|
const measurements = new MeasurementContainer({
|
|
autoConvert: true,
|
|
preferredUnits: { flow: 'm3/s', level: 'm', volume: 'm3' },
|
|
});
|
|
const basin = makeBasin();
|
|
if (seedVolume != null) {
|
|
measurements.type('volume').variant('predicted').position('atequipment')
|
|
.value(seedVolume, Date.now() - 5_000, 'm3').unit('m3');
|
|
}
|
|
const ctx = { measurements, basin };
|
|
return ctx;
|
|
}
|
|
|
|
test('calibratePredictedVolume clears prior series and writes new value', async () => {
|
|
const ctx = makeCtx(12);
|
|
const before = ctx.measurements.type('volume').variant('predicted').position('atequipment')
|
|
.getCurrentValue('m3');
|
|
assert.ok(Math.abs(before - 12) < 1e-9);
|
|
|
|
const ts = Date.now();
|
|
calibratePredictedVolume(ctx, 30, ts);
|
|
|
|
const m = ctx.measurements.type('volume').variant('predicted').position('atequipment').get();
|
|
assert.equal(m.values.length, 1, 'series should hold exactly the calibration point');
|
|
assert.ok(Math.abs(m.getCurrentValue() - 30) < 1e-9);
|
|
|
|
// Level was derived: 30 / 10 = 3 m.
|
|
const lvl = ctx.measurements.type('level').variant('predicted').position('atequipment')
|
|
.getCurrentValue('m');
|
|
assert.ok(Math.abs(lvl - 3) < 1e-9, `derived level was ${lvl}`);
|
|
|
|
assert.equal(ctx._predictedFlowState.lastTimestamp, ts);
|
|
assert.equal(ctx._predictedFlowState.inflow, 0);
|
|
assert.equal(ctx._predictedFlowState.outflow, 0);
|
|
});
|
|
|
|
test('calibratePredictedLevel writes both level and derived volume', async () => {
|
|
const ctx = makeCtx(2);
|
|
calibratePredictedLevel(ctx, 4.0, Date.now(), 'm');
|
|
|
|
const lvl = ctx.measurements.type('level').variant('predicted').position('atequipment')
|
|
.getCurrentValue('m');
|
|
assert.ok(Math.abs(lvl - 4.0) < 1e-9);
|
|
|
|
const vol = ctx.measurements.type('volume').variant('predicted').position('atequipment')
|
|
.getCurrentValue('m3');
|
|
assert.ok(Math.abs(vol - 40) < 1e-9, `derived volume was ${vol}`);
|
|
});
|
|
|
|
test('setManualInflow writes to flow.predicted.in.manual-qin', async () => {
|
|
const ctx = makeCtx();
|
|
const ts = Date.now();
|
|
setManualInflow(ctx, 0.025, ts, 'm3/s');
|
|
|
|
const series = ctx.measurements.type('flow').variant('predicted').position('in').child('manual-qin');
|
|
const val = series.getCurrentValue('m3/s');
|
|
assert.ok(Math.abs(val - 0.025) < 1e-9, `manual-qin value was ${val}`);
|
|
|
|
// It must NOT collide with the default child bucket.
|
|
const defaultBucket = ctx.measurements.measurements?.flow?.predicted?.in?.default;
|
|
assert.equal(defaultBucket, undefined);
|
|
});
|
|
|
|
test('calibration uses ctx.flowAggregator.resetState when present', async () => {
|
|
const ctx = makeCtx(5);
|
|
let resetCalled = null;
|
|
ctx.flowAggregator = { resetState: (ts) => { resetCalled = ts; } };
|
|
|
|
const ts = 1234567890;
|
|
calibratePredictedVolume(ctx, 20, ts);
|
|
|
|
assert.equal(resetCalled, ts);
|
|
// The plain bag should NOT be touched when the aggregator hook is present.
|
|
assert.equal(ctx._predictedFlowState, undefined);
|
|
});
|
|
|
|
test('calibratePredictedVolume rejects bad context', async () => {
|
|
assert.throws(() => calibratePredictedVolume({}, 10));
|
|
assert.throws(() => calibratePredictedLevel({}, 1.0));
|
|
assert.throws(() => setManualInflow({}, 0.01));
|
|
});
|