const test = require('node:test'); const assert = require('node:assert/strict'); const { makeMeasurementInstance } = require('../helpers/factories'); /** * Baseline coverage for every smoothing method exposed by the measurement * node. Each test forces scaling off + outlier-detection off so we can * assert on the raw smoothing arithmetic. */ function makeSmoother(method, windowSize = 5) { return makeMeasurementInstance({ scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 1000, offset: 0 }, smoothing: { smoothWindow: windowSize, smoothMethod: method }, }); } function feed(m, values) { values.forEach((v) => m.calculateInput(v)); } test("smoothing 'none' returns the latest value", () => { const m = makeSmoother('none'); feed(m, [10, 20, 30, 40, 50]); assert.equal(m.outputAbs, 50); }); test("smoothing 'mean' returns arithmetic mean over window", () => { const m = makeSmoother('mean'); feed(m, [10, 20, 30, 40, 50]); assert.equal(m.outputAbs, 30); }); test("smoothing 'min' returns minimum of window", () => { const m = makeSmoother('min'); feed(m, [10, 20, 5, 40, 50]); assert.equal(m.outputAbs, 5); }); test("smoothing 'max' returns maximum of window", () => { const m = makeSmoother('max'); feed(m, [10, 20, 5, 40, 50]); assert.equal(m.outputAbs, 50); }); test("smoothing 'sd' returns standard deviation of window", () => { const m = makeSmoother('sd'); feed(m, [2, 4, 4, 4, 5]); // Expected sample sd of [2,4,4,4,5] = 1.0954..., rounded to 1.1 by the outputAbs pipeline assert.ok(Math.abs(m.outputAbs - 1.1) < 0.01, `expected ~1.1, got ${m.outputAbs}`); }); test("smoothing 'median' returns median (odd window)", () => { const m = makeSmoother('median'); feed(m, [10, 50, 20, 40, 30]); assert.equal(m.outputAbs, 30); }); test("smoothing 'median' returns average of middle pair (even window)", () => { const m = makeSmoother('median', 4); feed(m, [10, 20, 30, 40]); assert.equal(m.outputAbs, 25); }); test("smoothing 'weightedMovingAverage' weights later samples more", () => { const m = makeSmoother('weightedMovingAverage'); feed(m, [10, 10, 10, 10, 50]); // weights [1,2,3,4,5], sum of weights = 15 // weighted sum = 10+20+30+40+250 = 350 -> 350/15 = 23.333..., rounded 23.33 assert.ok(Math.abs(m.outputAbs - 23.33) < 0.02, `expected ~23.33, got ${m.outputAbs}`); }); test("smoothing 'lowPass' attenuates transients", () => { const m = makeSmoother('lowPass'); feed(m, [0, 0, 0, 0, 100]); // EMA(alpha=0.2) from 0,0,0,0,100: last value should be well below 100. assert.ok(m.outputAbs < 100 * 0.3, `lowPass should attenuate step: ${m.outputAbs}`); assert.ok(m.outputAbs > 0, `lowPass should still react: ${m.outputAbs}`); }); test("smoothing 'highPass' emphasises differences", () => { const m = makeSmoother('highPass'); feed(m, [0, 0, 0, 0, 100]); // Highpass on a step should produce a positive transient; exact value is // recursive but we at least require it to be positive and non-zero. assert.ok(m.outputAbs > 10, `highPass should emphasise step: ${m.outputAbs}`); }); test("smoothing 'bandPass' produces a finite number", () => { const m = makeSmoother('bandPass'); feed(m, [1, 2, 3, 4, 5]); assert.ok(Number.isFinite(m.outputAbs)); }); test("smoothing 'kalman' converges toward steady values", () => { const m = makeSmoother('kalman'); feed(m, [100, 100, 100, 100, 100]); // Kalman filter fed with a constant input should converge to that value // (within a small tolerance due to its gain smoothing). assert.ok(Math.abs(m.outputAbs - 100) < 5, `kalman should approach steady value: ${m.outputAbs}`); }); test("smoothing 'savitzkyGolay' returns last sample when window < 5", () => { const m = makeSmoother('savitzkyGolay', 3); feed(m, [7, 8, 9]); assert.equal(m.outputAbs, 9); }); test("smoothing 'savitzkyGolay' smooths across a 5-point window", () => { const m = makeSmoother('savitzkyGolay', 5); feed(m, [1, 2, 3, 4, 5]); // SG coefficients [-3,12,17,12,-3] / 35 on linear data returns the // middle value unchanged (=3); exact numeric comes out to 35/35 * 3. assert.ok(Math.abs(m.outputAbs - 3) < 0.01, `SG on linear data should return middle ~3, got ${m.outputAbs}`); }); test("unknown smoothing method falls through to raw value with an error", () => { const m = makeSmoother('bogus-method'); // calculateInput will try the unknown key, hit the default branch in the // applySmoothing map, log an error, and return the raw value (as // implemented — the test pins that behaviour). feed(m, [42]); assert.equal(m.outputAbs, 42); }); test("smoothing window shifts oldest value when exceeded", () => { const m = makeSmoother('mean', 3); feed(m, [100, 100, 100, 10, 10, 10]); // Last three values are [10,10,10]; mean = 10. assert.equal(m.outputAbs, 10); });