99 lines
3.6 KiB
JavaScript
99 lines
3.6 KiB
JavaScript
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
|
||
|
|
const { makeMeasurementInstance } = require('../helpers/factories');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Unit coverage for the three outlier detection strategies shipped by the
|
||
|
|
* measurement node. Each test seeds the storedValues window first, then
|
||
|
|
* probes the classifier directly. This keeps the assertions focused on the
|
||
|
|
* detection logic rather than the full calculateInput pipeline.
|
||
|
|
*/
|
||
|
|
|
||
|
|
function makeDetector(method, threshold) {
|
||
|
|
return makeMeasurementInstance({
|
||
|
|
scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: -1000, absMax: 1000, offset: 0 },
|
||
|
|
smoothing: { smoothWindow: 20, smoothMethod: 'none' },
|
||
|
|
outlierDetection: { enabled: true, method, threshold },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function seed(m, values) {
|
||
|
|
// bypass calculateInput so we don't trigger outlier filtering while seeding
|
||
|
|
m.storedValues = [...values];
|
||
|
|
}
|
||
|
|
|
||
|
|
test("zScore flags a value far above the mean as an outlier", () => {
|
||
|
|
const m = makeDetector('zScore', 3);
|
||
|
|
seed(m, [10, 11, 10, 9, 10, 11, 10, 11, 9, 10]);
|
||
|
|
assert.equal(m.outlierDetection(100), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("zScore does not flag a value inside the distribution", () => {
|
||
|
|
const m = makeDetector('zScore', 3);
|
||
|
|
seed(m, [10, 11, 10, 9, 10, 11, 10, 11, 9, 10]);
|
||
|
|
assert.equal(m.outlierDetection(11), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("iqr flags a value outside Q1/Q3 fences", () => {
|
||
|
|
const m = makeDetector('iqr');
|
||
|
|
seed(m, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||
|
|
assert.equal(m.outlierDetection(100), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("iqr does not flag a value inside Q1/Q3 fences", () => {
|
||
|
|
const m = makeDetector('iqr');
|
||
|
|
seed(m, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||
|
|
assert.equal(m.outlierDetection(5), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("modifiedZScore flags heavy-tailed outliers", () => {
|
||
|
|
const m = makeDetector('modifiedZScore', 3.5);
|
||
|
|
seed(m, [10, 11, 10, 9, 10, 11, 10, 11, 9, 10]);
|
||
|
|
assert.equal(m.outlierDetection(1000), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("modifiedZScore accepts normal data", () => {
|
||
|
|
const m = makeDetector('modifiedZScore', 3.5);
|
||
|
|
seed(m, [10, 11, 10, 9, 10, 11, 10, 11, 9, 10]);
|
||
|
|
assert.equal(m.outlierDetection(11), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("unknown outlier method falls back to schema default (zScore) and still runs", () => {
|
||
|
|
// validateEnum replaces unknown values with the schema default. The
|
||
|
|
// schema default is "zScore"; the dispatcher normalizes to lowercase
|
||
|
|
// and routes to zScoreOutlierDetection. With a tight window, value=100
|
||
|
|
// is a clear outlier -> returns true.
|
||
|
|
const m = makeDetector('bogus', 3);
|
||
|
|
seed(m, [1, 2, 3, 4, 5]);
|
||
|
|
assert.equal(m.outlierDetection(100), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("outlier detection returns false when window has < 2 samples", () => {
|
||
|
|
const m = makeDetector('zScore', 3);
|
||
|
|
m.storedValues = [];
|
||
|
|
assert.equal(m.outlierDetection(500), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("calculateInput ignores a value flagged as outlier", () => {
|
||
|
|
const m = makeDetector('zScore', 3);
|
||
|
|
// Build a tight baseline then throw a spike at it.
|
||
|
|
[10, 10, 10, 10, 10].forEach((v) => m.calculateInput(v));
|
||
|
|
const before = m.outputAbs;
|
||
|
|
m.calculateInput(9999);
|
||
|
|
// Output must not move to the spike (outlier rejected).
|
||
|
|
assert.equal(m.outputAbs, before);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("toggleOutlierDetection flips the flag without corrupting config", () => {
|
||
|
|
const m = makeDetector('zScore', 3);
|
||
|
|
const initial = m.config.outlierDetection.enabled;
|
||
|
|
m.toggleOutlierDetection();
|
||
|
|
assert.equal(m.config.outlierDetection.enabled, !initial);
|
||
|
|
// Re-toggle restores
|
||
|
|
m.toggleOutlierDetection();
|
||
|
|
assert.equal(m.config.outlierDetection.enabled, initial);
|
||
|
|
// Method is preserved (enum values are normalized to lowercase by validateEnum).
|
||
|
|
assert.equal(m.config.outlierDetection.method.toLowerCase(), 'zscore');
|
||
|
|
});
|