131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
|
||
|
|
const DriftAssessor = require('../../src/drift/driftAssessor');
|
||
|
|
|
||
|
|
/* ---- fakes ---- */
|
||
|
|
function fakeMeasurements(predictedValue) {
|
||
|
|
return {
|
||
|
|
type() { return this; },
|
||
|
|
variant() { return this; },
|
||
|
|
position() { return this; },
|
||
|
|
getCurrentValue() { return predictedValue; },
|
||
|
|
getAllValues() { return { values: [predictedValue], timestamps: [1] }; },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeErrorMetrics(driftFactory) {
|
||
|
|
return {
|
||
|
|
assessPoint: (metricId, predicted, measured, opts) => driftFactory(metricId, predicted, measured, opts),
|
||
|
|
assessDrift: () => ({ nrmse: 0.1, valid: true }),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const SILENT = { warn() {}, debug() {} };
|
||
|
|
|
||
|
|
test('updateMetricDrift returns drift object when predicted+measured both finite', () => {
|
||
|
|
const drift = { valid: true, nrmse: 0.05, immediateLevel: 0, longTermLevel: 0 };
|
||
|
|
const assessor = new DriftAssessor({
|
||
|
|
errorMetrics: makeErrorMetrics(() => drift),
|
||
|
|
measurements: fakeMeasurements(10),
|
||
|
|
driftProfiles: { flow: {} },
|
||
|
|
logger: SILENT,
|
||
|
|
});
|
||
|
|
|
||
|
|
const out = assessor.updateMetricDrift('flow', 11);
|
||
|
|
assert.deepEqual(out, drift);
|
||
|
|
assert.equal(assessor.latest.flow, drift);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('updateMetricDrift returns null when predicted is non-finite', () => {
|
||
|
|
const assessor = new DriftAssessor({
|
||
|
|
errorMetrics: makeErrorMetrics(() => ({ valid: true })),
|
||
|
|
measurements: fakeMeasurements(NaN),
|
||
|
|
driftProfiles: {},
|
||
|
|
logger: SILENT,
|
||
|
|
});
|
||
|
|
assert.equal(assessor.updateMetricDrift('flow', 5), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('updateMetricDrift catches errorMetrics throw and logs', () => {
|
||
|
|
const warns = [];
|
||
|
|
const assessor = new DriftAssessor({
|
||
|
|
errorMetrics: { assessPoint() { throw new Error('boom'); } },
|
||
|
|
measurements: fakeMeasurements(10),
|
||
|
|
driftProfiles: {},
|
||
|
|
logger: { warn(m) { warns.push(m); }, debug() {} },
|
||
|
|
});
|
||
|
|
const out = assessor.updateMetricDrift('flow', 11);
|
||
|
|
assert.equal(out, null);
|
||
|
|
assert.match(warns[0], /Drift update failed for metric 'flow'/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyDriftPenalty leaves confidence unchanged for null/invalid drift', () => {
|
||
|
|
const assessor = new DriftAssessor({ logger: SILENT });
|
||
|
|
const flags = [];
|
||
|
|
assert.equal(assessor.applyDriftPenalty(null, 0.9, flags, 'flow'), 0.9);
|
||
|
|
assert.equal(assessor.applyDriftPenalty({ valid: false }, 0.9, flags, 'flow'), 0.9);
|
||
|
|
assert.deepEqual(flags, []);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyDriftPenalty level 1 reduces confidence by 0.1 + flag', () => {
|
||
|
|
const assessor = new DriftAssessor({ logger: SILENT });
|
||
|
|
const flags = [];
|
||
|
|
const c = assessor.applyDriftPenalty(
|
||
|
|
{ valid: true, nrmse: 0.1, immediateLevel: 1, longTermLevel: 0 },
|
||
|
|
0.9, flags, 'flow',
|
||
|
|
);
|
||
|
|
assert.ok(Math.abs(c - 0.8) < 1e-9);
|
||
|
|
assert.deepEqual(flags, ['flow_low_immediate_drift']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyDriftPenalty level 2 reduces confidence by 0.2 + flag', () => {
|
||
|
|
const assessor = new DriftAssessor({ logger: SILENT });
|
||
|
|
const flags = [];
|
||
|
|
const c = assessor.applyDriftPenalty(
|
||
|
|
{ valid: true, nrmse: 0.2, immediateLevel: 2, longTermLevel: 0 },
|
||
|
|
0.9, flags, 'power',
|
||
|
|
);
|
||
|
|
assert.ok(Math.abs(c - 0.7) < 1e-9);
|
||
|
|
assert.deepEqual(flags, ['power_medium_immediate_drift']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyDriftPenalty level 3 reduces confidence by 0.3 + flag', () => {
|
||
|
|
const assessor = new DriftAssessor({ logger: SILENT });
|
||
|
|
const flags = [];
|
||
|
|
const c = assessor.applyDriftPenalty(
|
||
|
|
{ valid: true, nrmse: 0.5, immediateLevel: 3, longTermLevel: 0 },
|
||
|
|
0.9, flags, 'flow',
|
||
|
|
);
|
||
|
|
assert.ok(Math.abs(c - 0.6) < 1e-9);
|
||
|
|
assert.deepEqual(flags, ['flow_high_immediate_drift']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyDriftPenalty stacks long-term penalty', () => {
|
||
|
|
const assessor = new DriftAssessor({ logger: SILENT });
|
||
|
|
const flags = [];
|
||
|
|
const c = assessor.applyDriftPenalty(
|
||
|
|
{ valid: true, nrmse: 0.4, immediateLevel: 2, longTermLevel: 2 },
|
||
|
|
0.9, flags, 'flow',
|
||
|
|
);
|
||
|
|
assert.ok(Math.abs(c - 0.6) < 1e-9);
|
||
|
|
assert.deepEqual(flags, ['flow_medium_immediate_drift', 'flow_long_term_drift']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('assessDrift returns null if no stored series', () => {
|
||
|
|
const assessor = new DriftAssessor({
|
||
|
|
errorMetrics: makeErrorMetrics(() => ({ valid: true })),
|
||
|
|
measurements: {
|
||
|
|
type() { return this; },
|
||
|
|
variant() { return this; },
|
||
|
|
position() { return this; },
|
||
|
|
getAllValues() { return {}; },
|
||
|
|
},
|
||
|
|
driftProfiles: {},
|
||
|
|
logger: SILENT,
|
||
|
|
});
|
||
|
|
assert.equal(assessor.assessDrift('flow', 0, 1), null);
|
||
|
|
});
|