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.4 KiB
JavaScript
107 lines
3.4 KiB
JavaScript
// Basic unit tests for BasinGeometry.
|
||
// Run with: node --test test/basic/BasinGeometry.basic.test.js
|
||
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
|
||
const BasinGeometry = require('../../src/basin/BasinGeometry');
|
||
|
||
function makeBasin(overrides = {}) {
|
||
const basin = {
|
||
volume: 50,
|
||
height: 5,
|
||
inflowLevel: 3,
|
||
outflowLevel: 0.2,
|
||
overflowLevel: 4.5,
|
||
...overrides.basin,
|
||
};
|
||
const hydraulics = {
|
||
minHeightBasedOn: 'outlet',
|
||
...overrides.hydraulics,
|
||
};
|
||
return new BasinGeometry(basin, hydraulics);
|
||
}
|
||
|
||
test('constructor produces correct surfaceArea = volume / height', () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.surfaceArea, 10); // 50 / 5
|
||
assert.equal(g.heightBasin, 5);
|
||
assert.equal(g.volEmptyBasin, 50);
|
||
});
|
||
|
||
test('maxVolAtOverflow equals overflowLevel × surfaceArea', () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.maxVolAtOverflow, 4.5 * 10); // 45
|
||
assert.equal(g.minVolAtInflow, 3 * 10); // 30
|
||
assert.equal(g.minVolAtOutflow, 0.2 * 10); // 2
|
||
assert.equal(g.maxVol, 50);
|
||
});
|
||
|
||
test("minVol selects outlet-based when minHeightBasedOn = 'outlet'", () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.minVol, g.minVolAtOutflow);
|
||
assert.equal(g.minHeightBasedOn, 'outlet');
|
||
});
|
||
|
||
test("minVol selects inlet-based when minHeightBasedOn = 'inlet'", () => {
|
||
const g = makeBasin({ hydraulics: { minHeightBasedOn: 'inlet' } });
|
||
assert.equal(g.minVol, g.minVolAtInflow);
|
||
assert.equal(g.minHeightBasedOn, 'inlet');
|
||
});
|
||
|
||
test('volumeFromLevel(0) returns 0; negative level clamps to 0', () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.volumeFromLevel(0), 0);
|
||
assert.equal(g.volumeFromLevel(-1), 0);
|
||
assert.equal(g.volumeFromLevel(-1e9), 0);
|
||
});
|
||
|
||
test('volumeFromLevel(positive) is level × surfaceArea', () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.volumeFromLevel(2.5), 25);
|
||
assert.equal(g.volumeFromLevel(5), 50);
|
||
});
|
||
|
||
test('levelFromVolume(maxVol) returns heightBasin', () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.levelFromVolume(g.maxVol), g.heightBasin);
|
||
});
|
||
|
||
test('levelFromVolume(0) returns 0; negative volume clamps to 0', () => {
|
||
const g = makeBasin();
|
||
assert.equal(g.levelFromVolume(0), 0);
|
||
assert.equal(g.levelFromVolume(-10), 0);
|
||
});
|
||
|
||
test('round-trip: volumeFromLevel(levelFromVolume(v)) ≈ v for v in range', () => {
|
||
const g = makeBasin();
|
||
for (const v of [0, 0.001, 1, 12.34, 25, 49.999, 50]) {
|
||
const back = g.volumeFromLevel(g.levelFromVolume(v));
|
||
assert.ok(Math.abs(back - v) < 1e-9, `round-trip failed for v=${v}, got ${back}`);
|
||
}
|
||
});
|
||
|
||
test('round-trip: levelFromVolume(volumeFromLevel(L)) ≈ L for L in range', () => {
|
||
const g = makeBasin();
|
||
for (const L of [0, 0.05, 1, 2.5, 4.5, 5]) {
|
||
const back = g.levelFromVolume(g.volumeFromLevel(L));
|
||
assert.ok(Math.abs(back - L) < 1e-9, `round-trip failed for L=${L}, got ${back}`);
|
||
}
|
||
});
|
||
|
||
test('snapshot() exposes legacy this.basin field names', () => {
|
||
const g = makeBasin();
|
||
const s = g.snapshot();
|
||
const expectedKeys = [
|
||
'volEmptyBasin', 'heightBasin', 'inflowLevel', 'outflowLevel',
|
||
'overflowLevel', 'surfaceArea', 'maxVol', 'maxVolAtOverflow',
|
||
'minVolAtInflow', 'minVolAtOutflow', 'minVol', 'minHeightBasedOn',
|
||
];
|
||
for (const k of expectedKeys) {
|
||
assert.ok(k in s, `snapshot missing key: ${k}`);
|
||
}
|
||
assert.equal(s.volEmptyBasin, 50);
|
||
assert.equal(s.surfaceArea, 10);
|
||
assert.equal(s.minHeightBasedOn, 'outlet');
|
||
});
|