1#![forbid(unsafe_code)]
2
3use std::time::Duration;
53
54const C_MIN: Duration = Duration::from_micros(1);
56
57const DEFAULT_GAMMA: f64 = 0.3;
59
60#[derive(Debug, Clone)]
62pub struct PipelineConfig {
63 pub prior_alpha: f64,
66
67 pub prior_beta: f64,
70
71 pub gamma: f64,
74
75 pub c_min: Duration,
77}
78
79impl Default for PipelineConfig {
80 fn default() -> Self {
81 Self {
82 prior_alpha: 1.0,
83 prior_beta: 1.0,
84 gamma: DEFAULT_GAMMA,
85 c_min: C_MIN,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct ValidatorStats {
93 pub id: usize,
95 pub name: String,
97 pub alpha: f64,
99 pub beta: f64,
101 pub cost_ema: Duration,
103 pub observations: u64,
105 pub failures: u64,
107}
108
109impl ValidatorStats {
110 #[inline]
112 pub fn failure_prob(&self) -> f64 {
113 let sum = self.alpha + self.beta;
114 if sum > 0.0 {
115 self.alpha / sum
116 } else {
117 0.5
119 }
120 }
121
122 #[inline]
124 pub fn score(&self, c_min: Duration) -> f64 {
125 let c = self.cost_ema.max(c_min).as_secs_f64();
126 self.failure_prob() / c
127 }
128
129 #[inline]
131 pub fn variance(&self) -> f64 {
132 let sum = self.alpha + self.beta;
133 if sum > 0.0 {
134 (self.alpha * self.beta) / (sum * sum * (sum + 1.0))
135 } else {
136 1.0 / 12.0
138 }
139 }
140
141 #[inline]
143 pub fn confidence_width(&self) -> f64 {
144 2.0 * 1.96 * self.variance().sqrt()
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct LedgerEntry {
151 pub id: usize,
153 pub name: String,
155 pub p: f64,
157 pub c: Duration,
159 pub score: f64,
161 pub rank: usize,
163}
164
165#[derive(Debug, Clone)]
167pub struct ValidationOutcome {
168 pub id: usize,
170 pub passed: bool,
172 pub duration: Duration,
174}
175
176#[derive(Debug, Clone)]
178pub struct PipelineResult {
179 pub all_passed: bool,
181 pub outcomes: Vec<ValidationOutcome>,
183 pub total_cost: Duration,
185 pub ordering: Vec<usize>,
187 pub ledger: Vec<LedgerEntry>,
189 pub skipped: usize,
191}
192
193#[derive(Debug, Clone)]
195pub struct ValidationPipeline {
196 config: PipelineConfig,
197 validators: Vec<ValidatorStats>,
198 total_runs: u64,
200}
201
202impl ValidationPipeline {
203 pub fn new() -> Self {
205 Self {
206 config: PipelineConfig::default(),
207 validators: Vec::new(),
208 total_runs: 0,
209 }
210 }
211
212 pub fn with_config(config: PipelineConfig) -> Self {
214 Self {
215 config,
216 validators: Vec::new(),
217 total_runs: 0,
218 }
219 }
220
221 pub fn register(&mut self, name: impl Into<String>, initial_cost: Duration) -> usize {
224 let id = self.validators.len();
225 self.validators.push(ValidatorStats {
226 id,
227 name: name.into(),
228 alpha: self.config.prior_alpha,
229 beta: self.config.prior_beta,
230 cost_ema: initial_cost.max(self.config.c_min),
231 observations: 0,
232 failures: 0,
233 });
234 id
235 }
236
237 pub fn compute_ordering(&self) -> (Vec<usize>, Vec<LedgerEntry>) {
240 if self.validators.is_empty() {
241 return (Vec::new(), Vec::new());
242 }
243
244 let mut scored: Vec<(usize, f64)> = self
246 .validators
247 .iter()
248 .map(|v| (v.id, v.score(self.config.c_min)))
249 .collect();
250
251 scored.sort_by(|a, b| {
253 b.1.partial_cmp(&a.1)
254 .unwrap_or(std::cmp::Ordering::Equal)
255 .then_with(|| a.0.cmp(&b.0))
256 });
257
258 let ordering: Vec<usize> = scored.iter().map(|(id, _)| *id).collect();
259
260 let ledger: Vec<LedgerEntry> = scored
261 .iter()
262 .enumerate()
263 .map(|(rank, (id, score))| {
264 let v = &self.validators[*id];
265 LedgerEntry {
266 id: *id,
267 name: v.name.clone(),
268 p: v.failure_prob(),
269 c: v.cost_ema,
270 score: *score,
271 rank,
272 }
273 })
274 .collect();
275
276 (ordering, ledger)
277 }
278
279 pub fn expected_cost(&self, ordering: &[usize]) -> f64 {
285 let mut survival = 1.0; let mut total = 0.0;
287
288 for &id in ordering {
289 let v = &self.validators[id];
290 let c = v.cost_ema.max(self.config.c_min).as_secs_f64();
291 total += c * survival;
292 survival *= 1.0 - v.failure_prob();
293 }
294
295 total
296 }
297
298 pub fn update(&mut self, outcome: &ValidationOutcome) {
300 if let Some(v) = self.validators.get_mut(outcome.id) {
301 v.observations += 1;
302 if outcome.passed {
303 v.beta += 1.0;
304 } else {
305 v.alpha += 1.0;
306 v.failures += 1;
307 }
308 let gamma = self.config.gamma;
310 let old_ns = v.cost_ema.as_nanos() as f64;
311 let new_ns = outcome.duration.as_nanos() as f64;
312 let updated_ns = gamma * new_ns + (1.0 - gamma) * old_ns;
313 v.cost_ema =
314 Duration::from_nanos(updated_ns.max(self.config.c_min.as_nanos() as f64) as u64);
315 }
316 }
317
318 pub fn update_batch(&mut self, result: &PipelineResult) {
320 self.total_runs += 1;
321 for outcome in &result.outcomes {
322 self.update(outcome);
323 }
324 }
325
326 pub fn run<F>(&self, mut validate: F) -> PipelineResult
331 where
332 F: FnMut(usize) -> (bool, Duration),
333 {
334 let (ordering, ledger) = self.compute_ordering();
335 let total_validators = ordering.len();
336 let mut outcomes = Vec::with_capacity(total_validators);
337 let mut total_cost = Duration::ZERO;
338 let mut all_passed = true;
339
340 for &id in &ordering {
341 let (passed, duration) = validate(id);
342 total_cost += duration;
343 outcomes.push(ValidationOutcome {
344 id,
345 passed,
346 duration,
347 });
348 if !passed {
349 all_passed = false;
350 break; }
352 }
353
354 let skipped = total_validators - outcomes.len();
355
356 PipelineResult {
357 all_passed,
358 outcomes,
359 total_cost,
360 ordering,
361 ledger,
362 skipped,
363 }
364 }
365
366 pub fn stats(&self, id: usize) -> Option<&ValidatorStats> {
368 self.validators.get(id)
369 }
370
371 pub fn all_stats(&self) -> &[ValidatorStats] {
373 &self.validators
374 }
375
376 pub fn total_runs(&self) -> u64 {
378 self.total_runs
379 }
380
381 pub fn validator_count(&self) -> usize {
383 self.validators.len()
384 }
385
386 pub fn summary(&self) -> PipelineSummary {
388 let (ordering, ledger) = self.compute_ordering();
389 let expected = self.expected_cost(&ordering);
390 let natural: Vec<usize> = (0..self.validators.len()).collect();
392 let natural_cost = self.expected_cost(&natural);
393 let improvement = if natural_cost > 0.0 {
394 1.0 - expected / natural_cost
395 } else {
396 0.0
397 };
398
399 PipelineSummary {
400 validator_count: self.validators.len(),
401 total_runs: self.total_runs,
402 optimal_ordering: ordering,
403 expected_cost_secs: expected,
404 natural_cost_secs: natural_cost,
405 improvement_fraction: improvement,
406 ledger,
407 }
408 }
409}
410
411impl Default for ValidationPipeline {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417#[derive(Debug, Clone)]
419pub struct PipelineSummary {
420 pub validator_count: usize,
421 pub total_runs: u64,
422 pub optimal_ordering: Vec<usize>,
423 pub expected_cost_secs: f64,
424 pub natural_cost_secs: f64,
425 pub improvement_fraction: f64,
426 pub ledger: Vec<LedgerEntry>,
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
436 fn empty_pipeline_returns_success() {
437 let pipeline = ValidationPipeline::new();
438 let result = pipeline.run(|_| unreachable!());
439 assert!(result.all_passed);
440 assert!(result.outcomes.is_empty());
441 assert_eq!(result.total_cost, Duration::ZERO);
442 assert_eq!(result.skipped, 0);
443 }
444
445 #[test]
446 fn single_validator_pass() {
447 let mut pipeline = ValidationPipeline::new();
448 pipeline.register("check_a", Duration::from_millis(10));
449 let result = pipeline.run(|_| (true, Duration::from_millis(8)));
450 assert!(result.all_passed);
451 assert_eq!(result.outcomes.len(), 1);
452 assert_eq!(result.skipped, 0);
453 }
454
455 #[test]
456 fn single_validator_fail() {
457 let mut pipeline = ValidationPipeline::new();
458 pipeline.register("check_a", Duration::from_millis(10));
459 let result = pipeline.run(|_| (false, Duration::from_millis(5)));
460 assert!(!result.all_passed);
461 assert_eq!(result.outcomes.len(), 1);
462 assert!(!result.outcomes[0].passed);
463 }
464
465 #[test]
466 fn early_exit_on_failure() {
467 let mut pipeline = ValidationPipeline::new();
468 pipeline.register("cheap_fail", Duration::from_millis(1));
469 pipeline.register("expensive", Duration::from_millis(100));
470 pipeline.register("also_expensive", Duration::from_millis(50));
471
472 for _ in 0..10 {
474 pipeline.update(&ValidationOutcome {
475 id: 0,
476 passed: false,
477 duration: Duration::from_millis(1),
478 });
479 }
480
481 let mut ran = Vec::new();
482 let result = pipeline.run(|id| {
483 ran.push(id);
484 if id == 0 {
485 (false, Duration::from_millis(1))
486 } else {
487 (true, Duration::from_millis(50))
488 }
489 });
490
491 assert!(!result.all_passed);
492 assert_eq!(ran.len(), 1);
494 assert_eq!(ran[0], 0);
495 assert_eq!(result.skipped, 2);
496 }
497
498 #[test]
499 fn unit_expected_cost_formula() {
500 let mut pipeline = ValidationPipeline::new();
504 let a = pipeline.register("A", Duration::from_millis(10));
505 let b = pipeline.register("B", Duration::from_millis(100));
506
507 for _ in 0..7 {
509 pipeline.update(&ValidationOutcome {
510 id: a,
511 passed: false,
512 duration: Duration::from_millis(10),
513 });
514 }
515 for _ in 0..1 {
516 pipeline.update(&ValidationOutcome {
517 id: a,
518 passed: true,
519 duration: Duration::from_millis(10),
520 });
521 }
522 for _ in 0..1 {
526 pipeline.update(&ValidationOutcome {
527 id: b,
528 passed: false,
529 duration: Duration::from_millis(100),
530 });
531 }
532 for _ in 0..7 {
533 pipeline.update(&ValidationOutcome {
534 id: b,
535 passed: true,
536 duration: Duration::from_millis(100),
537 });
538 }
539 let p_a = pipeline.stats(a).unwrap().failure_prob();
542 let p_b = pipeline.stats(b).unwrap().failure_prob();
543 assert!((p_a - 0.8).abs() < 1e-10);
544 assert!((p_b - 0.2).abs() < 1e-10);
545
546 let cost_ab = pipeline.expected_cost(&[a, b]);
548 let c_a = pipeline.stats(a).unwrap().cost_ema.as_secs_f64();
549 let c_b = pipeline.stats(b).unwrap().cost_ema.as_secs_f64();
550 let expected_ab = c_a + (1.0 - p_a) * c_b;
551 assert!((cost_ab - expected_ab).abs() < 1e-9);
552
553 let cost_ba = pipeline.expected_cost(&[b, a]);
555 let expected_ba = c_b + (1.0 - p_b) * c_a;
556 assert!((cost_ba - expected_ba).abs() < 1e-9);
557
558 assert!(cost_ab < cost_ba);
560 }
561
562 #[test]
563 fn zero_prior_defaults_to_uniform() {
564 let config = PipelineConfig {
565 prior_alpha: 0.0,
566 prior_beta: 0.0,
567 ..PipelineConfig::default()
568 };
569 let mut pipeline = ValidationPipeline::with_config(config);
570 pipeline.register("A", Duration::from_millis(10));
571 pipeline.register("B", Duration::from_millis(20));
572
573 let (ordering, ledger) = pipeline.compute_ordering();
574 assert_eq!(ordering.len(), 2);
575 assert_eq!(ledger.len(), 2);
576 for entry in ledger {
577 assert!(entry.p.is_finite());
578 assert!(entry.score.is_finite());
579 assert!((entry.p - 0.5).abs() < 1e-9);
580 }
581 }
582
583 #[test]
584 fn unit_posterior_update() {
585 let mut pipeline = ValidationPipeline::new();
586 let id = pipeline.register("v", Duration::from_millis(5));
587
588 assert!((pipeline.stats(id).unwrap().failure_prob() - 0.5).abs() < 1e-10);
590
591 for _ in 0..3 {
593 pipeline.update(&ValidationOutcome {
594 id,
595 passed: false,
596 duration: Duration::from_millis(5),
597 });
598 }
599 assert!((pipeline.stats(id).unwrap().failure_prob() - 0.8).abs() < 1e-10);
601
602 for _ in 0..4 {
604 pipeline.update(&ValidationOutcome {
605 id,
606 passed: true,
607 duration: Duration::from_millis(5),
608 });
609 }
610 assert!((pipeline.stats(id).unwrap().failure_prob() - 4.0 / 9.0).abs() < 1e-10);
612 }
613
614 #[test]
615 fn optimal_ordering_sorts_by_score() {
616 let mut pipeline = ValidationPipeline::new();
617 let a = pipeline.register("A_cheap_reliable", Duration::from_millis(1));
619 let b = pipeline.register("B_expensive_flaky", Duration::from_millis(100));
621 let c = pipeline.register("C_cheap_flaky", Duration::from_millis(1));
623
624 for _ in 0..8 {
626 pipeline.update(&ValidationOutcome {
627 id: b,
628 passed: false,
629 duration: Duration::from_millis(100),
630 });
631 }
632 for _ in 0..8 {
634 pipeline.update(&ValidationOutcome {
635 id: c,
636 passed: false,
637 duration: Duration::from_millis(1),
638 });
639 }
640 for _ in 0..8 {
642 pipeline.update(&ValidationOutcome {
643 id: a,
644 passed: true,
645 duration: Duration::from_millis(1),
646 });
647 }
648
649 let (ordering, _ledger) = pipeline.compute_ordering();
650 assert_eq!(ordering[0], c);
652 assert_eq!(ordering[1], a);
654 assert_eq!(ordering[2], b);
656 }
657
658 #[test]
659 fn cost_ema_updates() {
660 let mut pipeline = ValidationPipeline::with_config(PipelineConfig {
661 gamma: 0.5,
662 ..Default::default()
663 });
664 let id = pipeline.register("v", Duration::from_millis(10));
665
666 pipeline.update(&ValidationOutcome {
668 id,
669 passed: true,
670 duration: Duration::from_millis(20),
671 });
672 let cost = pipeline.stats(id).unwrap().cost_ema;
674 assert!((cost.as_millis() as i64 - 15).abs() <= 1);
675
676 pipeline.update(&ValidationOutcome {
678 id,
679 passed: true,
680 duration: Duration::from_millis(30),
681 });
682 let cost = pipeline.stats(id).unwrap().cost_ema;
684 assert!((cost.as_millis() as i64 - 22).abs() <= 1);
685 }
686
687 #[test]
688 fn cost_floor_prevents_zero() {
689 let mut pipeline = ValidationPipeline::new();
690 let id = pipeline.register("v", Duration::ZERO);
691 let cost = pipeline.stats(id).unwrap().cost_ema;
693 assert!(cost >= C_MIN);
694 }
695
696 #[test]
697 fn ledger_records_all_validators() {
698 let mut pipeline = ValidationPipeline::new();
699 pipeline.register("a", Duration::from_millis(5));
700 pipeline.register("b", Duration::from_millis(10));
701 pipeline.register("c", Duration::from_millis(15));
702
703 let (_, ledger) = pipeline.compute_ordering();
704 assert_eq!(ledger.len(), 3);
705
706 let mut ranks: Vec<usize> = ledger.iter().map(|e| e.rank).collect();
708 ranks.sort_unstable();
709 assert_eq!(ranks, vec![0, 1, 2]);
710 }
711
712 #[test]
713 fn deterministic_under_same_history() {
714 let run = || {
715 let mut p = ValidationPipeline::new();
716 p.register("x", Duration::from_millis(10));
717 p.register("y", Duration::from_millis(20));
718 p.register("z", Duration::from_millis(5));
719
720 let history = [
722 (0, false, 10),
723 (1, true, 20),
724 (2, false, 5),
725 (0, true, 12),
726 (1, false, 18),
727 (2, true, 6),
728 (0, false, 9),
729 (1, true, 22),
730 (2, false, 4),
731 ];
732 for (id, passed, ms) in history {
733 p.update(&ValidationOutcome {
734 id,
735 passed,
736 duration: Duration::from_millis(ms),
737 });
738 }
739
740 let (ordering, _) = p.compute_ordering();
741 let cost = p.expected_cost(&ordering);
742 (ordering, cost)
743 };
744
745 let (o1, c1) = run();
746 let (o2, c2) = run();
747 assert_eq!(o1, o2);
748 assert!((c1 - c2).abs() < 1e-15);
749 }
750
751 #[test]
752 fn summary_shows_improvement() {
753 let mut pipeline = ValidationPipeline::new();
754 pipeline.register("expensive_reliable", Duration::from_millis(100));
756 pipeline.register("cheap_flaky", Duration::from_millis(1));
757
758 for _ in 0..20 {
760 pipeline.update(&ValidationOutcome {
761 id: 1,
762 passed: false,
763 duration: Duration::from_millis(1),
764 });
765 }
766 for _ in 0..20 {
768 pipeline.update(&ValidationOutcome {
769 id: 0,
770 passed: true,
771 duration: Duration::from_millis(100),
772 });
773 }
774
775 let summary = pipeline.summary();
776 assert_eq!(summary.optimal_ordering[0], 1);
778 assert!(summary.improvement_fraction > 0.0);
780 }
781
782 #[test]
783 fn variance_decreases_with_observations() {
784 let mut pipeline = ValidationPipeline::new();
785 let id = pipeline.register("v", Duration::from_millis(5));
786
787 let var_0 = pipeline.stats(id).unwrap().variance();
788
789 for _ in 0..10 {
790 pipeline.update(&ValidationOutcome {
791 id,
792 passed: false,
793 duration: Duration::from_millis(5),
794 });
795 }
796 let var_10 = pipeline.stats(id).unwrap().variance();
797
798 for _ in 0..90 {
799 pipeline.update(&ValidationOutcome {
800 id,
801 passed: false,
802 duration: Duration::from_millis(5),
803 });
804 }
805 let var_100 = pipeline.stats(id).unwrap().variance();
806
807 assert!(var_10 < var_0);
809 assert!(var_100 < var_10);
810 }
811
812 #[test]
813 fn confidence_width_contracts() {
814 let mut pipeline = ValidationPipeline::new();
815 let id = pipeline.register("v", Duration::from_millis(5));
816
817 let w0 = pipeline.stats(id).unwrap().confidence_width();
818
819 for _ in 0..50 {
820 pipeline.update(&ValidationOutcome {
821 id,
822 passed: true,
823 duration: Duration::from_millis(5),
824 });
825 }
826 let w50 = pipeline.stats(id).unwrap().confidence_width();
827
828 assert!(w50 < w0, "CI should narrow: w0={w0}, w50={w50}");
829 }
830
831 #[test]
832 fn update_batch_increments_total_runs() {
833 let mut pipeline = ValidationPipeline::new();
834 pipeline.register("v", Duration::from_millis(5));
835 assert_eq!(pipeline.total_runs(), 0);
836
837 let result = PipelineResult {
838 all_passed: true,
839 outcomes: vec![ValidationOutcome {
840 id: 0,
841 passed: true,
842 duration: Duration::from_millis(4),
843 }],
844 total_cost: Duration::from_millis(4),
845 ordering: vec![0],
846 ledger: Vec::new(),
847 skipped: 0,
848 };
849 pipeline.update_batch(&result);
850 assert_eq!(pipeline.total_runs(), 1);
851 }
852
853 #[test]
856 fn expected_cost_matches_brute_force_n3() {
857 let mut pipeline = ValidationPipeline::new();
858 pipeline.register("a", Duration::from_millis(10));
859 pipeline.register("b", Duration::from_millis(20));
860 pipeline.register("c", Duration::from_millis(5));
861
862 for _ in 0..3 {
865 pipeline.update(&ValidationOutcome {
866 id: 0,
867 passed: false,
868 duration: Duration::from_millis(10),
869 });
870 }
871 pipeline.update(&ValidationOutcome {
873 id: 1,
874 passed: false,
875 duration: Duration::from_millis(20),
876 });
877 for _ in 0..3 {
878 pipeline.update(&ValidationOutcome {
879 id: 1,
880 passed: true,
881 duration: Duration::from_millis(20),
882 });
883 }
884 for _ in 0..2 {
886 pipeline.update(&ValidationOutcome {
887 id: 2,
888 passed: false,
889 duration: Duration::from_millis(5),
890 });
891 }
892 pipeline.update(&ValidationOutcome {
893 id: 2,
894 passed: true,
895 duration: Duration::from_millis(5),
896 });
897
898 let perms: &[&[usize]] = &[
900 &[0, 1, 2],
901 &[0, 2, 1],
902 &[1, 0, 2],
903 &[1, 2, 0],
904 &[2, 0, 1],
905 &[2, 1, 0],
906 ];
907 let mut best_cost = f64::MAX;
908 let mut best_perm = &[0usize, 1, 2][..];
909 for perm in perms {
910 let cost = pipeline.expected_cost(perm);
911 if cost < best_cost {
912 best_cost = cost;
913 best_perm = perm;
914 }
915 }
916
917 let (optimal, _) = pipeline.compute_ordering();
919 let optimal_cost = pipeline.expected_cost(&optimal);
920
921 assert!(
922 (optimal_cost - best_cost).abs() < 1e-12,
923 "optimal={optimal_cost}, brute_force={best_cost}, best_perm={best_perm:?}, our={optimal:?}"
924 );
925 }
926
927 #[test]
930 fn perf_ordering_overhead() {
931 let mut pipeline = ValidationPipeline::new();
932 for i in 0..100 {
934 pipeline.register(format!("v{i}"), Duration::from_micros(100 + i as u64 * 10));
935 }
936 for i in 0..100 {
938 for _ in 0..5 {
939 pipeline.update(&ValidationOutcome {
940 id: i,
941 passed: i % 3 != 0,
942 duration: Duration::from_micros(100 + i as u64 * 10),
943 });
944 }
945 }
946
947 let start = web_time::Instant::now();
948 for _ in 0..1000 {
949 let _ = pipeline.compute_ordering();
950 }
951 let elapsed = start.elapsed();
952 assert!(
954 elapsed < Duration::from_millis(100),
955 "ordering overhead too high: {elapsed:?} for 1000 iterations"
956 );
957 }
958
959 #[test]
960 fn pipeline_config_default_values() {
961 let config = PipelineConfig::default();
962 assert!((config.prior_alpha - 1.0).abs() < 1e-10);
963 assert!((config.prior_beta - 1.0).abs() < 1e-10);
964 assert!((config.gamma - DEFAULT_GAMMA).abs() < 1e-10);
965 assert_eq!(config.c_min, C_MIN);
966 }
967
968 #[test]
969 fn pipeline_default_impl() {
970 let p = ValidationPipeline::default();
971 assert_eq!(p.validator_count(), 0);
972 assert_eq!(p.total_runs(), 0);
973 }
974
975 #[test]
976 fn all_stats_returns_all_registered() {
977 let mut pipeline = ValidationPipeline::new();
978 pipeline.register("a", Duration::from_millis(5));
979 pipeline.register("b", Duration::from_millis(10));
980 pipeline.register("c", Duration::from_millis(15));
981 let stats = pipeline.all_stats();
982 assert_eq!(stats.len(), 3);
983 assert_eq!(stats[0].name, "a");
984 assert_eq!(stats[1].name, "b");
985 assert_eq!(stats[2].name, "c");
986 }
987
988 #[test]
989 fn stats_invalid_id_returns_none() {
990 let pipeline = ValidationPipeline::new();
991 assert!(pipeline.stats(0).is_none());
992 assert!(pipeline.stats(999).is_none());
993 }
994
995 #[test]
996 fn update_invalid_id_is_noop() {
997 let mut pipeline = ValidationPipeline::new();
998 pipeline.register("v", Duration::from_millis(5));
999 pipeline.update(&ValidationOutcome {
1000 id: 99,
1001 passed: false,
1002 duration: Duration::from_millis(5),
1003 });
1004 assert_eq!(pipeline.stats(0).unwrap().observations, 0);
1006 }
1007
1008 #[test]
1009 fn failure_prob_zero_sum_returns_half() {
1010 let config = PipelineConfig {
1011 prior_alpha: 0.0,
1012 prior_beta: 0.0,
1013 ..Default::default()
1014 };
1015 let mut pipeline = ValidationPipeline::with_config(config);
1016 let id = pipeline.register("v", Duration::from_millis(5));
1017 let p = pipeline.stats(id).unwrap().failure_prob();
1018 assert!((p - 0.5).abs() < 1e-10);
1019 }
1020
1021 #[test]
1022 fn variance_zero_sum_returns_uniform() {
1023 let config = PipelineConfig {
1024 prior_alpha: 0.0,
1025 prior_beta: 0.0,
1026 ..Default::default()
1027 };
1028 let mut pipeline = ValidationPipeline::with_config(config);
1029 let id = pipeline.register("v", Duration::from_millis(5));
1030 let var = pipeline.stats(id).unwrap().variance();
1031 assert!((var - 1.0 / 12.0).abs() < 1e-10);
1032 }
1033
1034 #[test]
1035 fn score_uses_cost_floor() {
1036 let mut pipeline = ValidationPipeline::new();
1037 let id = pipeline.register("v", Duration::ZERO);
1038 let score = pipeline.stats(id).unwrap().score(C_MIN);
1039 assert!(score.is_finite());
1040 assert!(score > 0.0);
1041 }
1042
1043 #[test]
1044 fn summary_empty_pipeline() {
1045 let pipeline = ValidationPipeline::new();
1046 let summary = pipeline.summary();
1047 assert_eq!(summary.validator_count, 0);
1048 assert_eq!(summary.total_runs, 0);
1049 assert!(summary.optimal_ordering.is_empty());
1050 assert!((summary.expected_cost_secs).abs() < 1e-10);
1051 assert!((summary.improvement_fraction).abs() < 1e-10);
1052 }
1053
1054 #[test]
1055 fn register_returns_sequential_ids() {
1056 let mut pipeline = ValidationPipeline::new();
1057 let id0 = pipeline.register("first", Duration::from_millis(1));
1058 let id1 = pipeline.register("second", Duration::from_millis(2));
1059 let id2 = pipeline.register("third", Duration::from_millis(3));
1060 assert_eq!(id0, 0);
1061 assert_eq!(id1, 1);
1062 assert_eq!(id2, 2);
1063 }
1064
1065 #[test]
1068 fn run_all_pass_multi_validator() {
1069 let mut pipeline = ValidationPipeline::new();
1070 pipeline.register("a", Duration::from_millis(5));
1071 pipeline.register("b", Duration::from_millis(10));
1072 pipeline.register("c", Duration::from_millis(15));
1073
1074 let result = pipeline.run(|_| (true, Duration::from_millis(7)));
1075 assert!(result.all_passed);
1076 assert_eq!(result.outcomes.len(), 3, "all validators should run");
1077 assert_eq!(result.skipped, 0);
1078 assert!(result.outcomes.iter().all(|o| o.passed));
1079 }
1080
1081 #[test]
1082 fn run_failure_at_second_position() {
1083 let mut pipeline = ValidationPipeline::new();
1084 pipeline.register("a", Duration::from_millis(5));
1085 pipeline.register("b", Duration::from_millis(10));
1086 pipeline.register("c", Duration::from_millis(15));
1087
1088 let result = pipeline.run(|id| {
1091 if id == 1 {
1092 (false, Duration::from_millis(10))
1093 } else {
1094 (true, Duration::from_millis(5))
1095 }
1096 });
1097 assert!(!result.all_passed);
1098 assert_eq!(result.outcomes.len(), 2);
1100 assert!(result.outcomes[0].passed);
1101 assert!(!result.outcomes[1].passed);
1102 assert_eq!(result.skipped, 1);
1103 }
1104
1105 #[test]
1106 fn ema_gamma_one_full_replacement() {
1107 let mut pipeline = ValidationPipeline::with_config(PipelineConfig {
1108 gamma: 1.0,
1109 ..Default::default()
1110 });
1111 let id = pipeline.register("v", Duration::from_millis(100));
1112
1113 pipeline.update(&ValidationOutcome {
1114 id,
1115 passed: true,
1116 duration: Duration::from_millis(50),
1117 });
1118 let cost = pipeline.stats(id).unwrap().cost_ema;
1120 assert_eq!(cost.as_millis(), 50);
1121 }
1122
1123 #[test]
1124 fn ema_gamma_near_zero_minimal_update() {
1125 let mut pipeline = ValidationPipeline::with_config(PipelineConfig {
1126 gamma: 0.01,
1127 ..Default::default()
1128 });
1129 let id = pipeline.register("v", Duration::from_millis(100));
1130
1131 pipeline.update(&ValidationOutcome {
1132 id,
1133 passed: true,
1134 duration: Duration::from_millis(200),
1135 });
1136 let cost = pipeline.stats(id).unwrap().cost_ema;
1138 assert!(
1139 (cost.as_millis() as i64 - 101).abs() <= 1,
1140 "cost should barely move: got {}ms",
1141 cost.as_millis()
1142 );
1143 }
1144
1145 #[test]
1146 fn cost_ema_floor_during_update() {
1147 let mut pipeline = ValidationPipeline::new();
1148 let id = pipeline.register("v", Duration::from_millis(10));
1149
1150 pipeline.update(&ValidationOutcome {
1152 id,
1153 passed: true,
1154 duration: Duration::ZERO,
1155 });
1156 let cost = pipeline.stats(id).unwrap().cost_ema;
1157 assert!(
1158 cost >= C_MIN,
1159 "cost should be floored to c_min, got {:?}",
1160 cost
1161 );
1162 }
1163
1164 #[test]
1165 fn ordering_tie_break_by_id() {
1166 let mut pipeline = ValidationPipeline::new();
1169 pipeline.register("second", Duration::from_millis(10));
1170 pipeline.register("first", Duration::from_millis(10));
1171
1172 let (ordering, _) = pipeline.compute_ordering();
1173 assert_eq!(
1174 ordering,
1175 vec![0, 1],
1176 "identical scores should tie-break by lower id first"
1177 );
1178 }
1179
1180 #[test]
1181 fn ordering_tie_break_three_way() {
1182 let mut pipeline = ValidationPipeline::new();
1183 pipeline.register("c", Duration::from_millis(5));
1184 pipeline.register("a", Duration::from_millis(5));
1185 pipeline.register("b", Duration::from_millis(5));
1186
1187 let (ordering, _) = pipeline.compute_ordering();
1189 assert_eq!(ordering, vec![0, 1, 2]);
1190 }
1191
1192 #[test]
1193 fn expected_cost_single_validator() {
1194 let mut pipeline = ValidationPipeline::new();
1195 pipeline.register("v", Duration::from_millis(10));
1196
1197 let cost = pipeline.expected_cost(&[0]);
1198 let c = pipeline.stats(0).unwrap().cost_ema.as_secs_f64();
1200 assert!((cost - c).abs() < 1e-12);
1201 }
1202
1203 #[test]
1204 fn expected_cost_empty_ordering() {
1205 let mut pipeline = ValidationPipeline::new();
1206 pipeline.register("v", Duration::from_millis(10));
1207
1208 let cost = pipeline.expected_cost(&[]);
1210 assert!((cost).abs() < 1e-15);
1211 }
1212
1213 #[test]
1214 fn summary_single_validator() {
1215 let mut pipeline = ValidationPipeline::new();
1216 pipeline.register("v", Duration::from_millis(10));
1217
1218 let summary = pipeline.summary();
1219 assert_eq!(summary.validator_count, 1);
1220 assert_eq!(summary.optimal_ordering, vec![0]);
1221 assert!(
1223 summary.improvement_fraction.abs() < 1e-10,
1224 "single validator can't improve: got {}",
1225 summary.improvement_fraction
1226 );
1227 }
1228
1229 #[test]
1230 fn summary_identical_validators_no_improvement() {
1231 let mut pipeline = ValidationPipeline::new();
1232 pipeline.register("a", Duration::from_millis(10));
1233 pipeline.register("b", Duration::from_millis(10));
1234
1235 let summary = pipeline.summary();
1237 assert!(
1238 summary.improvement_fraction.abs() < 1e-10,
1239 "identical validators should have zero improvement"
1240 );
1241 }
1242
1243 #[test]
1244 fn observations_and_failures_counters() {
1245 let mut pipeline = ValidationPipeline::new();
1246 let id = pipeline.register("v", Duration::from_millis(5));
1247
1248 assert_eq!(pipeline.stats(id).unwrap().observations, 0);
1249 assert_eq!(pipeline.stats(id).unwrap().failures, 0);
1250
1251 for _ in 0..3 {
1253 pipeline.update(&ValidationOutcome {
1254 id,
1255 passed: false,
1256 duration: Duration::from_millis(5),
1257 });
1258 }
1259 assert_eq!(pipeline.stats(id).unwrap().observations, 3);
1260 assert_eq!(pipeline.stats(id).unwrap().failures, 3);
1261
1262 for _ in 0..2 {
1264 pipeline.update(&ValidationOutcome {
1265 id,
1266 passed: true,
1267 duration: Duration::from_millis(5),
1268 });
1269 }
1270 assert_eq!(pipeline.stats(id).unwrap().observations, 5);
1271 assert_eq!(pipeline.stats(id).unwrap().failures, 3);
1272 }
1273
1274 #[test]
1275 fn update_batch_multiple_calls_increment_total_runs() {
1276 let mut pipeline = ValidationPipeline::new();
1277 pipeline.register("v", Duration::from_millis(5));
1278
1279 let result = PipelineResult {
1280 all_passed: true,
1281 outcomes: vec![ValidationOutcome {
1282 id: 0,
1283 passed: true,
1284 duration: Duration::from_millis(4),
1285 }],
1286 total_cost: Duration::from_millis(4),
1287 ordering: vec![0],
1288 ledger: Vec::new(),
1289 skipped: 0,
1290 };
1291
1292 pipeline.update_batch(&result);
1293 pipeline.update_batch(&result);
1294 pipeline.update_batch(&result);
1295 assert_eq!(pipeline.total_runs(), 3);
1296 }
1297
1298 #[test]
1299 fn update_batch_empty_outcomes_still_increments() {
1300 let mut pipeline = ValidationPipeline::new();
1301 pipeline.register("v", Duration::from_millis(5));
1302
1303 let result = PipelineResult {
1304 all_passed: true,
1305 outcomes: Vec::new(),
1306 total_cost: Duration::ZERO,
1307 ordering: vec![0],
1308 ledger: Vec::new(),
1309 skipped: 1,
1310 };
1311
1312 pipeline.update_batch(&result);
1313 assert_eq!(pipeline.total_runs(), 1);
1314 assert_eq!(pipeline.stats(0).unwrap().observations, 0);
1316 }
1317
1318 #[test]
1319 fn run_then_update_batch_round_trip() {
1320 let mut pipeline = ValidationPipeline::new();
1321 pipeline.register("fast", Duration::from_millis(1));
1322 pipeline.register("slow", Duration::from_millis(100));
1323
1324 let result = pipeline.run(|id| {
1325 if id == 0 {
1326 (true, Duration::from_millis(2))
1327 } else {
1328 (true, Duration::from_millis(80))
1329 }
1330 });
1331 assert!(result.all_passed);
1332
1333 pipeline.update_batch(&result);
1334 assert_eq!(pipeline.total_runs(), 1);
1335
1336 assert_eq!(pipeline.stats(0).unwrap().observations, 1);
1338 assert_eq!(pipeline.stats(1).unwrap().observations, 1);
1339 assert!(
1341 pipeline.stats(0).unwrap().beta > 1.0,
1342 "beta should increase on success"
1343 );
1344 }
1345
1346 #[test]
1347 fn run_then_update_batch_with_early_exit() {
1348 let mut pipeline = ValidationPipeline::new();
1349 pipeline.register("a", Duration::from_millis(5));
1350 pipeline.register("b", Duration::from_millis(10));
1351
1352 let result = pipeline.run(|id| {
1353 if id == 0 {
1354 (false, Duration::from_millis(3))
1355 } else {
1356 (true, Duration::from_millis(8))
1357 }
1358 });
1359 assert!(!result.all_passed);
1360
1361 pipeline.update_batch(&result);
1362 let ran_id = result.outcomes[0].id;
1364 assert_eq!(pipeline.stats(ran_id).unwrap().observations, 1);
1365
1366 let skipped_count: u64 = pipeline
1368 .all_stats()
1369 .iter()
1370 .filter(|s| s.observations == 0)
1371 .count() as u64;
1372 assert_eq!(skipped_count, 1, "one validator was skipped");
1373 }
1374
1375 #[test]
1376 fn confidence_width_always_positive() {
1377 let mut pipeline = ValidationPipeline::new();
1378 let id = pipeline.register("v", Duration::from_millis(5));
1379
1380 let w = pipeline.stats(id).unwrap().confidence_width();
1382 assert!(w > 0.0, "confidence_width should be positive: {w}");
1383
1384 for _ in 0..10 {
1386 pipeline.update(&ValidationOutcome {
1387 id,
1388 passed: true,
1389 duration: Duration::from_millis(5),
1390 });
1391 }
1392 let w2 = pipeline.stats(id).unwrap().confidence_width();
1393 assert!(w2 > 0.0, "confidence_width should be positive: {w2}");
1394 }
1395
1396 #[test]
1397 fn variance_known_values() {
1398 let mut pipeline = ValidationPipeline::new();
1399 let id = pipeline.register("v", Duration::from_millis(5));
1400
1401 let var = pipeline.stats(id).unwrap().variance();
1403 assert!(
1404 (var - 1.0 / 12.0).abs() < 1e-10,
1405 "Beta(1,1) variance should be 1/12"
1406 );
1407
1408 for _ in 0..3 {
1410 pipeline.update(&ValidationOutcome {
1411 id,
1412 passed: false,
1413 duration: Duration::from_millis(5),
1414 });
1415 }
1416 let var2 = pipeline.stats(id).unwrap().variance();
1417 let expected = 4.0 * 1.0 / (25.0 * 6.0);
1418 assert!(
1419 (var2 - expected).abs() < 1e-10,
1420 "Beta(4,1) variance: expected {expected}, got {var2}"
1421 );
1422 }
1423
1424 #[test]
1425 fn strong_prior_dominates() {
1426 let config = PipelineConfig {
1427 prior_alpha: 100.0,
1428 prior_beta: 100.0,
1429 ..Default::default()
1430 };
1431 let mut pipeline = ValidationPipeline::with_config(config);
1432 let id = pipeline.register("v", Duration::from_millis(5));
1433
1434 for _ in 0..5 {
1437 pipeline.update(&ValidationOutcome {
1438 id,
1439 passed: false,
1440 duration: Duration::from_millis(5),
1441 });
1442 }
1443 let p = pipeline.stats(id).unwrap().failure_prob();
1444 assert!(
1445 (p - 105.0 / 205.0).abs() < 1e-10,
1446 "strong prior should dominate: got {p}"
1447 );
1448 }
1449
1450 #[test]
1451 fn register_empty_name() {
1452 let mut pipeline = ValidationPipeline::new();
1453 let id = pipeline.register("", Duration::from_millis(5));
1454 assert_eq!(pipeline.stats(id).unwrap().name, "");
1455 }
1456
1457 #[test]
1458 fn expected_cost_many_validators_survival_shrinks() {
1459 let mut pipeline = ValidationPipeline::new();
1460 for i in 0..10 {
1462 pipeline.register(format!("v{i}"), Duration::from_millis(10));
1463 }
1464
1465 let ordering: Vec<usize> = (0..10).collect();
1466 let cost = pipeline.expected_cost(&ordering);
1467
1468 let c = 0.010; let geometric_sum: f64 = (0..10).map(|k| 0.5_f64.powi(k)).sum();
1472 let expected = c * geometric_sum;
1473 assert!(
1474 (cost - expected).abs() < 1e-10,
1475 "expected {expected}, got {cost}"
1476 );
1477 }
1478
1479 #[test]
1480 fn score_with_very_large_cost() {
1481 let mut pipeline = ValidationPipeline::new();
1482 let id = pipeline.register("v", Duration::from_secs(1_000_000));
1483 let score = pipeline.stats(id).unwrap().score(C_MIN);
1484 assert!(score.is_finite());
1485 assert!(score > 0.0);
1486 assert!(score < 1.0, "score with huge cost should be tiny");
1487 }
1488
1489 #[test]
1490 fn score_with_cost_at_c_min() {
1491 let mut pipeline = ValidationPipeline::new();
1492 let id = pipeline.register("v", C_MIN);
1493 let score = pipeline.stats(id).unwrap().score(C_MIN);
1494 assert!(score.is_finite());
1495 assert!(score > 1.0, "score with c_min cost should be large");
1497 }
1498
1499 #[test]
1500 fn ledger_entry_fields_match_stats() {
1501 let mut pipeline = ValidationPipeline::new();
1502 pipeline.register("v", Duration::from_millis(10));
1503
1504 for _ in 0..5 {
1506 pipeline.update(&ValidationOutcome {
1507 id: 0,
1508 passed: false,
1509 duration: Duration::from_millis(10),
1510 });
1511 }
1512
1513 let (_, ledger) = pipeline.compute_ordering();
1514 assert_eq!(ledger.len(), 1);
1515 let entry = &ledger[0];
1516 let stats = pipeline.stats(0).unwrap();
1517
1518 assert_eq!(entry.id, stats.id);
1519 assert_eq!(entry.name, stats.name);
1520 assert!((entry.p - stats.failure_prob()).abs() < 1e-10);
1521 assert_eq!(entry.c, stats.cost_ema);
1522 assert!((entry.score - stats.score(C_MIN)).abs() < 1e-10);
1523 assert_eq!(entry.rank, 0);
1524 }
1525
1526 #[test]
1527 fn run_closure_called_in_ordering_sequence() {
1528 let mut pipeline = ValidationPipeline::new();
1529 pipeline.register("expensive", Duration::from_millis(100));
1530 pipeline.register("cheap", Duration::from_millis(1));
1531
1532 let mut call_order = Vec::new();
1534 let _result = pipeline.run(|id| {
1535 call_order.push(id);
1536 (true, Duration::from_millis(5))
1537 });
1538
1539 let (expected_ordering, _) = pipeline.compute_ordering();
1540 assert_eq!(
1541 call_order, expected_ordering,
1542 "closure should be called in ordering sequence"
1543 );
1544 }
1545
1546 #[test]
1547 fn pipeline_result_ordering_matches_compute() {
1548 let mut pipeline = ValidationPipeline::new();
1549 pipeline.register("a", Duration::from_millis(5));
1550 pipeline.register("b", Duration::from_millis(50));
1551 pipeline.register("c", Duration::from_millis(1));
1552
1553 let (expected_ordering, _) = pipeline.compute_ordering();
1554 let result = pipeline.run(|_| (true, Duration::from_millis(3)));
1555 assert_eq!(result.ordering, expected_ordering);
1556 }
1557
1558 #[test]
1559 fn update_batch_applies_to_all_outcomes() {
1560 let mut pipeline = ValidationPipeline::new();
1561 pipeline.register("a", Duration::from_millis(5));
1562 pipeline.register("b", Duration::from_millis(10));
1563
1564 let result = PipelineResult {
1565 all_passed: true,
1566 outcomes: vec![
1567 ValidationOutcome {
1568 id: 0,
1569 passed: true,
1570 duration: Duration::from_millis(4),
1571 },
1572 ValidationOutcome {
1573 id: 1,
1574 passed: false,
1575 duration: Duration::from_millis(8),
1576 },
1577 ],
1578 total_cost: Duration::from_millis(12),
1579 ordering: vec![0, 1],
1580 ledger: Vec::new(),
1581 skipped: 0,
1582 };
1583
1584 pipeline.update_batch(&result);
1585 assert_eq!(pipeline.stats(0).unwrap().observations, 1);
1586 assert_eq!(pipeline.stats(0).unwrap().failures, 0);
1587 assert_eq!(pipeline.stats(1).unwrap().observations, 1);
1588 assert_eq!(pipeline.stats(1).unwrap().failures, 1);
1589 }
1590
1591 #[test]
1592 fn multiple_pipelines_independent() {
1593 let mut p1 = ValidationPipeline::new();
1594 let mut p2 = ValidationPipeline::new();
1595
1596 p1.register("v", Duration::from_millis(5));
1597 p2.register("v", Duration::from_millis(5));
1598
1599 for _ in 0..10 {
1600 p1.update(&ValidationOutcome {
1601 id: 0,
1602 passed: false,
1603 duration: Duration::from_millis(5),
1604 });
1605 }
1606
1607 assert_eq!(p2.stats(0).unwrap().observations, 0);
1609 assert_eq!(p1.stats(0).unwrap().observations, 10);
1610 }
1611
1612 #[test]
1613 fn pipeline_clone_independent() {
1614 let mut original = ValidationPipeline::new();
1615 original.register("v", Duration::from_millis(5));
1616
1617 let mut cloned = original.clone();
1618
1619 cloned.update(&ValidationOutcome {
1621 id: 0,
1622 passed: false,
1623 duration: Duration::from_millis(5),
1624 });
1625
1626 assert_eq!(original.stats(0).unwrap().observations, 0);
1628 assert_eq!(cloned.stats(0).unwrap().observations, 1);
1629 }
1630
1631 #[test]
1632 fn pipeline_config_clone() {
1633 let config = PipelineConfig {
1634 prior_alpha: 2.0,
1635 prior_beta: 3.0,
1636 gamma: 0.5,
1637 c_min: Duration::from_micros(10),
1638 };
1639 let cloned = config.clone();
1640 assert!((cloned.prior_alpha - 2.0).abs() < 1e-10);
1641 assert!((cloned.prior_beta - 3.0).abs() < 1e-10);
1642 assert!((cloned.gamma - 0.5).abs() < 1e-10);
1643 assert_eq!(cloned.c_min, Duration::from_micros(10));
1644 }
1645
1646 #[test]
1647 fn validator_stats_clone() {
1648 let mut pipeline = ValidationPipeline::new();
1649 let id = pipeline.register("v", Duration::from_millis(5));
1650 for _ in 0..3 {
1651 pipeline.update(&ValidationOutcome {
1652 id,
1653 passed: false,
1654 duration: Duration::from_millis(5),
1655 });
1656 }
1657 let stats = pipeline.stats(id).unwrap().clone();
1658 assert_eq!(stats.observations, 3);
1659 assert_eq!(stats.failures, 3);
1660 assert_eq!(stats.name, "v");
1661 }
1662
1663 #[test]
1664 fn debug_formatting_pipeline_result() {
1665 let result = PipelineResult {
1666 all_passed: true,
1667 outcomes: Vec::new(),
1668 total_cost: Duration::ZERO,
1669 ordering: vec![0],
1670 ledger: Vec::new(),
1671 skipped: 0,
1672 };
1673 let debug = format!("{result:?}");
1674 assert!(debug.contains("PipelineResult"));
1675 assert!(debug.contains("all_passed: true"));
1676 }
1677
1678 #[test]
1679 fn debug_formatting_validation_outcome() {
1680 let outcome = ValidationOutcome {
1681 id: 42,
1682 passed: false,
1683 duration: Duration::from_millis(10),
1684 };
1685 let debug = format!("{outcome:?}");
1686 assert!(debug.contains("ValidationOutcome"));
1687 assert!(debug.contains("42"));
1688 assert!(debug.contains("false"));
1689 }
1690
1691 #[test]
1692 fn debug_formatting_ledger_entry() {
1693 let entry = LedgerEntry {
1694 id: 7,
1695 name: "test_validator".to_string(),
1696 p: 0.75,
1697 c: Duration::from_millis(10),
1698 score: 75.0,
1699 rank: 0,
1700 };
1701 let debug = format!("{entry:?}");
1702 assert!(debug.contains("LedgerEntry"));
1703 assert!(debug.contains("test_validator"));
1704 }
1705
1706 #[test]
1707 fn debug_formatting_pipeline_summary() {
1708 let summary = PipelineSummary {
1709 validator_count: 2,
1710 total_runs: 5,
1711 optimal_ordering: vec![1, 0],
1712 expected_cost_secs: 0.015,
1713 natural_cost_secs: 0.020,
1714 improvement_fraction: 0.25,
1715 ledger: Vec::new(),
1716 };
1717 let debug = format!("{summary:?}");
1718 assert!(debug.contains("PipelineSummary"));
1719 assert!(debug.contains("validator_count: 2"));
1720 }
1721
1722 #[test]
1723 fn debug_formatting_pipeline_config() {
1724 let config = PipelineConfig::default();
1725 let debug = format!("{config:?}");
1726 assert!(debug.contains("PipelineConfig"));
1727 assert!(debug.contains("prior_alpha"));
1728 }
1729
1730 #[test]
1731 fn debug_formatting_validator_stats() {
1732 let mut pipeline = ValidationPipeline::new();
1733 let id = pipeline.register("test_v", Duration::from_millis(5));
1734 let stats = pipeline.stats(id).unwrap();
1735 let debug = format!("{stats:?}");
1736 assert!(debug.contains("ValidatorStats"));
1737 assert!(debug.contains("test_v"));
1738 }
1739
1740 #[test]
1741 fn debug_formatting_validation_pipeline() {
1742 let mut pipeline = ValidationPipeline::new();
1743 pipeline.register("v", Duration::from_millis(5));
1744 let debug = format!("{pipeline:?}");
1745 assert!(debug.contains("ValidationPipeline"));
1746 }
1747
1748 #[test]
1749 fn with_config_custom_prior() {
1750 let config = PipelineConfig {
1751 prior_alpha: 5.0,
1752 prior_beta: 10.0,
1753 ..Default::default()
1754 };
1755 let mut pipeline = ValidationPipeline::with_config(config);
1756 let id = pipeline.register("v", Duration::from_millis(5));
1757
1758 let p = pipeline.stats(id).unwrap().failure_prob();
1760 assert!(
1761 (p - 1.0 / 3.0).abs() < 1e-10,
1762 "custom prior should set initial p: got {p}"
1763 );
1764 }
1765
1766 #[test]
1767 fn with_config_custom_c_min() {
1768 let config = PipelineConfig {
1769 c_min: Duration::from_millis(10),
1770 ..Default::default()
1771 };
1772 let mut pipeline = ValidationPipeline::with_config(config);
1773 let id = pipeline.register("v", Duration::from_millis(1));
1774
1775 let cost = pipeline.stats(id).unwrap().cost_ema;
1777 assert!(
1778 cost >= Duration::from_millis(10),
1779 "cost should be clamped to c_min: got {:?}",
1780 cost
1781 );
1782 }
1783
1784 #[test]
1785 fn update_does_not_increment_total_runs() {
1786 let mut pipeline = ValidationPipeline::new();
1787 pipeline.register("v", Duration::from_millis(5));
1788
1789 pipeline.update(&ValidationOutcome {
1790 id: 0,
1791 passed: true,
1792 duration: Duration::from_millis(5),
1793 });
1794 assert_eq!(
1796 pipeline.total_runs(),
1797 0,
1798 "update() should not increment total_runs"
1799 );
1800 }
1801
1802 #[test]
1803 fn ordering_reverses_after_learning() {
1804 let mut pipeline = ValidationPipeline::new();
1805 let a = pipeline.register("a", Duration::from_millis(10));
1806 let b = pipeline.register("b", Duration::from_millis(10));
1807
1808 let (ordering1, _) = pipeline.compute_ordering();
1810 assert_eq!(ordering1, vec![0, 1]);
1811
1812 for _ in 0..20 {
1814 pipeline.update(&ValidationOutcome {
1815 id: b,
1816 passed: false,
1817 duration: Duration::from_millis(10),
1818 });
1819 }
1820 for _ in 0..20 {
1821 pipeline.update(&ValidationOutcome {
1822 id: a,
1823 passed: true,
1824 duration: Duration::from_millis(10),
1825 });
1826 }
1827
1828 let (ordering2, _) = pipeline.compute_ordering();
1829 assert_eq!(ordering2[0], 1, "flaky validator b should now come first");
1830 }
1831
1832 #[test]
1833 fn summary_natural_cost_matches_sequential_order() {
1834 let mut pipeline = ValidationPipeline::new();
1835 pipeline.register("a", Duration::from_millis(10));
1836 pipeline.register("b", Duration::from_millis(20));
1837 pipeline.register("c", Duration::from_millis(5));
1838
1839 let summary = pipeline.summary();
1840 let natural: Vec<usize> = (0..3).collect();
1841 let natural_cost = pipeline.expected_cost(&natural);
1842 assert!(
1843 (summary.natural_cost_secs - natural_cost).abs() < 1e-15,
1844 "summary natural cost should match sequential ordering"
1845 );
1846 }
1847
1848 #[test]
1849 fn validator_count_after_multiple_registers() {
1850 let mut pipeline = ValidationPipeline::new();
1851 assert_eq!(pipeline.validator_count(), 0);
1852 pipeline.register("a", Duration::from_millis(5));
1853 assert_eq!(pipeline.validator_count(), 1);
1854 pipeline.register("b", Duration::from_millis(10));
1855 assert_eq!(pipeline.validator_count(), 2);
1856 pipeline.register("c", Duration::from_millis(15));
1857 assert_eq!(pipeline.validator_count(), 3);
1858 }
1859
1860 #[test]
1861 fn alpha_beta_after_mixed_outcomes() {
1862 let mut pipeline = ValidationPipeline::new();
1863 let id = pipeline.register("v", Duration::from_millis(5));
1864
1865 for _ in 0..5 {
1868 pipeline.update(&ValidationOutcome {
1869 id,
1870 passed: false,
1871 duration: Duration::from_millis(5),
1872 });
1873 }
1874 for _ in 0..3 {
1875 pipeline.update(&ValidationOutcome {
1876 id,
1877 passed: true,
1878 duration: Duration::from_millis(5),
1879 });
1880 }
1881
1882 let stats = pipeline.stats(id).unwrap();
1883 assert!((stats.alpha - 6.0).abs() < 1e-10);
1884 assert!((stats.beta - 4.0).abs() < 1e-10);
1885 assert!((stats.failure_prob() - 0.6).abs() < 1e-10);
1886 }
1887
1888 #[test]
1889 fn pipeline_result_total_cost_accumulates() {
1890 let mut pipeline = ValidationPipeline::new();
1891 pipeline.register("a", Duration::from_millis(5));
1892 pipeline.register("b", Duration::from_millis(10));
1893
1894 let result = pipeline.run(|id| {
1895 if id == 0 {
1896 (true, Duration::from_millis(7))
1897 } else {
1898 (true, Duration::from_millis(12))
1899 }
1900 });
1901
1902 assert_eq!(result.total_cost, Duration::from_millis(19));
1903 }
1904
1905 #[test]
1906 fn cost_ema_multiple_updates_converge() {
1907 let mut pipeline = ValidationPipeline::with_config(PipelineConfig {
1908 gamma: 0.5,
1909 ..Default::default()
1910 });
1911 let id = pipeline.register("v", Duration::from_millis(100));
1912
1913 for _ in 0..50 {
1915 pipeline.update(&ValidationOutcome {
1916 id,
1917 passed: true,
1918 duration: Duration::from_millis(10),
1919 });
1920 }
1921 let cost = pipeline.stats(id).unwrap().cost_ema;
1922 assert!(
1924 (cost.as_millis() as i64 - 10).abs() <= 1,
1925 "EMA should converge to observed value: got {}ms",
1926 cost.as_millis()
1927 );
1928 }
1929
1930 #[test]
1931 fn ledger_ranks_are_contiguous() {
1932 let mut pipeline = ValidationPipeline::new();
1933 for i in 0..5 {
1934 pipeline.register(format!("v{i}"), Duration::from_millis((i as u64 + 1) * 10));
1935 }
1936
1937 let (_, ledger) = pipeline.compute_ordering();
1938 let mut ranks: Vec<usize> = ledger.iter().map(|e| e.rank).collect();
1939 ranks.sort_unstable();
1940 assert_eq!(ranks, vec![0, 1, 2, 3, 4]);
1941 }
1942
1943 #[test]
1944 fn ledger_scores_descending() {
1945 let mut pipeline = ValidationPipeline::new();
1946 pipeline.register("a", Duration::from_millis(5));
1947 pipeline.register("b", Duration::from_millis(50));
1948 pipeline.register("c", Duration::from_millis(1));
1949
1950 let (_, ledger) = pipeline.compute_ordering();
1951 for window in ledger.windows(2) {
1952 assert!(
1953 window[0].score >= window[1].score,
1954 "ledger scores should be descending: {} < {}",
1955 window[0].score,
1956 window[1].score
1957 );
1958 }
1959 }
1960}