[Task]: ML test suite passes with a detector that never fires and one that always fires
Problem / root cause
Two degenerate versions of the anomaly detector pass the entire ML test suite. ml_dimension_predict, the function that decides whether a dimension is anomalous, is never exercised by a test.
I built current master with -DENABLE_ML=On and ran netdata -W mltest. Baseline is 606 assertions, 0 failed. I then made one-line changes to ml_dimension_predict in src/ml/ml.cc and rebuilt.
Mutation 1, the detector can never report an anomaly:
if (anomaly_score < (100 * Cfg.dimension_anomaly_score_threshold)) {
...
return false;
}
sum += 0; // was: sum += 1
ML tests: 606 run, 0 failed
ML TESTS PASSED
Mutation 2, every dimension is always anomalous (100% false-positive rate):
if (false) { // was: if (anomaly_score < (100 * Cfg.dimension_anomaly_score_threshold)) {
ML tests: 606 run, 0 failed
ML TESTS PASSED
Root cause, three parts:
ml_dimension_predictis never called by the suite.grep -n ml_dimension_predict src/ml/ml-unittest.ccreturns one hit, inside a comment on line 635. The threshold comparison, thesumaccumulation and the suppression logic are untested.The scoring formula is tested, the decision is not.
test_kmeans_scoringcoversml_kmeans_anomaly_scorewell enough that mutating it directly does get caught. Both mutations above leave that function untouched and change only the decision built on top of it.The one detection-shaped assertion is not tied to the decision threshold. In
test_full_pipeline:
ML_TEST_ASSERT_DOUBLE_EQ(normal_score, 0.0, 1e-6, "best training sample should score ~0");
...
ML_TEST_ASSERT(anomaly_score > normal_score, "anomalous data should score higher than normal");
normal_score is the minimum score across training samples, asserted to be about 0. So the bar is that an anomalous sample scores above roughly zero, while ml_dimension_predict only declares an anomaly at 100 * dimension_anomaly_score_threshold, which is 99.0 at the default 0.99. A detector whose anomalous scores top out at 0.5 satisfies the assertion and detects nothing in production.
The rest of the suite is thorough on the machinery: feature preprocessing, kmeans training and serialization, circular buffer equivalence, queue and model-publish behaviour, config parameter combinations. The gap is specifically the detection outcome.
Clean end state
src/ml/ml-unittest.cc covers the detection decision, not only the machinery that feeds it. A change that breaks detection while leaving the scoring formula intact fails the suite. Specifically, ml_dimension_predict is driven end to end, and at least one assertion is expressed in terms of dimension_anomaly_score_threshold so the score scale stays pinned to the decision it drives.
Acceptance criteria
netdata -W mltestfails whensum += 1inml_dimension_predictis changed tosum += 0netdata -W mltestfails when the threshold comparison inml_dimension_predictis made unconditional- A test trains on a known-normal series, feeds a known anomaly through
ml_dimension_predict, and asserts the return is true, and false for a normal sample - A test asserts the decision changes when
dimension_anomaly_score_thresholdchanges, with input held constant - A test asserts that scores over a mixed normal and anomalous window are not all identical, and that anomalous samples rank above normal ones rather than above a single best-case sample
Category
test debt
Scope boundaries
In scope: additions to src/ml/ml-unittest.cc, and wiring -W mltest into CI if that is wanted.
Out of scope: any change to detector behaviour, to ml_kmeans_anomaly_score, to the default threshold, or to the training path. This is about what the tests assert, not about how the detector works. The two mutations above were reverted after measuring.
Validation
netdata -W mltest on an unmodified tree still reports 0 failed, and reports failures under each of the two mutations. I can supply the mutation patches so the result can be confirmed independently.
Risks / compatibility
Test-only, so no behaviour, API, schema, packaging or migration impact. The one risk worth naming is a flaky assertion: a detection test that depends on kmeans initialisation could be nondeterministic. Any test added here should use a fixed training set and assert on the decision rather than on an exact score value.
Related context
-W mltest does not appear in any workflow under .github/workflows/, in packaging scripts, or in an add_test registration, so the suite appears to run only when invoked by hand. Happy to be corrected if it runs somewhere I did not find.
I am willing to implement the additions above if the direction looks right. I wanted to check the framing before sending tests nobody asked for. Build used: master on macOS 15 arm64, Apple clang, cmake 4.4.0, -DENABLE_ML=On with the prometheus remote-write and mongodb exporters disabled.
Source: netdata/netdata