Files
measurement/test/basic/calibrator.basic.test.js
znetsixe b990f67df1 P3 wave 1: extract measurement simulator/calibration/commands + CONTRACT
src/simulation/simulator.js  random-walk generator (was simulateInput inline)
  src/calibration/calibrator.js  calibrate + isStable + evaluateRepeatability,
                                using generalFunctions/stats. NB: isStable
                                tautology preserved verbatim — see
                                OPEN_QUESTIONS.md 2026-05-10 for the bug.
  src/commands/                  registry + handlers (canonical names from start)
  CONTRACT.md                    inputs/outputs/events surface

77 basic tests pass (62 pre-refactor + 15 new across the three new files).
specificClass.js / nodeClass.js untouched — integration is P3 wave 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:32:26 +02:00

113 lines
4.1 KiB
JavaScript

'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);
});