Files
pumpingStation/test/basic/calibration.basic.test.js
znetsixe 7afcd6e54a 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>
2026-05-10 20:18:49 +02:00

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));
});