ktstr 0.23.0

Test harness for Linux process schedulers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! Noise-adjusted A/B comparison for perf-delta's `--noise-adjust N` mode.
//!
//! The default perf-delta compare runs each side ONCE and gates on a fixed
//! relative threshold, which conflates a real regression with run-to-run
//! measurement noise: a scheduling-dominated metric can swing several percent
//! between identical runs (e.g. schbench pipe throughput — see
//! `src/workload/schbench/validation.md`), tripping a fixed threshold on noise
//! alone. The noise-adjusted mode runs each side N times and decides whether the
//! two sides are distinguishable GIVEN their observed run-to-run variability.
//!
//! A metric is a confident regression when it is BOTH:
//!
//! - SEPARATED — the two sides' distributions do not overlap in a
//!   variability-aware sense. Two independent arms, OR'd: a two-sided Welch
//!   (unequal-variance) t-test rejecting equal means at [`NOISE_ALPHA`]
//!   (computed only when both sides have `n >= 2` and a finite, positive
//!   standard error), OR the observed `[min, max]` bands being fully disjoint.
//!   The Welch arm handles near-but-overlapping separations; the disjoint-band
//!   floor is a deterministic, distribution-free guarantee that a
//!   fully-separated move is NEVER hidden by a high-variance side.
//! - MATERIAL — the mean-to-mean delta clears BOTH the metric's `default_abs`
//!   AND its relative threshold (the same dual-gate the scalar
//!   [`crate::stats::compare`] pass applies), so a statistically-separated but
//!   trivially-small change stays Stable. Materiality is metric-specific, so the
//!   `classify_noise` caller applies it — this module reports only the
//!   metric-agnostic separation.
//!
//! Two trust flags are carried alongside, and — unlike the old range-spread gate
//! — NEITHER suppresses a separated + material regression:
//!
//! - `insufficient_samples` (a side realized `< 2` runs, so variance / Welch are
//!   undefined — a per-side run can fail even though `--noise-adjust` requests
//!   `N >= 2`) is a HARD gate: such a verdict is flagged Noisy and never gates.
//! - `high_spread` (a side's relative spread `(max - min) / |mean|` exceeds the
//!   threshold) is ADVISORY only: it annotates the row so the operator sees a
//!   noisy side, but a clearly-separated regression is still reported. The prior
//!   design let `high_spread` suppress the verdict, which INVERTED signal and
//!   noise — a real regression's degraded side is intrinsically higher-variance,
//!   so the biggest regressions were the ones dropped.
//!
//! The Welch t-test needs per-side variance, so [`SideSummary`] retains the
//! Bessel (N-1) sample variance; the `[min, max]` band still drives the
//! distribution-free floor and the advisory spread. Recommendation: use
//! `N >= 5` for a well-powered Welch arm. The disjoint-band floor keeps large
//! regressions detectable even at small N, but it cuts both ways: at `N < 5` two
//! same-distribution sides land on disjoint bands by chance ~33% of the time at
//! `N = 2` and ~10% at `N = 3` (well above the Welch arm's 0.05), so the floor
//! can also report a chance-separated move. The materiality dual-gate is the only
//! backstop against that, and it does NOT cover a chance-disjoint move whose mean
//! delta happens to clear `default_abs`/`default_rel` — another reason to prefer
//! `N >= 5`, where chance-disjoint bands fall below the Welch alpha. The floor is
//! outlier-fragile in the OTHER direction too: a single wild run bridging the gap
//! collapses the disjoint bands AND inflates that side's variance, weakening the
//! Welch arm at the same time, so a large real regression can read as
//! not-separated (a false negative) — the same variance-driven suppression this
//! change exists to defeat, arriving via one anomalous run instead of intrinsic
//! spread. `N >= 5` mitigates this too (a lone outlier shifts min/max and inflates
//! the variance less, relative to a real signal).

use statrs::distribution::{ContinuousCDF, StudentsT};
use statrs::statistics::Statistics;

/// Magnitude below which `|mean|` is treated as zero for the relative-spread
/// ratio. A metric whose mean is essentially zero but whose runs differ has
/// unbounded relative spread, reported as `f64::INFINITY` (which the gate always
/// flags); the epsilon avoids dividing by a true zero. Magnitude-bearing metrics
/// (rps, latencies, byte rates) sit far above this; count/ratio metrics that are
/// legitimately exactly 0 (migrations, stuck_count, off-cpu %, …) have
/// `max == min` and so report spread 0, never INFINITY — the INFINITY branch is
/// reachable only by a hypothetical signed metric straddling zero, where flagging
/// it noisy is the correct-conservative call.
pub(crate) const ZERO_MEAN_EPS: f64 = 1e-9;

/// Significance level for the Welch two-sample t-test arm of the separation
/// test: a metric whose two-sided Welch p-value is below this is SEPARATED on
/// the parametric arm (the disjoint-band floor is an independent OR that needs
/// no alpha). Fixed, not user-tunable — separation is a correctness signal, not
/// a knob; the operator tunes N (`--noise-adjust`) and the materiality
/// thresholds (`--noise-spread-threshold` is the advisory spread flag, not this).
pub(crate) const NOISE_ALPHA: f64 = 0.05;

/// Summary of one side's N runs of a single metric.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SideSummary {
    /// Number of runs summarized.
    pub n: usize,
    /// Arithmetic mean of the runs (`0.0` when `n == 0`).
    pub mean: f64,
    /// Smallest run value (`0.0` when `n == 0`).
    pub min: f64,
    /// Largest run value (`0.0` when `n == 0`).
    pub max: f64,
    /// Relative spread as a PERCENT: `(max - min) / |mean| * 100`. `0.0` when
    /// `n < 2` (no spread observable) or `max == min`. `f64::INFINITY` when the
    /// runs differ but `|mean|` is below `ZERO_MEAN_EPS` (a metric oscillating
    /// around zero is maximally noisy in relative terms). Drives the ADVISORY
    /// `high_spread` flag only — never gates on its own.
    pub spread_pct: f64,
    /// Bessel-corrected (N-1) sample variance of the runs — the dispersion the
    /// Welch t-test consumes (`var / n` is the per-side squared standard error).
    /// `f64::NAN` when `n < 2` (variance undefined for a single point), which the
    /// Welch arm guards against via `insufficient_samples`. Distinct from
    /// `spread_pct` (an outlier-sensitive RANGE ratio used only for the advisory
    /// flag); `var` is the full-sample dispersion the significance test needs.
    pub var: f64,
    /// Arithmetic mean of the RAW samples — equals [`Self::mean`] EXCEPT when a
    /// Rate centroid override ([`Self::with_pooled_mean`]) replaced `mean` with
    /// the pooled `Σnum/Σden`. The Welch arm uses THIS mean (not `mean`) so its
    /// numerator (the `sample_mean` difference) stays coherent with its
    /// denominator (`var`, the variance of the SAME raw samples): a hybrid
    /// pooled-mean-over-ratio-variance t-statistic is NOT a valid two-sample test.
    /// `mean` remains the reported / materiality centroid (the pooled value for a
    /// Rate); `sample_mean` and `var` describe the per-run distribution the
    /// parametric separation test is actually computed on.
    pub sample_mean: f64,
}

impl SideSummary {
    /// Summarize `samples` — one metric's value from each of N runs. Empty input
    /// yields an all-zero summary (`n == 0`); `--noise-adjust N` is expected to
    /// run with `N >= 2` (`N == 1` reports `spread 0`, undefined variance (NaN),
    /// and a single-point band, so `noise_verdict_from` flags it
    /// `insufficient_samples` and `classify_noise` returns Noisy — a single-run
    /// side never gates as a confident regression).
    pub fn of(samples: &[f64]) -> SideSummary {
        let n = samples.len();
        if n == 0 {
            return SideSummary {
                n: 0,
                mean: 0.0,
                min: 0.0,
                max: 0.0,
                spread_pct: 0.0,
                // No samples: variance is undefined, matching the `n < 2` NaN
                // the single-sample case produces below.
                var: f64::NAN,
                sample_mean: 0.0,
            };
        }
        let mean = samples.iter().sum::<f64>() / n as f64;
        let min = samples.iter().copied().fold(f64::INFINITY, f64::min);
        let max = samples.iter().copied().fold(f64::NEG_INFINITY, f64::max);
        // Bessel-corrected (N-1) sample variance via statrs, for the Welch arm.
        // Returns `f64::NAN` for `n < 2` (variance undefined for a single
        // point); the Welch path guards that via `insufficient_samples`.
        let var = samples.variance();
        let spread_pct = if max == min {
            0.0
        } else if mean.abs() < ZERO_MEAN_EPS {
            // Nonzero range around a ~zero mean: relative spread is unbounded.
            f64::INFINITY
        } else {
            (max - min) / mean.abs() * 100.0
        };
        SideSummary {
            n,
            mean,
            min,
            max,
            spread_pct,
            var,
            // Coherent with `var`/`min`/`max` (all from `samples`). Stays equal
            // to `mean` unless a later Rate centroid override shifts `mean` to the
            // pooled value; the Welch arm always reads `sample_mean`.
            sample_mean: mean,
        }
    }

    /// Override the centroid with a pooled mean (`Σnumerator/Σdenominator`)
    /// while KEEPING the per-run `[min, max]` band. For Rate metrics under
    /// `--noise-adjust`: the band still measures run-to-run variability from
    /// the per-run ratios, but the compared centroid is the duration-weighted
    /// pooled rate the metric registry documents as the cross-run Rate value,
    /// so `--noise-adjust` and the scalar averaging compare agree on a Rate's central value. The
    /// relative spread is recomputed against the pooled mean; `n`, the band, the
    /// variance, AND `sample_mean` are unchanged. The Welch arm reads
    /// `sample_mean` + `var` (both from the raw per-run ratios), so overriding
    /// only the reported `mean` keeps the parametric test COHERENT — its numerator
    /// (the `sample_mean` difference) and denominator (`var`) reference the same
    /// per-run distribution, never the pooled centroid (a pooled-mean-over-
    /// ratio-variance t-statistic is not a valid two-sample test). `n` unchanged
    /// keeps the `< 2` insufficient-samples guard applicable.
    pub fn with_pooled_mean(mut self, pooled_mean: f64) -> SideSummary {
        self.mean = pooled_mean;
        self.spread_pct = if self.max == self.min {
            0.0
        } else if pooled_mean.abs() < ZERO_MEAN_EPS {
            f64::INFINITY
        } else {
            (self.max - self.min) / pooled_mean.abs() * 100.0
        };
        self
    }
}

/// The noise-adjusted verdict for one metric across N runs per side.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NoiseVerdict {
    /// A side (baseline) summary.
    pub a: SideSummary,
    /// B side (candidate) summary.
    pub b: SideSummary,
    /// B's distribution is SEPARATED from A's: a two-sided Welch (unequal-
    /// variance) t-test rejects equal means at [`NOISE_ALPHA`] (computed only
    /// when both sides have `n >= 2` and a finite, positive standard error), OR
    /// the observed `[min, max]` bands are fully disjoint. Direction-agnostic;
    /// the caller applies the metric's polarity + `sign(b.mean - a.mean)` to
    /// split regression from improvement, and the materiality dual-gate to
    /// decide whether the separated move is large enough to report.
    pub separated: bool,
    /// HARD gate: a side realized fewer than 2 samples, so variance and the
    /// Welch statistic are undefined (a per-side run can fail even though
    /// `--noise-adjust` requests `N >= 2`). Such a verdict is never a confident
    /// regression — it renders Noisy and never fails the gate.
    pub insufficient_samples: bool,
    /// ADVISORY only: a side's `spread_pct` exceeds the spread threshold. Unlike
    /// the old `too_noisy`, this NEVER suppresses a separated + material
    /// regression (that suppression INVERTED signal and noise — a real
    /// regression's degraded side is intrinsically higher-variance); it only
    /// annotates the row so the operator sees a noisy side.
    pub high_spread: bool,
}

/// Decide the noise-adjusted verdict for one metric from A's and B's per-run
/// samples. `spread_threshold_pct` is the relative-spread limit in PERCENT
/// (e.g. `5.0` for 5%): a side whose spread STRICTLY exceeds it sets the
/// ADVISORY `high_spread` flag, which never suppresses a verdict. Separation is
/// the Welch t-test OR the disjoint-band test (see [`noise_verdict_from`]); the
/// caller (`classify_noise`) applies the metric's materiality dual-gate and
/// polarity on top.
pub fn noise_verdict(
    a_samples: &[f64],
    b_samples: &[f64],
    spread_threshold_pct: f64,
) -> NoiseVerdict {
    noise_verdict_from(
        SideSummary::of(a_samples),
        SideSummary::of(b_samples),
        spread_threshold_pct,
    )
}

/// Decide the verdict from two already-summarized sides. Split from
/// [`noise_verdict`] so the Rate consumer in `noise_findings` can inject a
/// pooled `Σnum/Σden` centroid ([`SideSummary::with_pooled_mean`]) while the
/// `[min, max]` band stays per-run.
///
/// `separated` is `welch_p < NOISE_ALPHA` OR fully-disjoint `[min, max]` bands.
/// The Welch arm is formed ONLY when both sides have `n >= 2` and a finite,
/// positive pooled squared standard error, AND the Welch-Satterthwaite dof is
/// finite and positive — so [`StudentsT::new`]'s freedom precondition holds at
/// every `unwrap`. Degenerate inputs (a side `< 2` samples, or zero within-side
/// variance on both sides → `se2 == 0`) make the Welch arm `false` WITHOUT
/// panicking, and the distribution-free disjoint-band floor decides.
pub fn noise_verdict_from(
    a: SideSummary,
    b: SideSummary,
    spread_threshold_pct: f64,
) -> NoiseVerdict {
    // HARD gate: a side with < 2 realized samples has undefined variance — the
    // Welch statistic cannot be formed, and a single-point band would read any
    // move as separated. Runs are REQUESTED >= 2 (the --noise-adjust clap gate),
    // but a per-side run can fail (noise_dual_run logs and continues), so a side
    // can REALIZE < 2. Flagged, never gated as a confident regression.
    let insufficient_samples = a.n < 2 || b.n < 2;
    // Distribution-free floor: fully disjoint observed bands are unambiguous
    // separation regardless of either side's within-side spread — the
    // deterministic fix for the signal-inverting bug (a large real move whose
    // degraded side is high-variance).
    let disjoint_bands = a.max < b.min || b.max < a.min;
    // Parametric arm: two-sided Welch (unequal-variance) t-test. Formed ONLY
    // when both sides have n >= 2 (variance defined) and the pooled squared
    // standard error `se2` is finite and strictly positive; then only when the
    // Welch-Satterthwaite dof is finite and positive. These guards make every
    // StudentsT::new freedom argument valid, so the unwrap cannot panic; on any
    // degenerate input the arm is `false` and the disjoint-band floor decides.
    let welch_separated = if insufficient_samples {
        false
    } else {
        let sa = a.var / a.n as f64;
        let sb = b.var / b.n as f64;
        let se2 = sa + sb;
        if se2.is_finite() && se2 > 0.0 {
            let dof = (se2 * se2) / (sa * sa / (a.n as f64 - 1.0) + sb * sb / (b.n as f64 - 1.0));
            if dof.is_finite() && dof > 0.0 {
                // Numerator uses sample_mean (coherent with var: both from the
                // raw samples). `mean` may be a Rate pooled centroid that does not
                // match the variance's samples, which would make t incoherent.
                let t = (a.sample_mean - b.sample_mean) / se2.sqrt();
                // freedom finite & > 0 => StudentsT::new(0, 1, dof) is Ok.
                let p = 2.0 * StudentsT::new(0.0, 1.0, dof).unwrap().sf(t.abs());
                p < NOISE_ALPHA
            } else {
                false
            }
        } else {
            false
        }
    };
    let separated = welch_separated || disjoint_bands;
    // ADVISORY only (see NoiseVerdict::high_spread): flags a noisy side, never
    // suppresses a separated verdict.
    let high_spread = a.spread_pct > spread_threshold_pct || b.spread_pct > spread_threshold_pct;
    NoiseVerdict {
        a,
        b,
        separated,
        insufficient_samples,
        high_spread,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn approx(x: f64, y: f64) -> bool {
        (x - y).abs() < 1e-9
    }

    #[test]
    fn side_summary_empty_field_by_field() {
        // A NaN var precludes a whole-struct assert_eq (NaN != NaN); check
        // fields: an empty side is all-zero with an undefined (NaN) variance.
        let s = SideSummary::of(&[]);
        assert_eq!(
            (s.n, s.mean, s.min, s.max, s.spread_pct),
            (0, 0.0, 0.0, 0.0, 0.0)
        );
        assert!(s.var.is_nan(), "empty side has undefined variance");
    }

    #[test]
    fn side_summary_single_has_zero_spread_and_nan_var() {
        // One sample: min == max == mean, no spread observable, variance
        // undefined (Bessel N-1 needs >= 2 points).
        let s = SideSummary::of(&[42.0]);
        assert_eq!((s.n, s.mean, s.min, s.max), (1, 42.0, 42.0, 42.0));
        assert_eq!(s.spread_pct, 0.0);
        assert!(s.var.is_nan(), "single-sample side has undefined variance");
    }

    #[test]
    fn noise_verdict_flags_side_with_fewer_than_two_samples() {
        // A per-side run can fail (noise_dual_run logs + continues), so a side
        // can realize <2 samples even though --noise-adjust requires N>=2. Such
        // a side has undefined variance and a degenerate single-point band; it
        // is a HARD gate (insufficient_samples), never a confident regression.
        // The Welch arm must be SKIPPED (dof undefined) — this asserts no panic.
        let v = noise_verdict(&[100.0], &[130.0, 130.0, 130.0], 5.0);
        assert_eq!(v.a.n, 1);
        assert!(
            v.insufficient_samples,
            "a side with <2 realized samples must be flagged insufficient",
        );
        // Symmetric: B realized 1 sample.
        let v = noise_verdict(&[100.0, 100.0, 100.0], &[130.0], 5.0);
        assert_eq!(v.b.n, 1);
        assert!(
            v.insufficient_samples,
            "a degenerate B side is also insufficient",
        );
        // Control: both sides have >=2 samples with a real, fully-separated
        // shift — separated and NOT insufficient (gateable once material).
        let v = noise_verdict(&[100.0, 100.0], &[130.0, 130.0], 5.0);
        assert!(
            v.separated && !v.insufficient_samples,
            "n>=2 sides with disjoint bands stay separated + gateable",
        );
    }

    #[test]
    fn side_summary_mean_min_max_spread_var() {
        // mean (90+110+100)/3 = 100; range 20; spread 20/100 = 20%; Bessel
        // variance (100+100+0)/(3-1) = 100.
        let s = SideSummary::of(&[90.0, 110.0, 100.0]);
        assert!(approx(s.mean, 100.0));
        assert_eq!((s.min, s.max), (90.0, 110.0));
        assert!(approx(s.spread_pct, 20.0));
        assert!(approx(s.var, 100.0), "Bessel N-1 variance");
    }

    #[test]
    fn side_summary_identical_runs_have_zero_spread_and_var() {
        let s = SideSummary::of(&[7.0, 7.0, 7.0]);
        assert_eq!(s.spread_pct, 0.0, "identical runs => no spread");
        assert_eq!(s.var, 0.0, "identical runs => zero variance");
    }

    #[test]
    fn side_summary_near_zero_mean_with_variance_is_infinite_spread() {
        // mean ~0 but runs differ => relative spread is unbounded -> INFINITY
        // (the advisory high_spread flag). Variance stays finite (Bessel
        // (1+1)/1 = 2), so the Welch arm is unaffected by the near-zero mean.
        let s = SideSummary::of(&[-1.0, 1.0]);
        assert!(approx(s.mean, 0.0));
        assert_eq!(s.spread_pct, f64::INFINITY);
        assert!(approx(s.var, 2.0));
    }

    #[test]
    fn with_pooled_mean_keeps_sample_mean_and_var_for_a_coherent_welch_arm() {
        // Rate centroid override: `mean` becomes the pooled value (reported +
        // materiality), but `sample_mean`, `var`, and the band stay the raw
        // per-run values so the Welch arm's numerator (sample_mean diff) stays
        // coherent with its denominator (var). Guards the Rate Welch-coherence fix
        // — a pooled-mean-over-ratio-variance t-statistic is not a valid test.
        let s = SideSummary::of(&[10.0, 20.0, 30.0]); // raw mean 20, var 100
        assert!(approx(s.mean, 20.0) && approx(s.sample_mean, 20.0));
        let p = s.with_pooled_mean(18.18);
        assert!(
            approx(p.mean, 18.18),
            "reported mean is the pooled centroid"
        );
        assert!(
            approx(p.sample_mean, 20.0),
            "Welch mean stays the raw sample mean"
        );
        assert!(
            approx(p.var, 100.0),
            "variance stays the raw-sample variance"
        );
        assert_eq!((p.min, p.max), (10.0, 30.0), "band stays raw");
    }

    #[test]
    fn verdict_overlapping_bands_equal_means_not_separated() {
        // A [90,110] mean 100; B [95,105] mean 100. Bands overlap and the means
        // are equal -> Welch t=0 (p=1) and no disjoint bands -> not separated.
        let v = noise_verdict(&[90.0, 110.0, 100.0], &[95.0, 105.0, 100.0], 100.0);
        assert!(
            !v.separated,
            "overlapping bands, equal means => not separated"
        );
    }

    #[test]
    fn verdict_disjoint_bands_separated_higher() {
        // A band [90,110]; B [150,150] entirely above -> the disjoint-band floor
        // fires (separated) regardless of the Welch arm; b.mean > a.mean.
        let v = noise_verdict(&[90.0, 110.0], &[150.0, 150.0], 100.0);
        assert!(v.separated);
        assert!(v.b.mean > v.a.mean);
    }

    #[test]
    fn verdict_disjoint_bands_separated_lower() {
        let v = noise_verdict(&[90.0, 110.0], &[50.0, 50.0], 100.0);
        assert!(v.separated);
        assert!(v.b.mean < v.a.mean);
    }

    #[test]
    fn verdict_touching_bands_weak_welch_not_separated() {
        // B mean == A.max exactly: bands TOUCH (not strictly disjoint), and the
        // Welch arm at n=2 with this separation does not reach alpha -> not
        // separated. Guards the strict `<` in the disjoint-band test.
        let v = noise_verdict(&[90.0, 110.0], &[110.0, 110.0], 100.0);
        assert!(!v.separated, "touching (non-disjoint) bands + weak Welch");
    }

    #[test]
    fn verdict_welch_separates_overlapping_bands() {
        // The parametric arm in isolation: bands OVERLAP (A [96,104], B
        // [102,110]) so the disjoint floor does NOT fire, but with n=5 and tight
        // per-side variance the Welch t-test clears alpha (t=-3, dof=8,
        // two-sided p ~ 0.017) -> separated via Welch, not the floor.
        let a = [96.0, 98.0, 100.0, 102.0, 104.0];
        let b = [102.0, 104.0, 106.0, 108.0, 110.0];
        let v = noise_verdict(&a, &b, 100.0);
        assert!(
            v.a.max >= v.b.min,
            "bands must overlap so this exercises the Welch arm, not the floor",
        );
        assert!(
            v.separated,
            "Welch t-test must separate overlapping-band sides"
        );
    }

    #[test]
    fn verdict_zero_variance_disjoint_no_panic() {
        // Both sides deterministic (zero within-side variance) with different
        // means: se2 == 0 so the Welch arm is SKIPPED (StudentsT::new would
        // otherwise get a NaN dof) — the disjoint-band floor separates. Pins the
        // no-panic guarantee on the common quiesced-counter case.
        let v = noise_verdict(&[100.0, 100.0, 100.0], &[130.0, 130.0, 130.0], 5.0);
        assert_eq!(v.a.var, 0.0);
        assert_eq!(v.b.var, 0.0);
        assert!(
            v.separated,
            "disjoint bands separate even when Welch is skipped"
        );
        assert!(!v.insufficient_samples);
    }

    #[test]
    fn verdict_high_spread_is_advisory_not_suppressing() {
        // The signal-inversion regression test: a high-variance side must NOT
        // suppress a clearly-separated move. A's 20% spread (> 5% threshold) sets
        // the advisory high_spread flag, but the disjoint bands keep the verdict
        // separated — the old too_noisy gate would have dropped this.
        let v = noise_verdict(&[90.0, 110.0, 100.0], &[300.0, 300.0, 300.0], 5.0);
        assert!(v.high_spread, "A's 20% spread exceeds the 5% threshold");
        assert!(
            v.separated,
            "high_spread is advisory and must not suppress a separated verdict",
        );
    }

    #[test]
    fn verdict_spread_flag_is_strict_threshold() {
        // Both spreads exactly 1% at a 1% threshold: strict `>` => NOT flagged.
        let v = noise_verdict(&[99.5, 100.5], &[99.5, 100.5], 1.0);
        assert!(approx(v.a.spread_pct, 1.0));
        assert!(
            !v.high_spread,
            "spread == threshold is not over it (strict >)"
        );
    }

    /// Demonstration on the real schbench engine (host-side `run_standalone`, no
    /// VM — each run is the short `-r` window, far under the 15s/run budget).
    /// Three windows per config feed the per-run request-p50 into `noise_verdict`,
    /// exercising both arms:
    ///  - A vs a much heavier B (10x matrix ops): a large real change the tool
    ///    must flag `separated` (the hard assertion + the true-positive arm).
    ///  - A vs A (identical config): the false-positive check — REPORTED for
    ///    real-data evaluation, not hard-asserted (the deterministic
    ///    no-false-positive logic is pinned by the unit tests above).
    ///
    /// Prints the observed spreads + verdicts so the behavior is inspectable.
    #[test]
    fn schbench_noise_adjust_demonstration() {
        use crate::workload::{SchbenchConfig, run_standalone};

        const RUN_SECS: u64 = 2; // << 15s/run; host-side, no VM boot
        const N: usize = 3;

        // request-latency p50 (us): index 1 of [20,50,90,99,99.9]. sleep_usec(0)
        // makes it pure matrix work, so `operations` scales it ~linearly and the
        // heavy config separates cleanly from the light one.
        let p50 = |cfg: &SchbenchConfig| run_standalone(cfg, RUN_SECS).request_pcts_us[1] as f64;
        let samples = |cfg: &SchbenchConfig| (0..N).map(|_| p50(cfg)).collect::<Vec<f64>>();

        let light = SchbenchConfig::default()
            .worker_threads(2)
            .sleep_usec(0)
            .operations(5);
        let heavy = SchbenchConfig::default()
            .worker_threads(2)
            .sleep_usec(0)
            .operations(50);

        let a = samples(&light);
        let b_same = samples(&light); // identical config: the no-regression arm
        let b_heavy = samples(&heavy); // 10x ops: the real-regression arm

        let same = noise_verdict(&a, &b_same, 5.0);
        let diff = noise_verdict(&a, &b_heavy, 5.0);

        eprintln!(
            "\nnoise-adjust schbench demo (request p50 us, N={N}, {RUN_SECS}s/run):\n  \
             A       = {:.0} [{:.0}-{:.0}] spread {:.2}%\n  \
             A vs A  : B {:.0} [{:.0}-{:.0}] spread {:.2}% -> separated={} high_spread={}\n  \
             A vs B' : B {:.0} [{:.0}-{:.0}] spread {:.2}% -> separated={} high_spread={}",
            same.a.mean,
            same.a.min,
            same.a.max,
            same.a.spread_pct,
            same.b.mean,
            same.b.min,
            same.b.max,
            same.b.spread_pct,
            same.separated,
            same.high_spread,
            diff.b.mean,
            diff.b.min,
            diff.b.max,
            diff.b.spread_pct,
            diff.separated,
            diff.high_spread,
        );

        // True positive: 10x matrix ops is a large real increase in request
        // latency — separated (the disjoint-band floor and/or Welch) and clearing
        // A's whole observed band (reliable at 10x, not flaky).
        assert!(
            diff.separated && diff.b.mean > a.iter().copied().fold(f64::NEG_INFINITY, f64::max),
            "10x operations must read as a separated increase clearing A's band: A={:?} B={:?}",
            diff.a,
            diff.b,
        );
        // The A-vs-A arm is REPORTED above for real-data evaluation, not hard
        // asserted: at small N two identical configs can still drift enough for
        // the Welch arm to flag them, so a hard `!separated` assertion here would
        // be flaky. The deterministic no-false-positive logic is pinned by the
        // unit tests above (verdict_overlapping_bands_equal_means_not_separated
        // et al.); this test hard-asserts only the reliable true positive.
    }
}