'use strict'; const test = require('node:test'); const assert = require('node:assert'); const Calibrator = require('../../src/calibration/calibrator.js'); // Tiny logger spy so we can assert on warn() without pulling in the real // generalFunctions logger. function makeLogger() { const calls = { warn: [], info: [], debug: [], error: [] }; return { calls, warn: (m) => calls.warn.push(m), info: (m) => calls.info.push(m), debug: (m) => calls.debug.push(m), error: (m) => calls.error.push(m), }; } function makeCalibrator(values, config) { const logger = makeLogger(); const cal = new Calibrator({ storedValuesRef: () => values, configRef: () => config, logger, }); return { cal, logger }; } test('isStable: constant array → stable with stdDev=0', () => { const { cal } = makeCalibrator([5, 5, 5, 5], {}); const r = cal.isStable(); assert.strictEqual(r.isStable, true); assert.strictEqual(r.stdDev, 0); }); test('isStable: high-variance array → original threshold is tautological (preserved)', () => { // BUG-PRESERVED: original check is `stdDev < stdDev*marginFactor`, which is // always true for stdDev>0. Length>=2 ⇒ isStable=true regardless of spread. // See calibrator stdDev-threshold note. We pin the behaviour here so the // refactor stays byte-equivalent; a separate behavioural PR can fix the rule. const { cal } = makeCalibrator([0, 100, 0, 100], {}); const r = cal.isStable(); assert.strictEqual(r.isStable, true); assert.ok(r.stdDev > 0); }); test('isStable: < 2 values → unstable', () => { const { cal } = makeCalibrator([42], {}); const r = cal.isStable(); assert.strictEqual(r.isStable, false); assert.strictEqual(r.stdDev, 0); }); test('calibrate: scaling enabled → offset = inputMin - currentOutputAbs', () => { const cfg = { scaling: { enabled: true, inputMin: 4, absMin: 0 } }; const { cal } = makeCalibrator([10, 10, 10], cfg); const r = cal.calibrate(10); assert.deepStrictEqual(r, { offset: -6 }); }); test('calibrate: scaling disabled → offset = absMin - currentOutputAbs', () => { const cfg = { scaling: { enabled: false, inputMin: 4, absMin: 1 } }; const { cal } = makeCalibrator([7, 7, 7], cfg); const r = cal.calibrate(7); assert.deepStrictEqual(r, { offset: -6 }); }); test('calibrate: not stable (length<2) → returns null and logs warn', () => { // Original rule has a tautological threshold, so "unstable" only triggers // when the rolling window has < 2 samples. const cfg = { scaling: { enabled: true, inputMin: 0, absMin: 0 } }; const { cal, logger } = makeCalibrator([], cfg); const r = cal.calibrate(50); assert.strictEqual(r, null); assert.strictEqual(logger.calls.warn.length, 1); assert.match(logger.calls.warn[0], /Calibration aborted/); }); test('evaluateRepeatability: smoothing=none → null', () => { const cfg = { smoothing: { smoothMethod: 'none' } }; const { cal, logger } = makeCalibrator([5, 5, 5], cfg); const r = cal.evaluateRepeatability(); assert.strictEqual(r.repeatability, null); assert.strictEqual(r.reason, 'smoothing-disabled'); assert.match(logger.calls.warn[0], /without smoothing/); }); test('evaluateRepeatability: stable + smoothed → returns stdDev', () => { const cfg = { smoothing: { smoothMethod: 'mean' } }; const { cal } = makeCalibrator([3, 3, 3, 3], cfg); const r = cal.evaluateRepeatability(); assert.strictEqual(r.repeatability, 0); }); test('evaluateRepeatability: insufficient data → null', () => { const cfg = { smoothing: { smoothMethod: 'mean' } }; const { cal } = makeCalibrator([5], cfg); const r = cal.evaluateRepeatability(); assert.strictEqual(r.repeatability, null); assert.strictEqual(r.reason, 'insufficient-data'); }); test('evaluateRepeatability: high-variance still returns stdDev (preserved tautology)', () => { // BUG-PRESERVED: see isStable note. Original rule treats any length>=2 // buffer as stable, so repeatability returns the raw stdDev even when the // spread is large. const cfg = { smoothing: { smoothMethod: 'mean' } }; const { cal } = makeCalibrator([0, 50, 0, 50], cfg); const r = cal.evaluateRepeatability(); assert.ok(r.repeatability > 0); });