// Basic unit tests for thresholdValidator. // Run with: node --test test/basic/thresholdValidator.basic.test.js const test = require('node:test'); const assert = require('node:assert/strict'); const { validateThresholdOrdering } = require('../../src/basin/thresholdValidator'); const BasinGeometry = require('../../src/basin/BasinGeometry'); // A valid baseline: outlet 0.2 < inflow 3 < overflow 4.5 ≤ height 5, // dryRun = 0.2 * 1.10 = 0.22 ≤ minLevel 1 ≤ start 2 < max 4 ≤ overfill 4.275. function validBasinAndCfg() { const basin = new BasinGeometry( { volume: 50, height: 5, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 4.5 }, { minHeightBasedOn: 'outlet' } ); const levelbased = { minLevel: 1, startLevel: 2, maxLevel: 4 }; const safety = { dryRunThresholdPercent: 10, overfillThresholdPercent: 95 }; return { basin, levelbased, safety }; } test('valid ordering returns empty array', () => { const { basin, levelbased, safety } = validBasinAndCfg(); const issues = validateThresholdOrdering(basin, levelbased, safety); assert.deepEqual(issues, []); }); test('outflowLevel >= inflowLevel triggers issue with correct shape', () => { const basin = new BasinGeometry( // outflow 3.5 > inflow 3 — invariant broken. { volume: 50, height: 5, inflowLevel: 3, outflowLevel: 3.5, overflowLevel: 4.5 }, { minHeightBasedOn: 'outlet' } ); const issues = validateThresholdOrdering(basin, { minLevel: 1, startLevel: 2, maxLevel: 4 }, { dryRunThresholdPercent: 0, overfillThresholdPercent: 100 }); const hit = issues.find((i) => i.aName === 'outflowLevel' && i.bName === 'inflowLevel'); assert.ok(hit, 'expected an outflowLevel < inflowLevel issue'); assert.equal(hit.op, '<'); assert.equal(hit.a, 3.5); assert.equal(hit.b, 3); assert.match(hit.msg, /outflowLevel.*<.*inflowLevel/); }); test('maxLevel >= overfillLevel triggers issue', () => { const { basin } = validBasinAndCfg(); // overfillLevel = overflowLevel × overfillPct/100 = 4.5 × 0.80 = 3.6. // maxLevel 4 > 3.6 → expect a `maxLevel <= overfillLevel` issue. const issues = validateThresholdOrdering( basin, { minLevel: 1, startLevel: 2, maxLevel: 4 }, { dryRunThresholdPercent: 10, overfillThresholdPercent: 80 } ); const hit = issues.find((i) => i.aName === 'maxLevel' && i.bName === 'overfillLevel'); assert.ok(hit, 'expected a maxLevel <= overfillLevel issue'); assert.equal(hit.op, '<='); assert.equal(hit.a, 4); assert.ok(Math.abs(hit.b - 3.6) < 1e-9); }); test('NaN / undefined values are skipped, not flagged as issues', () => { const { basin } = validBasinAndCfg(); const issues = validateThresholdOrdering( basin, { minLevel: undefined, startLevel: NaN, maxLevel: 4 }, { dryRunThresholdPercent: 10, overfillThresholdPercent: 95 } ); // dryRunLevel <= minLevel skipped (minLevel undefined → NaN) // minLevel <= startLevel skipped (both NaN-ish) // startLevel < maxLevel skipped (startLevel NaN) // maxLevel <= overfillLevel still checked → 4 ≤ 4.275 OK. // Geometry checks also OK. assert.deepEqual(issues, []); }); test('multiple violations produce multiple issues in stable order', () => { // Build a basin with two geometry violations. const basin = new BasinGeometry( // outflow 4 > inflow 3 (broken) AND overflow 6 > height 5 (broken) { volume: 50, height: 5, inflowLevel: 3, outflowLevel: 4, overflowLevel: 6 }, { minHeightBasedOn: 'outlet' } ); const issues = validateThresholdOrdering( basin, { minLevel: 1, startLevel: 2, maxLevel: 4 }, { dryRunThresholdPercent: 0, overfillThresholdPercent: 100 } ); // Expect at least the two geometry issues, in declaration order: // outflowLevel < inflowLevel comes before overflowLevel <= basinHeight. const idxOutflow = issues.findIndex((i) => i.aName === 'outflowLevel'); const idxOverflow = issues.findIndex((i) => i.aName === 'overflowLevel' && i.bName === 'basinHeight'); assert.ok(idxOutflow >= 0, 'expected outflowLevel issue'); assert.ok(idxOverflow >= 0, 'expected overflowLevel <= basinHeight issue'); assert.ok(idxOutflow < idxOverflow, 'issues should be in check-declaration order'); }); test('accepts a plain basin object (duck-typed via getters)', () => { const plainBasin = { volEmptyBasin: 50, heightBasin: 5, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 4.5, surfaceArea: 10, maxVol: 50, maxVolAtOverflow: 45, minVolAtInflow: 30, minVolAtOutflow: 2, minVol: 2, minHeightBasedOn: 'outlet', }; const issues = validateThresholdOrdering( plainBasin, { minLevel: 1, startLevel: 2, maxLevel: 4 }, { dryRunThresholdPercent: 10, overfillThresholdPercent: 95 } ); assert.deepEqual(issues, []); }); test('omitted levelbased / safety objects are tolerated', () => { const { basin } = validBasinAndCfg(); // No control or safety supplied → only geometry checks run; valid basin geometry → [] const issues = validateThresholdOrdering(basin, undefined, undefined); assert.deepEqual(issues, []); });