const assert = require('assert'); const fs = require('fs'); const path = require('path'); const Monster = require('../src/specificClass'); const { MeasurementContainer } = require('generalFunctions'); function test(name, fn) { try { fn(); console.log(`ok - ${name}`); } catch (err) { console.error(`not ok - ${name}`); console.error(err); process.exitCode = 1; } } function withMockedDate(iso, fn) { const RealDate = Date; let now = new RealDate(iso).getTime(); class MockDate extends RealDate { constructor(...args) { if (args.length === 0) { super(now); } else { super(...args); } } static now() { return now; } } global.Date = MockDate; try { return fn({ advance(ms) { now += ms; } }); } finally { global.Date = RealDate; } } function buildConfig(overrides = {}) { return { general: { name: 'Monster Test', logging: { enabled: false, logLevel: 'error' } }, asset: { emptyWeightBucket: 3 }, constraints: { samplingtime: 1, minVolume: 5, maxWeight: 23, nominalFlowMin: 1, flowMax: 10 }, ...overrides }; } function parseMonsternametijdenCsv(filePath) { const raw = fs.readFileSync(filePath, 'utf8').trim(); const lines = raw.split(/\r?\n/); const header = lines.shift(); const columns = header.split(','); return lines .filter((line) => line && !line.startsWith('-----------')) .map((line) => { const parts = []; let cur = ''; let inQ = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (ch === '"') { inQ = !inQ; continue; } if (ch === ',' && !inQ) { parts.push(cur); cur = ''; } else { cur += ch; } } parts.push(cur); const obj = {}; columns.forEach((col, idx) => { obj[col] = parts[idx]; }); return obj; }); } test('measured + manual flow averages into effective flow', () => { withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => { const monster = new Monster(buildConfig()); const child = { config: { general: { id: 'child-1', name: 'FlowSensor' }, asset: { type: 'flow' } }, measurements: new MeasurementContainer({ autoConvert: true, defaultUnits: { flow: 'm3/h' } }) }; monster.registerChild(child, 'measurement'); child.measurements .type('flow') .variant('measured') .position('downstream') .value(60, Date.now(), 'm3/h'); monster.handleInput('input_q', { value: 20, unit: 'm3/h' }); advance(1000); monster.tick(); assert.strictEqual(monster.q, 40); }); }); test('invalid flow bounds prevent sampling start', () => { const monster = new Monster(buildConfig({ constraints: { samplingtime: 1, minVolume: 5, maxWeight: 23, nominalFlowMin: 10, flowMax: 5 } })); monster.handleInput('i_start', true); monster.sampling_program(); assert.strictEqual(monster.invalidFlowBounds, true); assert.strictEqual(monster.running, false); assert.strictEqual(monster.i_start, false); }); test('flowCalc uses elapsed time to compute m3PerTick', () => { withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => { const monster = new Monster(buildConfig()); monster.q = 36; // m3/h monster.flowCalc(); assert.strictEqual(monster.m3PerTick, 0); advance(10000); monster.flowCalc(); const expected = 0.1; // 36 m3/h -> 0.01 m3/s over 10s assert.ok(Math.abs(monster.m3PerTick - expected) < 1e-6); }); }); test('prediction fallback uses nominalFlowMin * sampling_time when rain is stale', () => { const monster = new Monster(buildConfig()); monster.nominalFlowMin = 4; monster.flowMax = 10; monster.rainMaxRef = 8; monster.sampling_time = 24; monster.lastRainUpdate = 0; const pred = monster.get_model_prediction(); assert.strictEqual(pred, 96); }); test('pulses increment when running with manual flow and zero nominalFlowMin', () => { withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => { const monster = new Monster(buildConfig({ constraints: { samplingtime: 1, minVolume: 5, maxWeight: 23, nominalFlowMin: 0, flowMax: 6000, minSampleIntervalSec: 60, maxRainRef: 10 } })); monster.handleInput('input_q', { value: 200, unit: 'm3/h' }); monster.handleInput('i_start', true); for (let i = 0; i < 80; i++) { advance(1000); monster.tick(); } assert.ok(monster.sumPuls > 0); assert.ok(monster.bucketVol > 0); assert.ok(monster.missedSamples > 0); assert.ok(monster.getSampleCooldownMs() > 0); }); }); test('rain data aggregation produces totals', () => { const monster = new Monster(buildConfig()); const rainPath = path.join(__dirname, 'seed_data', 'raindataFormat.json'); const rainData = JSON.parse(fs.readFileSync(rainPath, 'utf8')); monster.updateRainData(rainData); assert.ok(Object.keys(monster.aggregatedOutput).length > 0); assert.ok(monster.sumRain >= 0); assert.ok(monster.avgRain >= 0); }); test('monsternametijden schedule sets next date', () => { withMockedDate('2024-10-15T00:00:00Z', () => { const monster = new Monster(buildConfig()); const csvPath = path.join(__dirname, 'seed_data', 'monsternametijden.csv'); const rows = parseMonsternametijdenCsv(csvPath); monster.aquonSampleName = '112100'; monster.updateMonsternametijden(rows); const nextDate = monster.nextDate instanceof Date ? monster.nextDate.getTime() : Number(monster.nextDate); assert.ok(Number.isFinite(nextDate)); assert.ok(nextDate > Date.now()); }); }); test('output includes pulse and flow fields', () => { const monster = new Monster(buildConfig()); const output = monster.getOutput(); assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulse')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'q')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPuls')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPulse')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulsesRemaining')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulseFraction')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'flowToNextPulseM3')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'timeToNextPulseSec')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetVolumeM3')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetProgressPct')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaM3')); assert.ok(Object.prototype.hasOwnProperty.call(output, 'predictedRateM3h')); });