ktstr 0.6.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
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
//! `Assert` struct tests: `NO_OVERRIDES` / `default_checks`,
//! per-field merge precedence, builder setters, the
//! `worker_plan` / `monitor_thresholds` extractors,
//! `has_worker_checks` discriminator, and the
//! `gap_threshold_ms` debug-vs-release helper.

use super::tests_common::rpt;
use super::*;

#[test]
fn assert_no_overrides_has_no_checks() {
    let v = Assert::NO_OVERRIDES;
    assert!(v.not_starved.is_none());
    assert!(v.isolation.is_none());
    assert!(v.max_gap_ms.is_none());
    assert!(v.max_spread_pct.is_none());
    assert!(v.max_imbalance_ratio.is_none());
}

#[test]
fn assert_default_checks_is_no_overrides() {
    let v = Assert::default_checks();
    assert!(v.not_starved.is_none());
    assert!(v.isolation.is_none());
    assert!(v.max_imbalance_ratio.is_none());
    assert!(v.max_local_dsq_depth.is_none());
    assert!(v.fail_on_stall.is_none());
    assert!(v.sustained_samples.is_none());
    assert!(v.max_fallback_rate.is_none());
    assert!(v.max_keep_last_rate.is_none());
}

/// Wire-format pin: `Assert::with_monitor_defaults()` MUST set
/// `enforce_monitor_thresholds = true`. The propagation chain is:
/// builder.with_monitor_defaults() → enforce_monitor_thresholds=true
/// → monitor_thresholds().enforce=true → MonitorThresholds.enforce=true
/// → evaluate() respects enforce.
///
/// Per `Assert::with_monitor_defaults` doc: opt into pass/fail
/// enforcement for monitor thresholds. Without this call, monitor
/// violations are reported in the verdict's details but do not
/// fail the test. With it, any monitor threshold violation fails
/// the test.
///
/// A regression that breaks the propagation (e.g. forgets to set
/// enforce_monitor_thresholds in the auto-fill helper, OR changes
/// the field name without updating the threshold extractor) would
/// silently disable enforcement for every test using the builder
/// pattern.
#[test]
fn with_monitor_defaults_propagates_enforce_true_to_thresholds() {
    let assert = Assert::NO_OVERRIDES.with_monitor_defaults();
    let t = assert.monitor_thresholds();
    assert!(
        t.enforce,
        "with_monitor_defaults() must propagate enforce=true to MonitorThresholds"
    );
}

/// Wire-format pin: without `.with_monitor_defaults()` the builder
/// produces enforce=false. This is the post-enforce-default-flip
/// report-only default that broke 19 tests when the default flipped
/// — pin both polarities so a flip in either direction is caught.
#[test]
fn assert_default_without_monitor_defaults_yields_enforce_false() {
    let assert = Assert::NO_OVERRIDES;
    let t = assert.monitor_thresholds();
    assert!(
        !t.enforce,
        "default Assert (no .with_monitor_defaults()) must yield enforce=false"
    );
}

/// Direct-field pin: `Assert::NO_OVERRIDES.enforce_monitor_thresholds`
/// MUST be `false`. Parallel to the `monitor_thresholds()` canary above —
/// catches a regression where the helper is preserved but the underlying
/// field default flips. NO_OVERRIDES is the runtime composition base
/// (see the runtime composition documented on `Assert::merge`), so a
/// flipped default here would silently enable enforcement for every
/// scheduler that doesn't explicitly opt out.
#[test]
fn no_overrides_enforce_monitor_thresholds_field_is_false() {
    let v = Assert::NO_OVERRIDES;
    assert!(
        !v.enforce_monitor_thresholds,
        "NO_OVERRIDES.enforce_monitor_thresholds must be false (post-enforce-default-flip default)"
    );
}

/// Merge semantics pin: `enforce_monitor_thresholds` uses sticky-OR
/// (see the OR expression in `Assert::merge`:
/// `self.enforce_monitor_thresholds || other.enforce_monitor_thresholds`).
/// Distinct from every other field which uses last-Some-wins. The
/// OR rule means a child Assert that opted into enforcement cannot
/// be silently downgraded by merging over (or under) a non-enforcing
/// peer.
///
/// This canary covers the false||false=false case — the baseline that
/// guarantees a merge of two NO_OVERRIDES (the runtime no-op composition)
/// stays in report-only mode.
#[test]
fn merge_enforce_monitor_thresholds_or_semantics_false_false() {
    let merged = Assert::NO_OVERRIDES.merge(&Assert::NO_OVERRIDES);
    assert!(
        !merged.enforce_monitor_thresholds,
        "false || false must yield false"
    );
}

/// Merge semantics pin: true (self) || false (other) → true.
/// A test-level `Assert::NO_OVERRIDES.with_monitor_defaults()` on the
/// left merged with a NO_OVERRIDES override on the right keeps
/// enforce=true. Without OR-semantics, a regression could silently
/// flip back to last-Some-wins and drop enforcement on every test
/// that uses a non-enforcing override layer.
#[test]
fn merge_enforce_monitor_thresholds_or_semantics_true_left() {
    let left = Assert::NO_OVERRIDES.with_monitor_defaults();
    let right = Assert::NO_OVERRIDES;
    let merged = left.merge(&right);
    assert!(
        merged.enforce_monitor_thresholds,
        "true (self) || false (other) must yield true (sticky OR)"
    );
}

/// Merge semantics pin: false (self) || true (other) → true.
/// Symmetric to the previous canary — a non-enforcing base layer
/// merged with an enforcing override layer must yield enforce=true.
/// Both directions matter because the runtime composes via
/// `default_checks().merge(&scheduler.assert).merge(&test.assert)`
/// (see `Assert::merge`), so the enforcing assert can appear on either
/// side depending on which layer opted in.
#[test]
fn merge_enforce_monitor_thresholds_or_semantics_false_true() {
    let left = Assert::NO_OVERRIDES;
    let right = Assert::NO_OVERRIDES.with_monitor_defaults();
    let merged = left.merge(&right);
    assert!(
        merged.enforce_monitor_thresholds,
        "false (self) || true (other) must yield true (sticky OR)"
    );
}

/// Behavior pin: `with_monitor_defaults()` fills ALL six unset
/// monitor-threshold Option fields with `MonitorThresholds::new()`
/// values, not just `enforce_monitor_thresholds`. Documented on
/// `Assert::with_monitor_defaults` ("Also populates any unset
/// monitor-threshold field with the canonical default"). Without
/// this canary, a regression that drops the per-field auto-fill
/// loop in `with_monitor_defaults` would silently let enforced
/// tests run against `None` fields that
/// downstream `monitor_thresholds()` would still default — same end
/// behavior today, but a future API consumer reading the raw
/// `Assert` fields would see misleading `None`s.
#[test]
fn with_monitor_defaults_fills_all_unset_threshold_fields() {
    use crate::monitor::MonitorThresholds;
    let assert = Assert::NO_OVERRIDES.with_monitor_defaults();
    let d = MonitorThresholds::new();
    assert!(
        (assert.max_imbalance_ratio.unwrap() - d.max_imbalance_ratio).abs() < f64::EPSILON,
        "max_imbalance_ratio must be auto-filled with DEFAULT"
    );
    assert_eq!(
        assert.max_local_dsq_depth,
        Some(d.max_local_dsq_depth),
        "max_local_dsq_depth must be auto-filled with DEFAULT"
    );
    assert_eq!(
        assert.fail_on_stall,
        Some(d.fail_on_stall),
        "fail_on_stall must be auto-filled with DEFAULT"
    );
    assert_eq!(
        assert.sustained_samples,
        Some(d.sustained_samples),
        "sustained_samples must be auto-filled with DEFAULT"
    );
    assert!(
        (assert.max_fallback_rate.unwrap() - d.max_fallback_rate).abs() < f64::EPSILON,
        "max_fallback_rate must be auto-filled with DEFAULT"
    );
    assert!(
        (assert.max_keep_last_rate.unwrap() - d.max_keep_last_rate).abs() < f64::EPSILON,
        "max_keep_last_rate must be auto-filled with DEFAULT"
    );
    assert!(
        assert.enforce_monitor_thresholds,
        "enforce_monitor_thresholds must be set to true"
    );
}

/// Merge semantics pin: true (self) || true (other) → true.
/// Completes the OR truth-table coverage that the false_false /
/// true_left / false_true canaries above pin three of four cells
/// for. The TT cell is the case a future XOR-vs-OR regression would
/// catch only via this canary — false||false=F, false||true=T, and
/// true||false=T pass under XOR semantics too. true||true=T does
/// NOT pass under XOR (XOR would yield F here), so a regression
/// that swapped OR for XOR escapes the existing 3 canaries unless
/// this 4th cell pins it.
#[test]
fn merge_enforce_monitor_thresholds_or_semantics_true_true() {
    let left = Assert::NO_OVERRIDES.with_monitor_defaults();
    let right = Assert::NO_OVERRIDES.with_monitor_defaults();
    let merged = left.merge(&right);
    assert!(
        merged.enforce_monitor_thresholds,
        "true || true must yield true (OR, not XOR)"
    );
}

/// Behavior pin: `with_monitor_defaults()` MUST NOT clobber
/// user-set values for the 6 unset-Option monitor-threshold fields.
/// The auto-fill loop in `with_monitor_defaults` uses `if X.is_none()`
/// guards; a regression that drops the guard and unconditionally
/// stores `Some(default)` would silently overwrite a user's custom
/// thresholds (e.g. test author sets max_imbalance_ratio=2.0 to
/// catch a known schedule-balance issue, with_monitor_defaults
/// silently restores 4.0). The companion canary
/// `with_monitor_defaults_fills_all_unset_threshold_fields` above
/// tests the auto-fill SIDE of the contract; this canary tests
/// the inverse SIDE: user-set values survive.
#[test]
fn with_monitor_defaults_preserves_user_set_values() {
    use crate::monitor::MonitorThresholds;
    let d = MonitorThresholds::new();
    // Pick values DIFFERENT from each DEFAULT so the test fails if
    // the auto-fill loop drops its `is_none()` guard and silently
    // overwrites. Differs from DEFAULT by ~50% on each axis where
    // the type allows; bool / counter axes flip / shift respectively.
    let custom_imbalance = d.max_imbalance_ratio * 0.5;
    let custom_local_dsq = d.max_local_dsq_depth * 2;
    let custom_fail_on_stall = !d.fail_on_stall;
    let custom_sustained = d.sustained_samples * 2;
    let custom_fallback = d.max_fallback_rate * 0.5;
    let custom_keep_last = d.max_keep_last_rate * 0.5;
    // Zero-default guard: if a future DEFAULT value happens to be 0,
    // the `* 0.5` / `* 2` derivation lands at the same value
    // (0 * 0.5 == 0; 0 * 2 == 0) and the test passes vacuously
    // against a regression that drops the `is_none()` guard.
    // Pre-check that each custom value DIFFERS from DEFAULT so a
    // future DEFAULT-of-zero fails the test loudly here rather
    // than silently elsewhere.
    assert!(
        (custom_imbalance - d.max_imbalance_ratio).abs() > f64::EPSILON,
        "test derivation produced custom_imbalance == DEFAULT — DEFAULT.max_imbalance_ratio may have shifted to 0"
    );
    assert_ne!(
        custom_local_dsq, d.max_local_dsq_depth,
        "test derivation produced custom_local_dsq == DEFAULT — DEFAULT.max_local_dsq_depth may have shifted to 0"
    );
    assert_ne!(
        custom_fail_on_stall, d.fail_on_stall,
        "bool flip should always differ"
    );
    assert_ne!(
        custom_sustained, d.sustained_samples,
        "test derivation produced custom_sustained == DEFAULT — DEFAULT.sustained_samples may have shifted to 0"
    );
    assert!(
        (custom_fallback - d.max_fallback_rate).abs() > f64::EPSILON,
        "test derivation produced custom_fallback == DEFAULT — DEFAULT.max_fallback_rate may have shifted to 0"
    );
    assert!(
        (custom_keep_last - d.max_keep_last_rate).abs() > f64::EPSILON,
        "test derivation produced custom_keep_last == DEFAULT — DEFAULT.max_keep_last_rate may have shifted to 0"
    );
    let assert = Assert::NO_OVERRIDES
        .max_imbalance_ratio(custom_imbalance)
        .max_local_dsq_depth(custom_local_dsq)
        .fail_on_stall(custom_fail_on_stall)
        .sustained_samples(custom_sustained)
        .max_fallback_rate(custom_fallback)
        .max_keep_last_rate(custom_keep_last)
        .with_monitor_defaults();
    assert!(
        (assert.max_imbalance_ratio.unwrap() - custom_imbalance).abs() < f64::EPSILON,
        "max_imbalance_ratio user-set value must survive with_monitor_defaults"
    );
    assert_eq!(
        assert.max_local_dsq_depth,
        Some(custom_local_dsq),
        "max_local_dsq_depth user-set value must survive"
    );
    assert_eq!(
        assert.fail_on_stall,
        Some(custom_fail_on_stall),
        "fail_on_stall user-set value must survive"
    );
    assert_eq!(
        assert.sustained_samples,
        Some(custom_sustained),
        "sustained_samples user-set value must survive"
    );
    assert!(
        (assert.max_fallback_rate.unwrap() - custom_fallback).abs() < f64::EPSILON,
        "max_fallback_rate user-set value must survive"
    );
    assert!(
        (assert.max_keep_last_rate.unwrap() - custom_keep_last).abs() < f64::EPSILON,
        "max_keep_last_rate user-set value must survive"
    );
    assert!(
        assert.enforce_monitor_thresholds,
        "enforce_monitor_thresholds still set to true (the only field with_monitor_defaults unconditionally writes)"
    );
}

/// OR-chain coverage: `has_monitor_thresholds()` MUST return false
/// when every monitor-threshold Option is None. NO_OVERRIDES is
/// the canonical all-None starting point; this canary pins the
/// false-side of the OR-chain in `has_monitor_thresholds`.
#[test]
fn has_monitor_thresholds_false_when_all_none() {
    let v = Assert::NO_OVERRIDES;
    assert!(
        !v.has_monitor_thresholds(),
        "NO_OVERRIDES must report has_monitor_thresholds() == false"
    );
}

/// OR-chain coverage: `has_monitor_thresholds()` MUST return true
/// when ANY single monitor-threshold Option is Some. Parametric
/// over the 6 fields — each iteration sets exactly one field to a
/// non-None value via the builder, asserts has_monitor_thresholds
/// returns true. Catches a regression that drops a field from the
/// `has_monitor_thresholds` OR-chain (the same field-drift class
/// the FailureDumpReport round-trip test catches on the dump
/// side). A dropped field would silently skip monitor enforcement
/// for tests that set only the missed field.
#[test]
fn has_monitor_thresholds_true_when_any_set() {
    // Compile-time anchor for the maintenance
    // contract. Adding a new MonitorThresholds field requires
    // updating BOTH this struct-update literal (compile error
    // otherwise) AND the closure list below. Without this anchor,
    // the closure-list maintenance is doc-only and easy to miss
    // when adding a field — Rust would let the test pass silently
    // against the new field. The literal's _ binding discards the
    // value; only the compile-time field enumeration matters.
    use crate::monitor::MonitorThresholds;
    let _ = MonitorThresholds {
        max_imbalance_ratio: 0.0,
        max_local_dsq_depth: 0,
        fail_on_stall: false,
        sustained_samples: 0,
        max_fallback_rate: 0.0,
        max_keep_last_rate: 0.0,
        enforce: false,
    };
    // Each closure returns an Assert with exactly one threshold
    // field set. Adding a new MonitorThresholds field requires
    // adding a closure here — that's the maintenance contract,
    // anchored by the struct-update above.
    type FieldSetter = (&'static str, fn() -> Assert);
    let setters: &[FieldSetter] = &[
        ("max_imbalance_ratio", || {
            Assert::NO_OVERRIDES.max_imbalance_ratio(3.0)
        }),
        ("max_local_dsq_depth", || {
            Assert::NO_OVERRIDES.max_local_dsq_depth(64)
        }),
        ("fail_on_stall", || Assert::NO_OVERRIDES.fail_on_stall(true)),
        ("sustained_samples", || {
            Assert::NO_OVERRIDES.sustained_samples(7)
        }),
        ("max_fallback_rate", || {
            Assert::NO_OVERRIDES.max_fallback_rate(150.0)
        }),
        ("max_keep_last_rate", || {
            Assert::NO_OVERRIDES.max_keep_last_rate(75.0)
        }),
    ];
    for (field, build) in setters {
        let v = build();
        assert!(
            v.has_monitor_thresholds(),
            "has_monitor_thresholds() must return true when only `{field}` is set"
        );
    }
}

#[test]
fn assert_merge_other_overrides_self() {
    let base = Assert::NO_OVERRIDES;
    let other = Assert::NO_OVERRIDES
        .check_not_starved()
        .max_gap_ms(5000)
        .max_imbalance_ratio(2.0);
    let merged = base.merge(&other);
    assert_eq!(merged.not_starved, Some(true));
    assert_eq!(merged.max_gap_ms, Some(5000));
    assert_eq!(merged.max_imbalance_ratio, Some(2.0));
}

#[test]
fn assert_merge_preserves_self_when_other_is_none() {
    let base = Assert::NO_OVERRIDES
        .check_not_starved()
        .max_imbalance_ratio(4.0);
    let merged = base.merge(&Assert::NO_OVERRIDES);
    assert_eq!(merged.not_starved, Some(true));
    assert_eq!(merged.max_imbalance_ratio, Some(4.0));
}

#[test]
fn assert_merge_other_takes_precedence() {
    let base = Assert::NO_OVERRIDES.max_imbalance_ratio(4.0);
    let other = Assert::NO_OVERRIDES.max_imbalance_ratio(2.0);
    let merged = base.merge(&other);
    assert_eq!(merged.max_imbalance_ratio, Some(2.0));
}

#[test]
fn assert_merge_last_some_wins() {
    let base = Assert::NO_OVERRIDES.check_not_starved();
    let other = Assert::NO_OVERRIDES.check_isolation();
    let merged = base.merge(&other);
    assert_eq!(merged.not_starved, Some(true));
    assert_eq!(merged.isolation, Some(true));
}

#[test]
fn assert_merge_child_disables_not_starved() {
    let base = Assert::NO_OVERRIDES.check_not_starved();
    let other = Assert {
        not_starved: Some(false),
        ..Assert::NO_OVERRIDES
    };
    let merged = base.merge(&other);
    assert_eq!(merged.not_starved, Some(false));
    assert!(!merged.worker_plan().not_starved);
}

#[test]
fn assert_merge_child_disables_isolation() {
    let base = Assert::NO_OVERRIDES.check_isolation(); // isolation = Some(true)
    let other = Assert {
        isolation: Some(false),
        ..Assert::NO_OVERRIDES
    };
    let merged = base.merge(&other);
    assert_eq!(merged.isolation, Some(false));
    assert!(!merged.worker_plan().isolation);
}

#[test]
fn assert_worker_plan_extraction() {
    let v = Assert::NO_OVERRIDES
        .check_not_starved()
        .check_isolation()
        .max_gap_ms(3000)
        .max_spread_pct(25.0);
    assert_eq!(v.not_starved, Some(true));
    assert_eq!(v.isolation, Some(true));
    let plan = v.worker_plan();
    assert!(plan.not_starved);
    assert!(plan.isolation);
    assert_eq!(plan.max_gap_ms, Some(3000));
    assert_eq!(plan.max_spread_pct, Some(25.0));
}

#[test]
fn assert_cgroup_delegates_to_plan() {
    let v = Assert::NO_OVERRIDES.check_not_starved();
    let reports = [rpt(1, 1000, 5e9 as u64, 5e8 as u64, &[0], 50)];
    let r = v.assert_cgroup(&reports, None);
    assert!(r.is_pass());
    assert_eq!(r.stats.total_workers, 1);
}

#[test]
fn assert_monitor_thresholds_extraction() {
    let v = Assert::NO_OVERRIDES
        .max_imbalance_ratio(2.5)
        .max_local_dsq_depth(100)
        .fail_on_stall(false)
        .sustained_samples(10)
        .max_fallback_rate(50.0)
        .max_keep_last_rate(25.0);
    let t = v.monitor_thresholds();
    assert!((t.max_imbalance_ratio - 2.5).abs() < f64::EPSILON);
    assert_eq!(t.max_local_dsq_depth, 100);
    assert!(!t.fail_on_stall);
    assert_eq!(t.sustained_samples, 10);
    assert!((t.max_fallback_rate - 50.0).abs() < f64::EPSILON);
    assert!((t.max_keep_last_rate - 25.0).abs() < f64::EPSILON);
}

#[test]
fn assert_monitor_thresholds_defaults_when_none() {
    let v = Assert::NO_OVERRIDES;
    let t = v.monitor_thresholds();
    let d = crate::monitor::MonitorThresholds::new();
    assert!((t.max_imbalance_ratio - d.max_imbalance_ratio).abs() < f64::EPSILON);
    assert_eq!(t.max_local_dsq_depth, d.max_local_dsq_depth);
}

#[test]
fn assert_chain_all_setters() {
    let v = Assert::NO_OVERRIDES
        .check_not_starved()
        .check_isolation()
        .max_gap_ms(1000)
        .max_spread_pct(5.0)
        .max_imbalance_ratio(3.0)
        .max_local_dsq_depth(20)
        .fail_on_stall(true)
        .sustained_samples(3)
        .max_fallback_rate(100.0)
        .max_keep_last_rate(50.0);
    assert_eq!(v.not_starved, Some(true));
    assert_eq!(v.isolation, Some(true));
    assert_eq!(v.max_gap_ms, Some(1000));
    assert_eq!(v.max_spread_pct, Some(5.0));
    assert_eq!(v.max_imbalance_ratio, Some(3.0));
    assert_eq!(v.max_local_dsq_depth, Some(20));
    assert_eq!(v.fail_on_stall, Some(true));
    assert_eq!(v.sustained_samples, Some(3));
    assert_eq!(v.max_fallback_rate, Some(100.0));
    assert_eq!(v.max_keep_last_rate, Some(50.0));
}

// -- gap_threshold_ms tests --

#[test]
fn gap_threshold_default() {
    let t = gap_threshold_ms();
    if cfg!(debug_assertions) {
        assert_eq!(t, 3000);
    } else {
        assert_eq!(t, 2000);
    }
}

// -- Assert::merge per-field tests --

#[test]
fn assert_merge_max_spread_pct() {
    let base = Assert::NO_OVERRIDES.max_spread_pct(10.0);
    let other = Assert::NO_OVERRIDES.max_spread_pct(5.0);
    assert_eq!(base.merge(&other).max_spread_pct, Some(5.0));
    assert_eq!(base.merge(&Assert::NO_OVERRIDES).max_spread_pct, Some(10.0));
}

#[test]
fn assert_merge_fail_on_stall() {
    let base = Assert::NO_OVERRIDES.fail_on_stall(true);
    let other = Assert::NO_OVERRIDES.fail_on_stall(false);
    assert_eq!(base.merge(&other).fail_on_stall, Some(false));
    assert_eq!(base.merge(&Assert::NO_OVERRIDES).fail_on_stall, Some(true));
}

#[test]
fn assert_merge_sustained_samples() {
    let base = Assert::NO_OVERRIDES.sustained_samples(5);
    let other = Assert::NO_OVERRIDES.sustained_samples(10);
    assert_eq!(base.merge(&other).sustained_samples, Some(10));
    assert_eq!(base.merge(&Assert::NO_OVERRIDES).sustained_samples, Some(5));
}

#[test]
fn assert_merge_max_fallback_rate() {
    let base = Assert::NO_OVERRIDES.max_fallback_rate(200.0);
    let other = Assert::NO_OVERRIDES.max_fallback_rate(50.0);
    assert_eq!(base.merge(&other).max_fallback_rate, Some(50.0));
    assert_eq!(
        base.merge(&Assert::NO_OVERRIDES).max_fallback_rate,
        Some(200.0)
    );
}

#[test]
fn assert_merge_max_keep_last_rate() {
    let base = Assert::NO_OVERRIDES.max_keep_last_rate(100.0);
    let other = Assert::NO_OVERRIDES.max_keep_last_rate(25.0);
    assert_eq!(base.merge(&other).max_keep_last_rate, Some(25.0));
    assert_eq!(
        base.merge(&Assert::NO_OVERRIDES).max_keep_last_rate,
        Some(100.0)
    );
}

#[test]
fn assert_merge_max_local_dsq_depth() {
    let base = Assert::NO_OVERRIDES.max_local_dsq_depth(50);
    let other = Assert::NO_OVERRIDES.max_local_dsq_depth(100);
    assert_eq!(base.merge(&other).max_local_dsq_depth, Some(100));
    assert_eq!(
        base.merge(&Assert::NO_OVERRIDES).max_local_dsq_depth,
        Some(50)
    );
}

#[test]
fn assert_merge_max_gap_ms() {
    let base = Assert::NO_OVERRIDES.max_gap_ms(2000);
    let other = Assert::NO_OVERRIDES.max_gap_ms(5000);
    assert_eq!(base.merge(&other).max_gap_ms, Some(5000));
    assert_eq!(base.merge(&Assert::NO_OVERRIDES).max_gap_ms, Some(2000));
}

#[test]
fn assert_merge_three_layers() {
    let defaults = Assert::default_checks();
    let sched = Assert::NO_OVERRIDES
        .max_imbalance_ratio(2.0)
        .max_fallback_rate(50.0);
    let test = Assert::NO_OVERRIDES.max_gap_ms(5000);
    let merged = defaults.merge(&sched).merge(&test);
    assert_eq!(merged.not_starved, None);
    assert_eq!(merged.max_imbalance_ratio, Some(2.0));
    assert_eq!(merged.max_fallback_rate, Some(50.0));
    assert_eq!(merged.max_gap_ms, Some(5000));
    assert_eq!(merged.sustained_samples, None);
}

#[test]
fn assert_merge_no_overrides_preserves_base() {
    let base = Assert::NO_OVERRIDES
        .check_not_starved()
        .max_imbalance_ratio(4.0)
        .fail_on_stall(true);
    let merged = base.merge(&Assert::NO_OVERRIDES);
    assert_eq!(merged.not_starved, Some(true));
    assert_eq!(merged.max_imbalance_ratio, Some(4.0));
    assert_eq!(merged.fail_on_stall, Some(true));
}

/// `default_checks()` is `NO_OVERRIDES`, so merging in either
/// direction is the identity.
#[test]
fn assert_merge_no_overrides_is_left_identity() {
    let merged = Assert::NO_OVERRIDES.merge(&Assert::default_checks());
    assert!(merged.not_starved.is_none());
    assert!(merged.max_imbalance_ratio.is_none());
    assert!(merged.max_gap_ms.is_none());
    assert!(merged.isolation.is_none());
}

/// The runtime three-layer chain
/// `default_checks -> scheduler -> test` collapses to
/// `NO_OVERRIDES` when both override layers are also `NO_OVERRIDES`.
#[test]
fn assert_merge_runtime_chain_with_no_overrides_yields_defaults() {
    let scheduler_assert = Assert::NO_OVERRIDES;
    let test_assert = Assert::NO_OVERRIDES;
    let merged = Assert::default_checks()
        .merge(&scheduler_assert)
        .merge(&test_assert);
    assert!(merged.not_starved.is_none());
    assert!(merged.max_imbalance_ratio.is_none());
    assert!(merged.max_local_dsq_depth.is_none());
    assert!(merged.fail_on_stall.is_none());
}

#[test]
fn assert_merge_overrides_fields() {
    let base = Assert::NO_OVERRIDES;
    let overrides = Assert::NO_OVERRIDES
        .max_imbalance_ratio(5.0)
        .max_gap_ms(1000)
        .check_not_starved();
    let merged = base.merge(&overrides);
    assert_eq!(merged.not_starved, Some(true));
    assert_eq!(merged.max_imbalance_ratio, Some(5.0));
    assert_eq!(merged.max_gap_ms, Some(1000));
}

#[test]
fn assert_merge_later_overrides_earlier() {
    let a = Assert::NO_OVERRIDES.max_imbalance_ratio(2.0);
    let b = Assert::NO_OVERRIDES.max_imbalance_ratio(10.0);
    let merged = a.merge(&b);
    assert_eq!(merged.max_imbalance_ratio, Some(10.0));
}

#[test]
fn assert_worker_plan_extracts_fields() {
    let v = Assert::NO_OVERRIDES
        .check_not_starved()
        .check_isolation()
        .max_gap_ms(500)
        .max_spread_pct(10.0);
    assert_eq!(v.not_starved, Some(true));
    assert_eq!(v.isolation, Some(true));
    let plan = v.worker_plan();
    assert!(plan.not_starved);
    assert!(plan.isolation);
    assert_eq!(plan.max_gap_ms, Some(500));
    assert_eq!(plan.max_spread_pct, Some(10.0));
}

#[test]
fn assert_monitor_thresholds_defaults() {
    let v = Assert::NO_OVERRIDES;
    let t = v.monitor_thresholds();
    // Should use MonitorThresholds::new() values.
    let d = crate::monitor::MonitorThresholds::new();
    assert_eq!(t.max_imbalance_ratio, d.max_imbalance_ratio);
    assert_eq!(t.max_local_dsq_depth, d.max_local_dsq_depth);
}

#[test]
fn assert_monitor_thresholds_overridden() {
    let v = Assert::NO_OVERRIDES
        .max_imbalance_ratio(99.0)
        .max_local_dsq_depth(42)
        .fail_on_stall(false)
        .sustained_samples(10)
        .max_fallback_rate(0.5)
        .max_keep_last_rate(0.3);
    let t = v.monitor_thresholds();
    assert_eq!(t.max_imbalance_ratio, 99.0);
    assert_eq!(t.max_local_dsq_depth, 42);
    assert!(!t.fail_on_stall);
    assert_eq!(t.sustained_samples, 10);
    assert_eq!(t.max_fallback_rate, 0.5);
    assert_eq!(t.max_keep_last_rate, 0.3);
}

#[test]
fn assert_max_spread_pct() {
    let v = Assert::NO_OVERRIDES.max_spread_pct(25.0);
    assert_eq!(v.max_spread_pct, Some(25.0));
}

#[test]
fn gap_threshold_debug_vs_release() {
    let t = gap_threshold_ms();
    // In test builds (debug_assertions=true), threshold is 3000.
    assert!(t >= 2000, "threshold should be at least 2000ms: {t}");
}

// -- Assert::has_worker_checks --

#[test]
fn assert_no_overrides_has_no_worker_checks() {
    assert!(!Assert::NO_OVERRIDES.has_worker_checks());
}

#[test]
fn assert_default_checks_has_no_worker_checks() {
    assert!(!Assert::default_checks().has_worker_checks());
}

#[test]
fn assert_single_field_has_worker_checks() {
    assert!(Assert::NO_OVERRIDES.max_gap_ms(5000).has_worker_checks());
    assert!(Assert::NO_OVERRIDES.check_isolation().has_worker_checks());
    assert!(
        Assert::NO_OVERRIDES
            .max_spread_pct(10.0)
            .has_worker_checks()
    );
    assert!(
        Assert::NO_OVERRIDES
            .max_throughput_cv(0.5)
            .has_worker_checks()
    );
    assert!(
        Assert::NO_OVERRIDES
            .min_work_rate(100.0)
            .has_worker_checks()
    );
    assert!(
        Assert::NO_OVERRIDES
            .max_p99_wake_latency_ns(1000)
            .has_worker_checks()
    );
    assert!(
        Assert::NO_OVERRIDES
            .max_wake_latency_cv(0.5)
            .has_worker_checks()
    );
    assert!(
        Assert::NO_OVERRIDES
            .min_iteration_rate(10.0)
            .has_worker_checks()
    );
    assert!(
        Assert::NO_OVERRIDES
            .max_migration_ratio(0.5)
            .has_worker_checks()
    );
}

#[test]
fn assert_monitor_only_no_worker_checks() {
    let a = Assert::NO_OVERRIDES
        .max_imbalance_ratio(5.0)
        .fail_on_stall(true);
    assert!(!a.has_worker_checks());
}

// -- Assert::merge worker + benchmark + monitor fields --

#[test]
fn assert_merge_all_field_categories() {
    // Layer 1: defaults (all None now).
    let defaults = Assert::default_checks();

    // Layer 2: scheduler sets worker and benchmark fields.
    let sched = Assert::NO_OVERRIDES
        .max_spread_pct(50.0)
        .max_p99_wake_latency_ns(100_000)
        .max_migration_ratio(0.5)
        .fail_on_stall(true);

    // Layer 3: test overrides a worker field and sets isolation.
    let test = Assert::NO_OVERRIDES.check_isolation().max_spread_pct(80.0);

    let merged = defaults.merge(&sched).merge(&test);

    // test overrides sched's spread.
    assert_eq!(merged.max_spread_pct, Some(80.0));
    // sched's benchmark fields survive (test didn't set them).
    assert_eq!(merged.max_p99_wake_latency_ns, Some(100_000));
    assert_eq!(merged.max_migration_ratio, Some(0.5));
    // test sets isolation.
    assert_eq!(merged.isolation, Some(true));
    // sched's monitor fields survive (test didn't set them).
    assert_eq!(merged.fail_on_stall, Some(true));
}

/// `Assert::expect_scx_bpf_error_contains` builder is `const fn` so
/// it composes inside `Assert::NO_OVERRIDES.expect_..._contains(...)`
/// const-context chains. Empty literals panic at construction (an
/// empty literal would silently match every message via
/// `str::contains`).
#[test]
fn expect_scx_bpf_error_contains_builder_sets_field() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_config");
    assert_eq!(a.expect_scx_bpf_error_contains, Some("apply_cell_config"));
    assert_eq!(a.expect_scx_bpf_error_matches, None);
}

#[test]
#[should_panic(expected = "must be non-empty")]
fn expect_scx_bpf_error_contains_builder_panics_on_empty() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("");
}

#[test]
fn expect_scx_bpf_error_matches_builder_sets_field() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"apply_cell_config.*-EINVAL");
    assert_eq!(
        a.expect_scx_bpf_error_matches,
        Some(r"apply_cell_config.*-EINVAL"),
    );
    assert_eq!(a.expect_scx_bpf_error_contains, None);
}

#[test]
#[should_panic(expected = "must be non-empty")]
fn expect_scx_bpf_error_matches_builder_panics_on_empty() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches("");
}

/// No matchers set → evaluator returns empty Vec regardless of
/// captured text or expect_err state. Existing tests that never opt
/// into reproducer mode must see zero matcher-driven details.
#[test]
fn evaluate_scx_bpf_error_match_returns_empty_when_no_matcher_configured() {
    let a = Assert::NO_OVERRIDES;
    assert!(
        a.evaluate_scx_bpf_error_match("some scheduler text", true)
            .is_empty(),
    );
    assert!(
        a.evaluate_scx_bpf_error_match("some scheduler text", false)
            .is_empty(),
    );
    assert!(a.evaluate_scx_bpf_error_match("", true).is_empty());
}

/// Matcher set + expect_err = false → evaluator rejects with a
/// misuse diagnostic naming the expect_err requirement. Catches
/// runtime misuse even if the entry-time `KtstrTestEntry::validate`
/// gate is somehow bypassed.
#[test]
fn evaluate_scx_bpf_error_match_rejects_when_expect_err_unset() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("anything");
    let details = a.evaluate_scx_bpf_error_match("text including anything substring", false);
    assert_eq!(details.len(), 1);
    let msg = &details[0].message;
    assert!(
        msg.contains("expect_err = true"),
        "misuse diagnostic must name expect_err: {msg}",
    );
}

/// Literal-substring matcher passes when the literal appears in the
/// captured corpus.
#[test]
fn evaluate_scx_bpf_error_match_contains_passes_on_substring_present() {
    let a =
        Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_config returned -EINVAL");
    let corpus = "scx_mitosis: apply_cell_config returned -EINVAL at line:42; cell_id=3\n";
    let details = a.evaluate_scx_bpf_error_match(corpus, true);
    assert!(
        details.is_empty(),
        "matcher must pass on substring match: {details:?}"
    );
}

/// Literal-substring matcher fails when the literal is absent, with
/// a diagnostic naming the expected literal AND quoting the corpus
/// so the operator can compare at a glance.
#[test]
fn evaluate_scx_bpf_error_match_contains_fails_on_absent_substring() {
    let a =
        Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_config returned -EINVAL");
    let corpus = "scx_mitosis: a DIFFERENT bug fired: stuck task X for 5000ms\n";
    let details = a.evaluate_scx_bpf_error_match(corpus, true);
    assert_eq!(details.len(), 1);
    let msg = &details[0].message;
    assert!(
        msg.contains("apply_cell_config returned -EINVAL"),
        "diagnostic must name expected literal: {msg}",
    );
    assert!(
        msg.contains("a DIFFERENT bug fired"),
        "diagnostic must include the actual captured corpus: {msg}",
    );
}

/// Regex matcher passes on match — anchors, character classes, and
/// captures all work via the `regex` crate.
#[test]
fn evaluate_scx_bpf_error_match_regex_passes_on_match() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"apply_cell_config.*-E[A-Z]+");
    let corpus = "scx_mitosis: apply_cell_config returned -EINVAL at line:42\n";
    let details = a.evaluate_scx_bpf_error_match(corpus, true);
    assert!(details.is_empty(), "regex matcher must pass: {details:?}");
}

#[test]
fn evaluate_scx_bpf_error_match_regex_fails_on_mismatch() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"apply_cell_config.*-E[A-Z]+");
    let corpus = "scx_mitosis: stuck task for 5000ms\n";
    let details = a.evaluate_scx_bpf_error_match(corpus, true);
    assert_eq!(details.len(), 1);
    let msg = &details[0].message;
    assert!(msg.contains("apply_cell_config"));
    assert!(msg.contains("stuck task"));
}

/// Invalid regex syntax panics at construction with a diagnostic
/// naming the pattern AND the compile error. Construction-time
/// rejection means a typo in the pattern surfaces immediately when
/// the test binary loads (or the `#[ktstr_test]` macro generates
/// the entry) instead of silently sitting in the entry until the
/// matcher actually evaluates a corpus.
#[test]
#[should_panic(expected = "is not valid regex")]
fn evaluate_scx_bpf_error_match_regex_invalid_pattern_panics_at_construction() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches("[unclosed");
}

/// Both matchers set + both match → empty details.
#[test]
fn evaluate_scx_bpf_error_match_both_set_and_both_match_passes() {
    let a = Assert::NO_OVERRIDES
        .expect_scx_bpf_error_contains("apply_cell_config")
        .expect_scx_bpf_error_matches(r"returned -E[A-Z]+");
    let corpus = "scx_mitosis: apply_cell_config returned -EINVAL\n";
    assert!(a.evaluate_scx_bpf_error_match(corpus, true).is_empty());
}

/// Both matchers set + only one matches → matcher fails with a
/// diagnostic for the one that didn't.
#[test]
fn evaluate_scx_bpf_error_match_both_set_one_mismatches_fails() {
    let a = Assert::NO_OVERRIDES
        .expect_scx_bpf_error_contains("apply_cell_config")
        .expect_scx_bpf_error_matches(r"returned -E[A-Z]+");
    let corpus = "scx_mitosis: apply_cell_config but no errno here\n";
    let details = a.evaluate_scx_bpf_error_match(corpus, true);
    assert_eq!(details.len(), 1);
    assert!(details[0].message.contains("returned -E"));
}

/// Multi-event scenario: when the upstream pipeline concatenates
/// two or more `sched_ext: BPF scheduler "..." disabled (...)` exit
/// events into one corpus (rapid load+disable cycles per
/// `dmesg_scx.rs:157-159`), the matcher must scan the WHOLE
/// concatenated string, not stop at the first event boundary.
///
/// The corpus below interleaves two distinct events; the matched
/// pattern lives ONLY in the second event. A regression that
/// scanned only the first event would treat the second as if it
/// didn't exist — the matcher would fail despite the bug actually
/// reproducing in the second cycle. Pin BOTH the literal and regex
/// matchers since their corpus-scanning paths are independent
/// (`str::contains` vs. `regex::Regex::is_match`).
#[test]
fn evaluate_scx_bpf_error_match_finds_pattern_in_later_event() {
    let corpus = "scx_mitosis: first different error at line:1\n\
                  sched_ext: BPF scheduler \"scx_mitosis\" disabled (Error)\n\
                  scx_mitosis: apply_cell_config returned -EINVAL at line:42\n\
                  sched_ext: BPF scheduler \"scx_mitosis\" disabled (Error)\n";

    // Literal-substring matcher walks the whole corpus.
    let a_lit =
        Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_config returned -EINVAL");
    let lit_details = a_lit.evaluate_scx_bpf_error_match(corpus, true);
    assert!(
        lit_details.is_empty(),
        "contains matcher must scan the WHOLE concatenated corpus, not stop \
         at the first event boundary: {lit_details:?}",
    );

    // Regex matcher walks the whole corpus.
    let a_re = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"apply_cell_config.*-E[A-Z]+");
    let re_details = a_re.evaluate_scx_bpf_error_match(corpus, true);
    assert!(
        re_details.is_empty(),
        "regex matcher must scan the WHOLE concatenated corpus: {re_details:?}",
    );
}

/// Empty corpus with a configured literal matcher fails loudly. The
/// diagnostic must name the matcher, quote the expected literal, and
/// surface the corpus length so an operator can tell at a glance
/// whether the scheduler produced any captured text at all. Pins that
/// "0 bytes" appears verbatim — that token is the operator's signal
/// that the upstream capture pipeline (bulk-port scheduler log +
/// sched_ext dump extract) produced nothing, distinct from "matcher
/// mismatched substantial output."
#[test]
fn evaluate_scx_bpf_error_match_contains_fails_on_empty_corpus() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_config");
    let details = a.evaluate_scx_bpf_error_match("", true);
    assert_eq!(details.len(), 1);
    let msg = &details[0].message;
    assert!(
        msg.contains("expect_scx_bpf_error_contains"),
        "diagnostic must name the matcher: {msg}",
    );
    assert!(
        msg.contains("apply_cell_config"),
        "diagnostic must quote the expected literal: {msg}",
    );
    assert!(
        msg.contains("0 bytes"),
        "diagnostic must surface the corpus length so operators can distinguish \
         empty-capture from matcher-mismatch: {msg}",
    );
    assert!(
        msg.contains("up to 400 bytes follow:"),
        "diagnostic must include the standard excerpt suffix even when empty: {msg}",
    );
}

/// Empty corpus with a configured regex matcher fails loudly with
/// the same diagnostic shape as the literal-substring variant.
#[test]
fn evaluate_scx_bpf_error_match_regex_fails_on_empty_corpus() {
    let pattern = r"apply_cell_config.*-EINVAL";
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(pattern);
    let details = a.evaluate_scx_bpf_error_match("", true);
    assert_eq!(details.len(), 1);
    let msg = &details[0].message;
    assert!(
        msg.contains("expect_scx_bpf_error_matches"),
        "diagnostic must name the matcher: {msg}",
    );
    assert!(
        msg.contains(pattern),
        "diagnostic must quote the pattern: {msg}",
    );
    assert!(
        msg.contains("0 bytes"),
        "diagnostic must surface the corpus length: {msg}",
    );
    assert!(
        msg.contains("up to 400 bytes follow:"),
        "diagnostic must include the standard excerpt suffix: {msg}",
    );
}

/// Both matchers configured + empty corpus → each produces its own
/// failure detail (AND-semantics: every matcher must hit, every miss
/// surfaces independently). Pin both details land, but do not pin
/// their order — a future refactor that swaps order would still be
/// semantically correct.
#[test]
fn evaluate_scx_bpf_error_match_both_fail_on_empty_corpus() {
    let pattern = r"apply_cell_config.*-EINVAL";
    let a = Assert::NO_OVERRIDES
        .expect_scx_bpf_error_contains("apply_cell_config")
        .expect_scx_bpf_error_matches(pattern);
    let details = a.evaluate_scx_bpf_error_match("", true);
    assert_eq!(
        details.len(),
        2,
        "both matchers must produce independent failure details on empty corpus; got {details:?}",
    );
    assert!(
        details
            .iter()
            .any(|d| d.message.contains("expect_scx_bpf_error_contains")),
        "one detail must name the contains matcher: {details:?}",
    );
    assert!(
        details
            .iter()
            .any(|d| d.message.contains("expect_scx_bpf_error_matches")),
        "the other detail must name the regex matcher: {details:?}",
    );
    for d in &details {
        assert!(
            d.message.contains("0 bytes"),
            "every detail must include the corpus-size diagnostic: {}",
            d.message,
        );
    }
}

/// The failure-diagnostic excerpt is byte-truncated at a UTF-8
/// boundary so its size matches the "up to 400 bytes follow:" prose.
/// A future regression that switched back to `chars().take(400)` would
/// produce excerpts up to 4 × 400 bytes on multi-byte corpora; pin
/// here so the byte-budget contract holds. The fixture uses em-dashes
/// (3-byte UTF-8): byte 400 lands mid-codepoint, so the boundary walk
/// steps back to 399. This is the only 1-byte/2-byte/3-byte/4-byte
/// width that produces a non-no-op walk against a 400-byte budget;
/// 2-byte and 4-byte codepoints land cleanly on byte 400 and would
/// trivially pass the fast path.
///
/// Both matcher branches share the `excerpt` closure today, so this
/// test pins the contract via the literal branch AND a second
/// assertion via the regex branch — if a future refactor splits the
/// closure (e.g. matcher path adds context lines), this test catches
/// the regression rather than silently passing on the surviving
/// shared path.
#[test]
fn evaluate_scx_bpf_error_match_excerpt_byte_truncated_at_char_boundary() {
    // 200 em-dashes (each 3 bytes in UTF-8) = 600 bytes / 200 chars.
    // chars().take(400) would yield all 200 chars = 600 bytes (over budget);
    // byte-truncation stops at or below 400 bytes.
    let mut corpus = String::new();
    for _ in 0..200 {
        corpus.push('\u{2014}'); // em-dash, 3 UTF-8 bytes
    }
    assert_eq!(corpus.len(), 600, "test fixture: 200 em-dashes = 600 bytes");

    let assert_excerpt_within_budget = |msg: &str| {
        let after_marker = msg
            .split_once("up to 400 bytes follow:\n")
            .unwrap_or_else(|| panic!("diagnostic missing excerpt marker: {msg}"))
            .1;
        // 400 / 3 = 133 full em-dashes fit (399 bytes); the 401st byte
        // would split a codepoint, so the truncation steps back to 399.
        // Exact-length pin catches both "no truncation" regressions
        // (600 bytes) and forward-walk off-by-one (402 bytes) — and
        // since the slice is valid UTF-8 by construction, the codepoint
        // composition assertion below also pins that the excerpt is a
        // legitimate em-dash prefix rather than a lossy decode.
        assert_eq!(
            after_marker.len(),
            399,
            "200 em-dashes at 3 bytes each: 133 fit in 399 bytes; 134 would overflow at 402",
        );
        assert!(
            after_marker.chars().all(|c| c == '\u{2014}'),
            "excerpt must contain only em-dashes (no replacement char or partial codepoint): {after_marker:?}",
        );
    };

    // Literal matcher branch.
    let literal = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("not present");
    let literal_details = literal.evaluate_scx_bpf_error_match(&corpus, true);
    assert_eq!(literal_details.len(), 1);
    assert_excerpt_within_budget(&literal_details[0].message);

    // Regex matcher branch — uses the same `excerpt` closure today,
    // but a future refactor that splits the two branches would let
    // a regression slip past a literal-only pin.
    let regex = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"not present anywhere");
    let regex_details = regex.evaluate_scx_bpf_error_match(&corpus, true);
    assert_eq!(regex_details.len(), 1);
    assert_excerpt_within_budget(&regex_details[0].message);
}

/// Literal-substring matcher does NOT trim or normalize whitespace.
/// A pattern with leading or trailing spaces only matches a corpus
/// with those bytes present verbatim. Pins this so a future "helpful"
/// trim regression (e.g. `captured_text.trim().contains(literal.trim())`)
/// fails here — operators rely on byte-for-byte matching to pin
/// exact kernel printk strings.
#[test]
fn evaluate_scx_bpf_error_match_contains_no_trim_on_literal() {
    let leading = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("  leading spaces");
    // Corpus has "leading spaces" but NOT with the two-space prefix.
    let no_match = leading.evaluate_scx_bpf_error_match("the line: leading spaces here", true);
    assert_eq!(
        no_match.len(),
        1,
        "literal with two leading spaces must NOT match corpus where the only \
         instance lacks the leading spaces; got {no_match:?}",
    );
    // Same matcher against corpus that DOES have the two-space prefix → empty.
    let matches =
        leading.evaluate_scx_bpf_error_match("prefix\n  leading spaces appear here", true);
    assert!(
        matches.is_empty(),
        "literal with leading spaces must match when corpus has them byte-for-byte; got {matches:?}",
    );

    // Trailing-whitespace symmetry. The corpus must contain the BASE
    // token "trailing" so that a hypothetical `.trim_end()` regression
    // would make the literal match incorrectly — comparing against an
    // unrelated corpus would pass under either implementation and
    // wouldn't pin no-trim semantics.
    let trailing = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("trailing  ");
    // Negative: corpus has "trailing" with only one trailing space, not two.
    // Under a `.trim_end()` regression both sides would collapse to "trailing"
    // and the matcher would silently succeed.
    let no_trail_match = trailing.evaluate_scx_bpf_error_match("some trailing text", true);
    assert_eq!(
        no_trail_match.len(),
        1,
        "literal with two trailing spaces must NOT match corpus where the base \
         token is followed by one space (catches hypothetical trim regression): \
         {no_trail_match:?}",
    );
    // Positive: corpus has the two trailing spaces byte-for-byte.
    let trail_match = trailing.evaluate_scx_bpf_error_match("some trailing  text", true);
    assert!(
        trail_match.is_empty(),
        "literal with two trailing spaces must match corpus with two spaces \
         byte-for-byte: {trail_match:?}",
    );
}

/// Literal-substring matcher is unaffected by embedded `\n` / `\t` in
/// the corpus around the literal token. Pin that the contains() walk
/// is byte-level — newlines and tabs surrounding the match are not
/// boundaries the matcher cares about.
#[test]
fn evaluate_scx_bpf_error_match_contains_unaffected_by_newlines_and_tabs() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_config");
    let corpus = "first line\n\tindented tab here apply_cell_config more\n";
    let details = a.evaluate_scx_bpf_error_match(corpus, true);
    assert!(
        details.is_empty(),
        "literal match must succeed across embedded whitespace surroundings: {details:?}",
    );

    // A literal that itself contains a newline only matches when that
    // exact byte sequence is present — newlines aren't elided.
    let with_nl = Assert::NO_OVERRIDES.expect_scx_bpf_error_contains("apply_cell_conf\nig");
    let nl_details = with_nl.evaluate_scx_bpf_error_match("apply_cell_config", true);
    assert_eq!(
        nl_details.len(),
        1,
        "literal containing a newline must NOT match a corpus without that newline: {nl_details:?}",
    );
}

/// Regex `^` / `$` are STRING-boundary anchors by default — they
/// match the start / end of the WHOLE corpus, not individual lines.
/// Operators who want line-level anchoring must opt in with the
/// inline `(?m)` flag. Pin BOTH anchors:
///   `$` end-anchor:
///     (1) `apply_cell_config$` fails when the token is mid-corpus,
///     (2) `apply_cell_config$` matches when the token is at corpus-end,
///     (3) `(?m)apply_cell_config$` matches the token at any line-end.
///   `^` start-anchor:
///     (4) `^apply_cell_config` fails when the token is mid-corpus,
///     (5) `^apply_cell_config` matches when the token is at corpus-start,
///     (6) `(?m)^apply_cell_config` matches the token at any line-start.
#[test]
fn evaluate_scx_bpf_error_match_regex_anchors_are_string_boundaries_by_default() {
    // $ end-anchor — default mode: anchors to string-end.
    let dollar = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"apply_cell_config$");
    let dollar_mid = "apply_cell_config\nbut more after";
    let dollar_mid_details = dollar.evaluate_scx_bpf_error_match(dollar_mid, true);
    assert_eq!(
        dollar_mid_details.len(),
        1,
        "default-mode $ anchors to STRING-end; mid-corpus token must NOT match: {dollar_mid_details:?}",
    );

    // $ end-anchor — default mode positive: matches when token IS at string-end.
    let dollar_end = "prefix junk\napply_cell_config";
    let dollar_end_details = dollar.evaluate_scx_bpf_error_match(dollar_end, true);
    assert!(
        dollar_end_details.is_empty(),
        "default-mode $ matches when corpus actually ends with the token: {dollar_end_details:?}",
    );

    // $ end-anchor — (?m) opt-in: now matches line-end.
    let dollar_multiline =
        Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"(?m)apply_cell_config$");
    let dollar_multi_details = dollar_multiline.evaluate_scx_bpf_error_match(dollar_mid, true);
    assert!(
        dollar_multi_details.is_empty(),
        "(?m) opt-in makes $ match line-end; mid-corpus token at line-end matches: {dollar_multi_details:?}",
    );

    // ^ start-anchor — default mode: anchors to string-start.
    let caret = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"^apply_cell_config");
    let caret_mid = "prefix junk\napply_cell_config more";
    let caret_mid_details = caret.evaluate_scx_bpf_error_match(caret_mid, true);
    assert_eq!(
        caret_mid_details.len(),
        1,
        "default-mode ^ anchors to STRING-start; mid-corpus token must NOT match: {caret_mid_details:?}",
    );

    // ^ start-anchor — default mode positive: matches when token IS at string-start.
    let caret_start = "apply_cell_config\nbut more after";
    let caret_start_details = caret.evaluate_scx_bpf_error_match(caret_start, true);
    assert!(
        caret_start_details.is_empty(),
        "default-mode ^ matches when corpus actually starts with the token: {caret_start_details:?}",
    );

    // ^ start-anchor — (?m) opt-in: now matches line-start.
    let caret_multiline =
        Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"(?m)^apply_cell_config");
    let caret_multi_details = caret_multiline.evaluate_scx_bpf_error_match(caret_mid, true);
    assert!(
        caret_multi_details.is_empty(),
        "(?m) opt-in makes ^ match line-start; mid-corpus token at line-start matches: {caret_multi_details:?}",
    );
}

/// Regex `.` does NOT match `\n` by default. Operators who want a
/// pattern to span lines must opt in with the inline `(?s)` (DOTALL)
/// flag. Pin all three cases:
///   (1) `apply.*EINVAL` fails when the two tokens are on separate lines,
///   (2) `apply.*EINVAL` matches when both are on the same line,
///   (3) `(?s)apply.*EINVAL` matches across line boundaries.
#[test]
fn evaluate_scx_bpf_error_match_regex_dot_excludes_newline_by_default() {
    let a = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"apply.*EINVAL");

    let split = "apply_cell_config triggered\n-EINVAL returned";
    let split_details = a.evaluate_scx_bpf_error_match(split, true);
    assert_eq!(
        split_details.len(),
        1,
        "default-mode . excludes \\n; pattern spanning lines must NOT match: {split_details:?}",
    );

    let same_line = "apply_cell_config triggered -EINVAL returned";
    let same_details = a.evaluate_scx_bpf_error_match(same_line, true);
    assert!(
        same_details.is_empty(),
        "default-mode . matches non-newline bytes; same-line pattern matches: {same_details:?}",
    );

    let a_dotall = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"(?s)apply.*EINVAL");
    let dotall_details = a_dotall.evaluate_scx_bpf_error_match(split, true);
    assert!(
        dotall_details.is_empty(),
        "(?s) DOTALL opt-in makes . cross newlines; split-line pattern matches: {dotall_details:?}",
    );
}

/// Each pattern that satisfies `is_match("")` gets its own
/// `#[should_panic]` test. Per-pattern tests give each rejection a
/// distinct backtrace target — when the rejection gate ever
/// weakens, the test for the specific pattern that slipped through
/// fails by name, telling the operator which case regressed. The
/// four pinned cases cover both no-op classes the gate rejects:
/// patterns that match every position (`a?` optional, `.*`
/// zero-or-more, `(?:)` empty non-capturing group) trivially pass
/// against any corpus, and the start-immediately-end anchor (`^$`)
/// trivially fails against any non-empty corpus. All four share
/// the `is_match("")` predicate — one check rejects both no-op
/// classes.
#[test]
#[should_panic(expected = "matches the empty string")]
fn evaluate_scx_bpf_error_match_rejects_vacuous_optional() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"a?");
}

#[test]
#[should_panic(expected = "matches the empty string")]
fn evaluate_scx_bpf_error_match_rejects_vacuous_star() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r".*");
}

#[test]
#[should_panic(expected = "matches the empty string")]
fn evaluate_scx_bpf_error_match_rejects_vacuous_empty_group() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"(?:)");
}

#[test]
#[should_panic(expected = "matches the empty string")]
fn evaluate_scx_bpf_error_match_rejects_vacuous_anchored_empty() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"^$");
}

/// `(a|)` is the alternation form of vacuous — distinct syntactic
/// family from the quantifier (`a?`, `.*`), empty-group (`(?:)`),
/// and anchor-pair (`^$`) families. A real-world footgun: an
/// operator writes `expect_matches(r"(error|)")` intending "match
/// the word error or anything else" without realizing the empty
/// alternation branch makes the whole pattern vacuous. Pinning
/// this case ensures the construction gate's `is_match("")`
/// predicate continues to catch the alternation form too.
#[test]
#[should_panic(expected = "matches the empty string")]
fn evaluate_scx_bpf_error_match_rejects_vacuous_alternation_with_empty_branch() {
    let _ = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches(r"(a|)");
}

/// The builder's construction-time validation is bypassed when an
/// `Assert` is built via struct-literal syntax (the field is `pub`).
/// In that case the evaluator's defense-in-depth catches invalid
/// regex syntax at first use — pinning that path here ensures a
/// hypothetical bypass route (a deserializer, a test helper that
/// constructs `Assert` directly, or any future code that field-
/// inits the struct) still surfaces a loud diagnostic rather than
/// silently no-op'ing. The builder-path equivalent
/// (`evaluate_scx_bpf_error_match_regex_invalid_pattern_panics_at_construction`)
/// pins the construction-time panic; this pins the evaluator-side
/// fallback for the bypass case.
#[test]
fn evaluate_scx_bpf_error_match_regex_invalid_pattern_via_direct_construction_fails_loudly() {
    let a = Assert {
        expect_scx_bpf_error_matches: Some("[unclosed"),
        ..Assert::NO_OVERRIDES
    };
    let details = a.evaluate_scx_bpf_error_match("corpus text", true);
    assert_eq!(details.len(), 1);
    let msg = &details[0].message;
    assert!(
        msg.contains("[unclosed"),
        "invalid-pattern diagnostic must name the pattern: {msg}",
    );
    assert!(
        msg.contains("regex compilation failed"),
        "invalid-pattern diagnostic must say the pattern failed to compile: {msg}",
    );
}

/// Regex literal whitespace (space, tab) in the pattern matches the
/// same bytes in the corpus — no normalization. A pattern with a
/// literal tab `\t` only matches a corpus with a literal tab, not a
/// corpus with a space at that position.
#[test]
fn evaluate_scx_bpf_error_match_regex_literal_whitespace_matches_byte_for_byte() {
    let a_tab = Assert::NO_OVERRIDES.expect_scx_bpf_error_matches("apply\tcell");

    let with_tab = "apply\tcell_config triggered";
    let tab_match = a_tab.evaluate_scx_bpf_error_match(with_tab, true);
    assert!(
        tab_match.is_empty(),
        "regex with literal tab must match corpus with literal tab: {tab_match:?}",
    );

    let with_space = "apply cell_config triggered";
    let space_no_match = a_tab.evaluate_scx_bpf_error_match(with_space, true);
    assert_eq!(
        space_no_match.len(),
        1,
        "regex with literal tab must NOT match corpus with a space in that position: {space_no_match:?}",
    );
}

// -- serde derive on Assert -------------------------------------------

/// Compile-time pin that `Assert` implements `Serialize +
/// Deserialize`. A future regression that drops either trait would
/// break sidecar carrying of threshold config; pin it here so the
/// drop trips at type-check time, not at the sidecar consumer's
/// deserialize failure.
#[test]
fn assert_implements_serialize_and_deserialize() {
    fn requires<T: serde::Serialize + serde::de::DeserializeOwned>() {}
    requires::<Assert>();
}

/// Full round-trip: every numeric / bool field on `Assert` survives
/// serialize → JSON → deserialize. The two reproducer-string fields
/// (expect_scx_bpf_error_contains / _matches) are intentionally marked
/// `#[serde(skip)]` because their `&'static str` shape can't be
/// deserialized (no lifetime context in the deserializer); the
/// strings are test-author static literals carried in the test
/// definition, not runtime data the sidecar needs. See the dedicated
/// `assert_serde_skips_reproducer_string_fields` test below for the
/// skip contract.
#[test]
fn assert_serde_roundtrip_preserves_every_non_skipped_field() {
    let a = Assert {
        not_starved: Some(true),
        isolation: Some(true),
        max_gap_ms: Some(1234),
        max_spread_pct: Some(12.5),
        max_throughput_cv: Some(0.42),
        min_work_rate: Some(100.0),
        max_p99_wake_latency_ns: Some(5_000_000),
        max_wake_latency_cv: Some(0.7),
        min_iteration_rate: Some(500.0),
        max_migration_ratio: Some(0.05),
        max_imbalance_ratio: Some(2.5),
        max_local_dsq_depth: Some(64),
        fail_on_stall: Some(true),
        sustained_samples: Some(3),
        max_fallback_rate: Some(0.01),
        max_keep_last_rate: Some(0.005),
        enforce_monitor_thresholds: true,
        min_page_locality: Some(0.95),
        max_cross_node_migration_ratio: Some(0.02),
        max_slow_tier_ratio: Some(0.1),
        expect_scx_bpf_error_contains: None,
        expect_scx_bpf_error_matches: None,
    };
    let json = serde_json::to_string(&a).unwrap();
    let b: Assert = serde_json::from_str(&json).unwrap();
    assert_eq!(a.not_starved, b.not_starved);
    assert_eq!(a.isolation, b.isolation);
    assert_eq!(a.max_gap_ms, b.max_gap_ms);
    assert_eq!(a.max_spread_pct, b.max_spread_pct);
    assert_eq!(a.max_throughput_cv, b.max_throughput_cv);
    assert_eq!(a.min_work_rate, b.min_work_rate);
    assert_eq!(a.max_p99_wake_latency_ns, b.max_p99_wake_latency_ns);
    assert_eq!(a.max_wake_latency_cv, b.max_wake_latency_cv);
    assert_eq!(a.min_iteration_rate, b.min_iteration_rate);
    assert_eq!(a.max_migration_ratio, b.max_migration_ratio);
    assert_eq!(a.max_imbalance_ratio, b.max_imbalance_ratio);
    assert_eq!(a.max_local_dsq_depth, b.max_local_dsq_depth);
    assert_eq!(a.fail_on_stall, b.fail_on_stall);
    assert_eq!(a.sustained_samples, b.sustained_samples);
    assert_eq!(a.max_fallback_rate, b.max_fallback_rate);
    assert_eq!(a.max_keep_last_rate, b.max_keep_last_rate);
    assert_eq!(a.enforce_monitor_thresholds, b.enforce_monitor_thresholds);
    assert_eq!(a.min_page_locality, b.min_page_locality);
    assert_eq!(
        a.max_cross_node_migration_ratio,
        b.max_cross_node_migration_ratio
    );
    assert_eq!(a.max_slow_tier_ratio, b.max_slow_tier_ratio);
}

// -- Outcome enum -----------------------------------------------------

/// Compile-time pin that `Outcome` implements the documented derive
/// set. A regression that drops Serialize/Deserialize (breaking
/// sidecar wire-format) or PartialEq/Eq (breaking stats-comparison
/// tooling) would trip at type-check time.
#[test]
fn outcome_implements_expected_traits() {
    fn requires<
        T: Clone + std::fmt::Debug + PartialEq + Eq + serde::Serialize + serde::de::DeserializeOwned,
    >() {
    }
    requires::<Outcome>();
}

/// `Outcome::merge` precedence on the **3-variant (Fail / Pass /
/// Skip) subset** of the full `Fail > Inconclusive > Pass > Skip`
/// lattice. Full 9-case commutative truth table over these three
/// variants; any cell flipping would indicate the sub-lattice
/// reordered. The `Inconclusive` arm of the full lattice is pinned
/// separately by
/// [`outcome_merge_precedence_inconclusive_above_pass_below_fail`].
/// A "Fail > Skip > Pass" alternative would flip the existing
/// `merge_skip_plus_pass_demotes_skip` test invariant; the
/// `Fail > Pass > Skip` ordering is the one this implementation
/// actually realizes on the three-variant subset.
#[test]
fn outcome_merge_precedence_three_variant_subset_fail_pass_skip() {
    let d = AssertDetail::new(DetailKind::Other, "payload");
    let pass = || Outcome::Pass;
    let skip = || Outcome::Skip(d.clone());
    let fail = || Outcome::Fail(d.clone());
    // Skip ∪ Pass → Pass (both orderings)
    assert!(skip().merge(pass()).is_pass());
    assert!(pass().merge(skip()).is_pass());
    // Skip ∪ Fail → Fail (both orderings)
    assert!(skip().merge(fail()).is_fail());
    assert!(fail().merge(skip()).is_fail());
    // Pass ∪ Fail → Fail (both orderings — any failure wins)
    assert!(pass().merge(fail()).is_fail());
    assert!(fail().merge(pass()).is_fail());
    // Identity cases
    assert!(pass().merge(pass()).is_pass());
    assert!(skip().merge(skip()).is_skip());
    assert!(fail().merge(fail()).is_fail());
}

/// `Outcome::{is_pass, is_skip, is_fail, is_inconclusive}` 4×4 truth
/// table. A regression that returned wrong polarity (e.g. `is_skip`
/// silently returning false for `Skip(_)` because of a pattern-match
/// typo) would trip here.
#[test]
fn outcome_accessor_truth_table() {
    let d = AssertDetail::new(DetailKind::Other, "x");
    assert!(Outcome::Pass.is_pass());
    assert!(!Outcome::Pass.is_skip());
    assert!(!Outcome::Pass.is_fail());
    assert!(!Outcome::Pass.is_inconclusive());

    assert!(!Outcome::Skip(d.clone()).is_pass());
    assert!(Outcome::Skip(d.clone()).is_skip());
    assert!(!Outcome::Skip(d.clone()).is_fail());
    assert!(!Outcome::Skip(d.clone()).is_inconclusive());

    assert!(!Outcome::Fail(d.clone()).is_pass());
    assert!(!Outcome::Fail(d.clone()).is_skip());
    assert!(Outcome::Fail(d.clone()).is_fail());
    assert!(!Outcome::Fail(d.clone()).is_inconclusive());

    assert!(!Outcome::Inconclusive(d.clone()).is_pass());
    assert!(!Outcome::Inconclusive(d.clone()).is_skip());
    assert!(!Outcome::Inconclusive(d.clone()).is_fail());
    assert!(Outcome::Inconclusive(d.clone()).is_inconclusive());
}

/// `Outcome::merge` precedence around the `Inconclusive` variant.
/// Lattice: `Fail > Inconclusive > Pass > Skip`. An Inconclusive
/// dominates Pass and Skip (because "couldn't evaluate" is not a
/// real Pass), but Fail still dominates Inconclusive (a real
/// failure beats an unevaluated check). Pins the precedence rule
/// the verdict pipeline relies on for zero-denominator ratio
/// asserts.
#[test]
fn outcome_merge_precedence_inconclusive_above_pass_below_fail() {
    let d = AssertDetail::new(DetailKind::Other, "payload");
    let pass = || Outcome::Pass;
    let skip = || Outcome::Skip(d.clone());
    let fail = || Outcome::Fail(d.clone());
    let inconc = || Outcome::Inconclusive(d.clone());
    // Inconclusive ∪ Pass → Inconclusive (both orderings)
    assert!(inconc().merge(pass()).is_inconclusive());
    assert!(pass().merge(inconc()).is_inconclusive());
    // Inconclusive ∪ Skip → Inconclusive (both orderings)
    assert!(inconc().merge(skip()).is_inconclusive());
    assert!(skip().merge(inconc()).is_inconclusive());
    // Inconclusive ∪ Fail → Fail (both orderings — Fail dominates)
    assert!(inconc().merge(fail()).is_fail());
    assert!(fail().merge(inconc()).is_fail());
    // Identity (any payload survives — same-discriminant merge).
    assert!(inconc().merge(inconc()).is_inconclusive());

    // LEFT-payload-tie pin: same-discriminant Inconclusive merges
    // preserve the LEFT operand's payload (mirrors the Fail+Fail and
    // Skip+Skip semantic). A regression to RIGHT-wins would silently
    // change which Inconclusive reason surfaces in the terminal
    // verdict.
    let left = AssertDetail::new(DetailKind::Migration, "first-inconc");
    let right = AssertDetail::new(DetailKind::Benchmark, "second-inconc");
    let Outcome::Inconclusive(d) = Outcome::Inconclusive(left).merge(Outcome::Inconclusive(right))
    else {
        panic!("Inconclusive+Inconclusive must yield Inconclusive");
    };
    assert_eq!(d.kind, DetailKind::Migration, "LEFT-wins on payload tie");
    assert!(d.message.contains("first-inconc"));
}

/// `AssertResult::record_inconclusive` appends an `Outcome::Inconclusive`
/// and the payload surfaces via `inconclusive_details()`. Pins that
/// a Fail-plus-Inconclusive stream reads as Fail (Fail dominates) and
/// that `is_pass()` returns false for an Inconclusive-only stream so a
/// zero-denominator ratio doesn't slip past CI gates as Pass.
#[test]
fn record_inconclusive_appears_in_inconclusive_details_and_is_not_pass() {
    let mut r = AssertResult::pass();
    r.record_inconclusive(AssertDetail::new(
        DetailKind::Migration,
        "denominator was zero",
    ));
    assert!(!r.is_pass(), "inconclusive must not read as pass");
    assert!(!r.is_fail());
    assert!(!r.is_skip());
    assert!(r.is_inconclusive());
    let reasons: Vec<_> = r.inconclusive_details().collect();
    assert_eq!(reasons.len(), 1);
    assert_eq!(reasons[0].kind, DetailKind::Migration);
    assert!(reasons[0].message.contains("denominator was zero"));
    // failure_details must NOT include Inconclusive payloads — they
    // are a sibling iterator. A regression that folded them in would
    // misclassify the verdict as a Fail.
    assert_eq!(r.failure_details().count(), 0);

    // Fail dominates Inconclusive — adding a Fail to an Inconclusive
    // stream flips is_fail() true and clears is_inconclusive() per
    // the `Fail > Inconclusive > Pass > Skip` precedence.
    r.record_fail(AssertDetail::new(DetailKind::Other, "real failure"));
    assert!(r.is_fail());
    assert!(!r.is_inconclusive(), "Fail dominates Inconclusive");
}

/// `AssertResult::outcome()` folds the `outcomes: Vec<Outcome>` slot
/// into a single terminal verdict per `Outcome::merge`'s precedence
/// (`Fail > Inconclusive > Pass > Skip`; identity Pass). Pins the
/// three non-Inconclusive arms of the synthesis rules so consumers
/// that read `outcome()` see Pass / Skip / Fail without depending
/// on the inner vec structure. The Inconclusive arm is pinned by
/// [`record_inconclusive_appears_in_inconclusive_details_and_is_not_pass`].
#[test]
fn assert_result_outcome_folds_outcomes_vec() {
    // Pass result with no notable details → Outcome::Pass.
    assert!(AssertResult::pass().outcome().is_pass());

    // Skip result → Outcome::Skip carrying the Skip-kind detail.
    let skip_result = AssertResult::skip("topology missing");
    let Outcome::Skip(d) = skip_result.outcome() else {
        panic!("expected Outcome::Skip, got {:?}", skip_result.outcome());
    };
    assert_eq!(d.kind, DetailKind::Skip);
    assert!(d.message.contains("topology missing"));

    // Fail result → Outcome::Fail carrying the non-Skip detail.
    let fail_result = AssertResult::fail(AssertDetail::new(DetailKind::Starved, "boom"));
    let Outcome::Fail(d) = fail_result.outcome() else {
        panic!("expected Outcome::Fail, got {:?}", fail_result.outcome());
    };
    assert_eq!(d.kind, DetailKind::Starved);
    assert!(d.message.contains("boom"));
}

/// Mixed `Pass + Skip` stream: `outcome()` must return
/// `Outcome::Pass`, NOT Skip. Pins the inner-else branch in the
/// outcome() folder — a real Pass marker alongside any Skip beats
/// the Skip per the "Pass demotes Skip" semantic that
/// `merge_skip_plus_explicit_pass_demotes_skip` already pins on
/// `is_pass()`. A regression that returned `Outcome::Skip` here
/// would silently downgrade a passing scenario's terminal verdict.
#[test]
fn assert_result_outcome_pass_plus_skip_is_pass_not_skip() {
    let mut r = AssertResult::pass();
    r.record_pass();
    r.record_skip("optional probe");
    assert!(matches!(r.outcome(), Outcome::Pass));
    assert!(r.is_pass());
    assert!(!r.is_skip());
}

/// Multi-Skip stream: `outcome()` returns the FIRST Skip's payload
/// (mirrors `Outcome::merge`'s left-wins payload-tie semantic). Pins
/// the .next() iterator in the all-Skip branch — a regression to
/// .last() would silently change which reason surfaces in the
/// terminal verdict.
#[test]
fn assert_result_outcome_multi_skip_returns_first_payload() {
    let mut r = AssertResult::pass();
    r.record_skip("first");
    r.record_skip("second");
    r.record_skip("third");
    let Outcome::Skip(d) = r.outcome() else {
        panic!("expected Skip, got {:?}", r.outcome());
    };
    assert!(
        d.message.contains("first"),
        "first-Skip-wins; got: {}",
        d.message
    );
}

/// `Outcome::as_ref` projection preserves the discriminant +
/// borrows the payload in place. Pins the no-clone contract that
/// the verdict-read fast path ([`AssertResult::outcome_ref`])
/// relies on. (Borrow-in-place is enforced by the type system —
/// `OutcomeRef::Fail(&'a AssertDetail)` cannot construct without
/// borrowing from the source.)
#[test]
fn outcome_as_ref_preserves_discriminant_and_payload() {
    assert!(matches!(Outcome::Pass.as_ref(), OutcomeRef::Pass));
    let fail = Outcome::Fail(AssertDetail::new(DetailKind::Starved, "boom"));
    let OutcomeRef::Fail(d) = fail.as_ref() else {
        panic!("Fail as_ref should be Fail variant");
    };
    assert_eq!(d.kind, DetailKind::Starved);
    assert!(d.message.contains("boom"));
    let skip = Outcome::Skip(AssertDetail::new(DetailKind::Skip, "missing"));
    let OutcomeRef::Skip(d) = skip.as_ref() else {
        panic!("Skip as_ref should be Skip variant");
    };
    assert_eq!(d.kind, DetailKind::Skip);
    assert!(d.message.contains("missing"));
    let inconc =
        Outcome::Inconclusive(AssertDetail::new(DetailKind::Migration, "zero denominator"));
    let OutcomeRef::Inconclusive(d) = inconc.as_ref() else {
        panic!("Inconclusive as_ref should be Inconclusive variant");
    };
    assert_eq!(d.kind, DetailKind::Migration);
    assert!(d.message.contains("zero denominator"));
}

/// `AssertResult::outcome_ref` matches `outcome()` shape across
/// every branch (Pass identity / non-empty all-Skip / mixed-Pass-
/// plus-Skip / Fail wins) while borrowing the payload instead of
/// cloning. Pins the borrowed-fast-path semantic against the
/// owned accessor so a regression that drifted one but not the
/// other trips here.
#[test]
fn assert_result_outcome_ref_matches_owned_outcome_shape() {
    // Pass identity: empty stream.
    assert!(matches!(
        AssertResult::pass().outcome_ref(),
        OutcomeRef::Pass
    ));
    // Non-empty all-Skip → Skip with first payload.
    let mut all_skip = AssertResult::pass();
    all_skip.record_skip("only-skip");
    let OutcomeRef::Skip(d) = all_skip.outcome_ref() else {
        panic!("all-Skip stream should yield Skip");
    };
    assert!(d.message.contains("only-skip"));
    // Mixed Pass + Skip → Pass (Pass demotes Skip per outcome()
    // inner-else branch).
    let mut mixed = AssertResult::pass();
    mixed.record_pass();
    mixed.record_skip("demoted");
    assert!(matches!(mixed.outcome_ref(), OutcomeRef::Pass));
    // Any Fail → Fail with first Fail's payload.
    let mut fail = AssertResult::pass();
    fail.record_fail(AssertDetail::new(DetailKind::Stuck, "first-fail"));
    fail.record_fail(AssertDetail::new(DetailKind::Starved, "second-fail"));
    let OutcomeRef::Fail(d) = fail.outcome_ref() else {
        panic!("any-Fail stream should yield Fail");
    };
    assert_eq!(d.kind, DetailKind::Stuck);
    assert!(d.message.contains("first-fail"));
    // Non-empty all-Inconclusive → Inconclusive with first payload
    // (mirrors the all-Skip branch and the `Outcome::merge` LEFT-
    // wins payload-tie semantic).
    let mut all_inconc = AssertResult::pass();
    all_inconc.record_inconclusive(AssertDetail::new(DetailKind::Migration, "first-inconc"));
    all_inconc.record_inconclusive(AssertDetail::new(DetailKind::Benchmark, "second-inconc"));
    let OutcomeRef::Inconclusive(d) = all_inconc.outcome_ref() else {
        panic!("all-Inconclusive stream should yield Inconclusive");
    };
    assert_eq!(d.kind, DetailKind::Migration);
    assert!(d.message.contains("first-inconc"));
    // Fail dominates Inconclusive: a Fail-plus-Inconclusive stream
    // yields Fail (per `Fail > Inconclusive > Pass > Skip`).
    let mut fail_over_inconc = AssertResult::pass();
    fail_over_inconc
        .record_inconclusive(AssertDetail::new(DetailKind::Migration, "denominator-zero"));
    fail_over_inconc.record_fail(AssertDetail::new(DetailKind::Stuck, "real-fail"));
    let OutcomeRef::Fail(d) = fail_over_inconc.outcome_ref() else {
        panic!("Fail+Inconclusive stream should yield Fail");
    };
    assert_eq!(d.kind, DetailKind::Stuck);
    assert!(d.message.contains("real-fail"));
    // Inconclusive dominates Skip: an Inconclusive-plus-Skip stream
    // (no Fail, no Pass) yields Inconclusive (per
    // `Fail > Inconclusive > Pass > Skip`). LEFT-wins payload-tie
    // semantics carry the first Inconclusive's detail through, even
    // though the Skip arrived after.
    let mut inconc_over_skip = AssertResult::pass();
    inconc_over_skip.record_inconclusive(AssertDetail::new(DetailKind::Benchmark, "left-inconc"));
    inconc_over_skip.record_skip("right-skip");
    let OutcomeRef::Inconclusive(d) = inconc_over_skip.outcome_ref() else {
        panic!(
            "Inconclusive+Skip stream should yield Inconclusive (Inconclusive > Skip): got {:?}",
            inconc_over_skip.outcome_ref()
        );
    };
    assert_eq!(d.kind, DetailKind::Benchmark);
    assert!(d.message.contains("left-inconc"));
    // Reverse order (Skip first, Inconclusive second) must yield the
    // same verdict — merge is commutative on lattice precedence even
    // though LEFT-wins on payload ties. With distinct ranks the rank
    // alone decides the variant, and the Inconclusive payload wins
    // because it is the only Inconclusive in the stream.
    let mut skip_then_inconc = AssertResult::pass();
    skip_then_inconc.record_skip("left-skip");
    skip_then_inconc.record_inconclusive(AssertDetail::new(DetailKind::Benchmark, "right-inconc"));
    let OutcomeRef::Inconclusive(d) = skip_then_inconc.outcome_ref() else {
        panic!(
            "Skip+Inconclusive stream should still yield Inconclusive: got {:?}",
            skip_then_inconc.outcome_ref()
        );
    };
    assert_eq!(d.kind, DetailKind::Benchmark);
    assert!(d.message.contains("right-inconc"));
}

/// Mutator semantics: repeated `record_fail` calls append distinct
/// Fail outcomes onto the vec; `outcome()` folds them and the
/// result is Fail with the LEFT operand's payload winning per
/// `Outcome::merge`'s payload-tie semantics.
#[test]
fn assert_result_record_fail_appends_and_folds_fail() {
    let mut r = AssertResult::pass();
    assert!(r.is_pass(), "fresh AssertResult::pass is_pass");
    r.record_fail(AssertDetail::new(DetailKind::Starved, "first"));
    r.record_fail(AssertDetail::new(DetailKind::Stuck, "second"));
    assert_eq!(r.outcomes.len(), 2);
    assert!(r.is_fail());
    let Outcome::Fail(d) = r.outcome() else {
        panic!("expected Outcome::Fail, got {:?}", r.outcome());
    };
    // LEFT-wins on Fail+Fail ties: first record_fail's detail wins.
    assert_eq!(d.kind, DetailKind::Starved);
    assert!(d.message.contains("first"));
    // Iteration helper surfaces both fails.
    let collected: Vec<&AssertDetail> = r.failure_details().collect();
    assert_eq!(collected.len(), 2);
}

/// `record_skip` appends a Skip outcome carrying the reason; mixed
/// Skip + Pass = Pass (Pass dominates Skip in the merge fold).
#[test]
fn assert_result_record_skip_then_pass_marker_yields_pass() {
    let mut r = AssertResult::pass();
    r.record_skip("topology mismatch");
    assert!(r.is_skip(), "skip-only stream is is_skip");
    r.record_pass();
    // Skip + Pass = Pass (the Pass entry beats the Skip per merge precedence).
    assert!(r.is_pass(), "Skip + Pass folds to Pass");
    assert!(!r.is_skip(), "is_skip requires all-Skip + non-empty");
}

/// `is_skip()` returns FALSE on empty outcomes (Pass identity, not
/// vacuous skip). Pins the "empty = Pass" convention so a future
/// refactor flipping is_skip to vacuous-all-skip semantics trips
/// loudly.
#[test]
fn assert_result_empty_outcomes_is_pass_not_skip() {
    let r = AssertResult::pass();
    assert!(r.outcomes.is_empty());
    assert!(r.is_pass());
    assert!(!r.is_skip());
    assert!(!r.is_fail());
}

/// `record_outcome` escape hatch pushes a pre-folded [`Outcome`]
/// onto the stream. Pins the surface so the dead-code lint doesn't
/// strip it, and verifies it composes with the variant-specific
/// mutators (record_outcome of a Fail is observable via is_fail()
/// and failure_details() identically to a record_fail call).
#[test]
fn assert_result_record_outcome_pushes_and_observable() {
    let mut r = AssertResult::pass();
    let d = AssertDetail::new(DetailKind::Other, "external verdict");
    r.record_outcome(Outcome::Fail(d.clone()));
    assert_eq!(r.outcomes.len(), 1);
    assert!(r.is_fail());
    let collected: Vec<&AssertDetail> = r.failure_details().collect();
    assert_eq!(collected.len(), 1);
    assert!(collected[0].message.contains("external verdict"));
    // Composes with record_outcome of a Skip: Fail still dominates.
    r.record_outcome(Outcome::Skip(AssertDetail::new(DetailKind::Skip, "stop")));
    assert_eq!(r.outcomes.len(), 2);
    assert!(r.is_fail(), "Fail dominates the merged outcome");
    assert_eq!(r.skip_details().count(), 1);
}

/// `Outcome` serde uses the externally-tagged default (no
/// `#[serde(tag, content)]`). The adjacently-tagged style was
/// dropped because postcard — the wire format for the AssertResult
/// TLV in [`crate::test_support::output::parse_assert_result_from_drain`]
/// and [`crate::test_support::test_helpers::assert_result_tlv_entry`]
/// — is not self-describing and cannot decode adjacently-tagged
/// enums. Pin both the JSON shape and the postcard roundtrip so a
/// refactor that re-adds `#[serde(tag, content)]` trips loudly on
/// the wire format that consumers actually depend on.
#[test]
fn outcome_serde_externally_tagged_roundtrips_via_json_and_postcard() {
    let d = AssertDetail::new(DetailKind::Other, "msg");

    // JSON shape: unit variant serializes as the bare variant
    // name; data variants serialize as `{"<Variant>": {...}}`.
    let pass_json = serde_json::to_string(&Outcome::Pass).unwrap();
    assert_eq!(
        pass_json, "\"Pass\"",
        "Pass must serialize as bare variant name"
    );
    let fail_json = serde_json::to_string(&Outcome::Fail(d.clone())).unwrap();
    assert!(
        fail_json.starts_with("{\"Fail\":"),
        "Fail must serialize as externally-tagged object: {fail_json}"
    );
    // Absence-check for the dropped adjacently-tagged shape: the
    // outer object's first key must be the variant name, not the
    // prior `"kind"` discriminator, and there must be no outer
    // `"data"` field wrapping the payload. (The inner
    // `AssertDetail` still has a `kind: DetailKind` field — that's
    // unrelated to Outcome's tagging.)
    assert!(
        !fail_json.starts_with("{\"kind\":"),
        "Fail outer key must be the variant name, not the dropped \"kind\" tag: {fail_json}"
    );
    assert!(
        !fail_json.contains("\"data\":"),
        "Fail must not carry the dropped \"data\" wrapper: {fail_json}"
    );

    // JSON roundtrip.
    let recovered: Outcome = serde_json::from_str(&fail_json).unwrap();
    assert!(recovered.is_fail());

    // Inconclusive serializes with the same externally-tagged shape
    // — `{"Inconclusive":{...}}` — and roundtrips via JSON. Pin
    // both so a regression that drops Inconclusive from the wire
    // format or re-adds `#[serde(tag, content)]` trips loudly.
    let inconc_detail = AssertDetail::new(DetailKind::Migration, "zero denom");
    let inconc_json = serde_json::to_string(&Outcome::Inconclusive(inconc_detail.clone())).unwrap();
    assert!(
        inconc_json.starts_with("{\"Inconclusive\":"),
        "Inconclusive must serialize as externally-tagged object: {inconc_json}"
    );
    assert!(
        !inconc_json.contains("\"data\":"),
        "Inconclusive must not carry the dropped \"data\" wrapper: {inconc_json}"
    );
    let inconc_recovered: Outcome = serde_json::from_str(&inconc_json).unwrap();
    assert!(inconc_recovered.is_inconclusive());

    // Postcard roundtrip — the wire format that the TLV path uses.
    // A regression that re-adds `#[serde(tag, content)]` would
    // silently break this and surface only at runtime as
    // ERR_NO_TEST_FUNCTION_OUTPUT.
    let pc_pass = postcard::to_stdvec(&Outcome::Pass).unwrap();
    let pc_pass_recovered: Outcome = postcard::from_bytes(&pc_pass).unwrap();
    assert!(pc_pass_recovered.is_pass());
    let pc_fail = postcard::to_stdvec(&Outcome::Fail(d)).unwrap();
    let pc_fail_recovered: Outcome = postcard::from_bytes(&pc_fail).unwrap();
    assert!(pc_fail_recovered.is_fail());
    let pc_inconc = postcard::to_stdvec(&Outcome::Inconclusive(inconc_detail)).unwrap();
    let pc_inconc_recovered: Outcome = postcard::from_bytes(&pc_inconc).unwrap();
    assert!(pc_inconc_recovered.is_inconclusive());
}

/// Wire-format byte-sentinel: postcard encodes `Outcome` by
/// **variant index** (Pass=0, Skip=1, Inconclusive=2, Fail=3), and
/// that index is the FIRST byte of the encoded payload (varint,
/// single byte for indices < 128). A refactor that reorders,
/// removes, or inserts a variant ahead of one of these four would
/// shift the leading byte and silently reinterpret guest payloads
/// on the host — a Pass byte becoming Skip, for example, with no
/// decode error.
///
/// Pin the leading byte for every variant so the silent-shift
/// regression trips at test time rather than at production
/// runtime. The Outcome enum doc explicitly documents this
/// stability contract ("Wire-format stability (postcard variant
/// index)" section) — this test is the enforcement for that
/// contract.
#[test]
fn outcome_postcard_variant_index_byte_sentinel() {
    let d = AssertDetail::new(DetailKind::Other, "x");

    let pc_pass = postcard::to_stdvec(&Outcome::Pass).unwrap();
    assert_eq!(
        pc_pass.first().copied(),
        Some(0u8),
        "Outcome::Pass MUST encode with leading variant-index byte 0 (got bytes {pc_pass:?}); \
         a variant reorder would silently corrupt the TLV wire path",
    );

    let pc_skip = postcard::to_stdvec(&Outcome::Skip(d.clone())).unwrap();
    assert_eq!(
        pc_skip.first().copied(),
        Some(1u8),
        "Outcome::Skip MUST encode with leading variant-index byte 1 (got bytes {pc_skip:?})",
    );

    let pc_inconc = postcard::to_stdvec(&Outcome::Inconclusive(d.clone())).unwrap();
    assert_eq!(
        pc_inconc.first().copied(),
        Some(2u8),
        "Outcome::Inconclusive MUST encode with leading variant-index byte 2 \
         (got bytes {pc_inconc:?})",
    );

    let pc_fail = postcard::to_stdvec(&Outcome::Fail(d)).unwrap();
    assert_eq!(
        pc_fail.first().copied(),
        Some(3u8),
        "Outcome::Fail MUST encode with leading variant-index byte 3 (got bytes {pc_fail:?})",
    );
}

/// `expect_scx_bpf_error_contains` and `expect_scx_bpf_error_matches`
/// are `#[serde(skip)]` because `Option<&'static str>` cannot
/// round-trip through a borrowed deserializer (no source-string
/// lifetime to bind to). Pin both invariants:
///  1. Serialize omits the two reproducer fields from JSON output
///     entirely (no key, no null).
///  2. Deserialize of a previously-serialized Assert that HAD those
///     fields populated produces `None` for both — the skipped
///     fields default per `Option::default() == None`.
///
/// The sidecar use case for `Assert` serde is "threshold config
/// roundtrip for stats comparison." Reproducer matcher strings are
/// part of the test definition itself (not per-run data), so
/// skipping them keeps the wire format clean.
///
/// Switching the field type to `Option<Cow<'static, str>>` (which
/// can deserialize) was considered but rejected: `Cow` is not
/// `Copy`, which cascade-breaks `declare_scheduler!`'s const-fn
/// `Scheduler::assert` macro path used by tests that build `Assert`
/// values in `const` / `static` initializers. Keeping
/// `Option<&'static str>` + `#[serde(skip)]` preserves the const-
/// construction surface; the reproducer matcher strings are test-
/// author static literals carried in the test definition rather than
/// runtime data the sidecar needs to round-trip. A future
/// decomposition into a `ReproducerMatchers` sub-config may revisit
/// this if a use case (e.g. sidecar-loaded test definitions) needs
/// the strings to survive serialization end-to-end.
#[test]
fn assert_serde_skips_reproducer_string_fields() {
    let a = Assert::NO_OVERRIDES
        .expect_scx_bpf_error_contains("apply_cell_config")
        .expect_scx_bpf_error_matches(r"\bEINVAL\b");
    let json = serde_json::to_string(&a).unwrap();
    assert!(
        !json.contains("expect_scx_bpf_error_contains"),
        "expect_scx_bpf_error_contains must NOT appear in serialized JSON; got: {json}"
    );
    assert!(
        !json.contains("expect_scx_bpf_error_matches"),
        "expect_scx_bpf_error_matches must NOT appear in serialized JSON; got: {json}"
    );
    let b: Assert = serde_json::from_str(&json).unwrap();
    assert_eq!(
        b.expect_scx_bpf_error_contains, None,
        "deserialized reproducer-contains field must default to None"
    );
    assert_eq!(
        b.expect_scx_bpf_error_matches, None,
        "deserialized reproducer-matches field must default to None"
    );
}