use muxer::{Outcome, Router, RouterConfig, TriageSessionConfig};
fn clean() -> Outcome {
Outcome::success(2, 80)
}
fn degraded() -> Outcome {
Outcome::failure(2, 300)
}
fn main() {
println!("=== 1. Basic two-arm routing ===");
let arms = vec!["arm-a".to_string(), "arm-b".to_string()];
let mut router = Router::new(arms, RouterConfig::default()).unwrap();
for i in 0..10 {
let d = router.select(1, i as u64);
let arm = d.primary().unwrap().to_string();
println!(" round {i}: chose {arm:?} prechosen={:?}", d.prechosen);
router.observe(&arm, clean());
}
println!("\n=== 2. Quality divergence ===");
for _ in 0..30 {
router.observe("arm-b", Outcome::degraded(2, 80));
router.observe("arm-a", clean());
}
let d = router.select(1, 99);
println!(
" After arm-b accumulates junk: chose {:?}",
d.primary().unwrap()
);
assert_eq!(
d.primary().unwrap(),
"arm-a",
"should prefer arm-a after arm-b degrades"
);
println!("\n=== 3. Triage mode ===");
let tcfg = TriageSessionConfig {
min_n: 10,
threshold: 3.0,
..TriageSessionConfig::default()
};
let cfg = RouterConfig::default().with_triage_cfg(tcfg);
let arms2 = vec!["stable".to_string(), "degraded".to_string()];
let mut r2 = Router::new(arms2, cfg).unwrap();
for _ in 0..20 {
r2.observe("stable", clean());
r2.observe("degraded", clean());
}
for _ in 0..30 {
r2.observe("degraded", degraded());
}
println!(" mode after failures: {:?}", r2.mode());
assert!(
r2.mode().is_triage(),
"should be in triage after hard failures"
);
println!(" alarmed arms: {:?}", r2.mode().alarmed_arms());
let d = r2.select(2, 0);
println!(" triage picks: {:?}", d.chosen);
println!(" triage cells: {} cells", d.triage_cells.len());
r2.acknowledge_change("degraded");
println!(" mode after acknowledge: {:?}", r2.mode());
assert!(
!r2.mode().is_triage(),
"should return to Normal after acknowledge"
);
println!("\n=== 4. Large-K batch exploration (K=20, k=3) ===");
let n = 20;
let arms_large: Vec<String> = (0..n).map(|i| format!("svc-{i:02}")).collect();
let cfg_large = RouterConfig::default().with_coverage(0.02, 1);
let mut rl = Router::new(arms_large, cfg_large).unwrap();
let mut seen = std::collections::HashSet::new();
let mut rounds = 0;
while seen.len() < n && rounds < 20 {
let d = rl.select(3, rounds as u64);
for arm in &d.chosen {
seen.insert(arm.clone());
rl.observe(arm, clean());
}
rounds += 1;
}
println!(" Covered all {n} arms in {rounds} rounds with k=3");
assert_eq!(seen.len(), n, "all arms should be covered");
println!("\n=== 5. Dynamic arm management ===");
let mut rd = Router::new(
vec!["old-a".to_string(), "old-b".to_string()],
RouterConfig::default(),
)
.unwrap();
for _ in 0..20 {
rd.observe("old-a", clean());
rd.observe("old-b", clean());
}
rd.add_arm("new-c".to_string()).unwrap();
let d = rd.select(1, 0);
println!(" After add_arm('new-c'): chose {:?}", d.primary().unwrap());
assert_eq!(
d.primary().unwrap(),
"new-c",
"newly added arm should be explored first"
);
let _ = rd.remove_arm("old-b");
for _ in 0..100 {
let d = rd.select(1, 0);
assert_ne!(
d.primary().unwrap(),
"old-b",
"removed arm must not be selected"
);
}
println!(" remove_arm('old-b'): never selected again ✓");
println!("\nAll assertions passed.");
}