#![allow(clippy::all, dead_code)]
use wasm4pm::reinforcement::Agent;
use wasm4pm::rl_orchestrator::{compute_reward, AgentType};
use wasm4pm::spc::{check_western_electric_rules, ChartData};
use wasm4pm::{RlAction, RlState};
fn make_test_state(health_level: u8) -> RlState {
let features = [0.5, 0.3, 0.2, 0.0, 0.0, 0.0, 0.5, 0.0]; RlState::from_features(&features, health_level, 0.0) }
#[test]
fn test_orchestrator_default() {
let orch = wasm4pm::rl_orchestrator::RlOrchestrator::new();
assert_eq!(orch.active_agent(), AgentType::QLearning);
assert_eq!(orch.telemetry().cycle_count, 0);
}
#[test]
fn test_orchestrator_persists_across_cycles() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new();
let features = [0.1, 0.2, 0.3, 0.5, 0.0, 1.0, 1.0, 0.0];
let state = make_test_state(1);
let next_state = make_test_state(1);
orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
assert_eq!(orch.telemetry().cycle_count, 1);
orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
assert_eq!(orch.telemetry().cycle_count, 2);
orch.run_cycle(&features, &state, &next_state, 2, false, true, false);
assert_eq!(orch.telemetry().cycle_count, 3);
assert_eq!(orch.telemetry().last_spc_alert_count, 2);
}
#[test]
fn test_reward_improves_with_health_gain() {
let r_good = compute_reward(3, 1, 0, true, true, false, 0);
let r_stable = compute_reward(2, 2, 0, true, true, false, 0);
let r_bad = compute_reward(1, 3, 0, true, true, false, 0);
assert!(r_good > r_stable);
assert!(r_stable > r_bad);
}
#[test]
fn test_reward_penalizes_spc_alerts() {
let r_clean = compute_reward(2, 2, 0, true, true, false, 0);
let r_dirty = compute_reward(2, 2, 5, true, true, false, 0);
assert!(r_clean > r_dirty);
}
#[test]
fn test_reward_terminal_is_worst() {
let r_terminal = compute_reward(3, 4, 0, true, true, false, 0);
let r_stable = compute_reward(4, 4, 0, true, true, false, 0); assert!(r_terminal <= r_stable);
}
#[test]
fn test_all_five_agents_work_in_loop() {
let features = [0.1, 0.2, 0.3, 0.25, 0.0, 1.0, 1.0, 0.0];
let state = make_test_state(1);
let next_state = make_test_state(1);
for agent_type in &[
AgentType::QLearning,
AgentType::SARSA,
AgentType::DoubleQLearning,
AgentType::ExpectedSARSA,
AgentType::REINFORCE,
] {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new();
orch.switch_agent(*agent_type);
for i in 0..10 {
let spc_alerts = if i % 3 == 0 { 2 } else { 0 };
let (action, reward) = orch.run_cycle(
&features,
&state,
&next_state,
spc_alerts,
true,
true,
false,
);
assert!(
!action.is_empty(),
"Agent {:?} should produce an action",
agent_type
);
assert!(!reward.is_nan(), "Reward should not be NaN");
}
assert_eq!(orch.telemetry().cycle_count, 10);
}
}
#[test]
fn test_linucb_agent_selection_changes_agent() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new();
orch.set_linucb_selection(true);
let features = [0.5, 0.5, 0.5, 0.5, 0.0, 1.0, 1.0, 0.0];
let state = make_test_state(1);
let next_state = make_test_state(1);
let mut seen_agents = std::collections::HashSet::new();
for _ in 0..50 {
orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
seen_agents.insert(orch.active_agent() as u8);
}
assert_eq!(orch.telemetry().cycle_count, 50);
}
#[test]
fn test_agent_trait_polymorphism() {
let state = make_test_state(1);
let next = make_test_state(2);
let q = wasm4pm::reinforcement::QLearning::<RlState, RlAction>::new();
let act1 = Agent::select_action(&q, &state);
assert!(
act1 == RlAction::Continue
|| act1 == RlAction::Scale
|| act1 == RlAction::Retry
|| act1 == RlAction::Restart
|| act1 == RlAction::Fallback,
"Valid action returned"
);
Agent::update(&q, &state, &RlAction::Continue, 0.5, &next, false);
let sa = wasm4pm::reinforcement::SARSAAgent::<RlState, RlAction>::new();
let act2 = Agent::select_action(&sa, &state);
assert!(
act2 == RlAction::Continue
|| act2 == RlAction::Scale
|| act2 == RlAction::Retry
|| act2 == RlAction::Restart
|| act2 == RlAction::Fallback,
"Valid action returned"
);
Agent::update(&sa, &state, &RlAction::Scale, 0.3, &next, false);
let dq = wasm4pm::reinforcement::DoubleQLearning::<RlState, RlAction>::new();
let act3 = Agent::select_action(&dq, &state);
assert!(
act3 == RlAction::Continue
|| act3 == RlAction::Scale
|| act3 == RlAction::Retry
|| act3 == RlAction::Restart
|| act3 == RlAction::Fallback,
"Valid action returned"
);
Agent::update(&dq, &state, &RlAction::Retry, -0.2, &next, false);
let es = wasm4pm::reinforcement::ExpectedSARSAAgent::<RlState, RlAction>::new();
let act4 = Agent::select_action(&es, &state);
assert!(
act4 == RlAction::Continue
|| act4 == RlAction::Scale
|| act4 == RlAction::Retry
|| act4 == RlAction::Restart
|| act4 == RlAction::Fallback,
"Valid action returned"
);
Agent::update(&es, &state, &RlAction::Fallback, 0.1, &next, false);
let rf = wasm4pm::reinforcement::ReinforceAgent::<RlState, RlAction>::new();
let act5 = Agent::select_action(&rf, &state);
assert!(
act5 == RlAction::Continue
|| act5 == RlAction::Scale
|| act5 == RlAction::Retry
|| act5 == RlAction::Restart
|| act5 == RlAction::Fallback,
"Valid action returned"
);
Agent::update(&rf, &state, &RlAction::Restart, -1.0, &next, true);
}
#[test]
fn test_single_autonomic_cycle_completes_in_under_100ms() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new_with_seed(42);
let features = [0.5, 0.3, 0.2, 0.0, 0.0, 0.0, 0.5, 0.0];
let state = make_test_state(1);
let next_state = make_test_state(1);
let start = std::time::Instant::now();
let (_action, _reward) = orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(100),
"Single autonomic cycle must complete in <100ms: took {:?}",
elapsed
);
}
#[test]
fn test_g2_fifty_consecutive_cycles_no_panic() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new_with_seed(42);
let features = [0.1, 0.2, 0.3, 0.25, 0.0, 1.0, 1.0, 0.0];
let state = make_test_state(1);
let next_state = make_test_state(1);
for i in 0..50 {
let (action, reward) = orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
assert!(
!action.is_empty(),
"cycle {}: action should not be empty",
i + 1
);
assert!(
!reward.is_nan(),
"cycle {}: reward should not be NaN",
i + 1
);
}
assert_eq!(
orch.telemetry().cycle_count,
50,
"cycle_count must equal exactly 50 after 50 run_cycle calls"
);
}
#[test]
fn test_g3_degraded_to_recovery_reward_increases() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new_with_seed(42);
let features = [0.1, 0.2, 0.3, 0.25, 0.0, 1.0, 1.0, 0.0];
let degraded = make_test_state(3);
let mut phase1_rewards = Vec::new();
for _ in 0..10 {
let (_, reward) = orch.run_cycle(&features, °raded, °raded, 0, true, true, false);
phase1_rewards.push(reward);
}
let mut phase2_rewards = Vec::new();
let mut current_health: u8 = 3;
for _ in 0..10 {
let state = make_test_state(current_health);
let next_health = if current_health > 0 {
current_health - 1
} else {
0
};
let next_state = make_test_state(next_health);
let (_, reward) = orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
phase2_rewards.push(reward);
current_health = next_health;
}
let mean_phase1: f32 = phase1_rewards.iter().sum::<f32>() / phase1_rewards.len() as f32;
let mean_phase2: f32 = phase2_rewards.iter().sum::<f32>() / phase2_rewards.len() as f32;
assert!(
mean_phase2 > mean_phase1,
"Mean reward during recovery ({:.4}) should exceed mean reward during degraded phase ({:.4}). \
Phase 1 rewards: {:?}\nPhase 2 rewards: {:?}",
mean_phase2,
mean_phase1,
phase1_rewards,
phase2_rewards
);
}
#[test]
fn test_reward_penalizes_latency_budget_exceeded() {
let r_no_latency = compute_reward(2, 1, 0, true, true, false, 0);
let r_with_latency = compute_reward(2, 1, 0, true, true, true, 0);
let penalty = r_no_latency - r_with_latency;
assert_eq!(
penalty, 0.3,
"Latency budget exceeded should apply -0.3 penalty: \
reward without latency={:.4}, with latency={:.4}, penalty={:.4}",
r_no_latency, r_with_latency, penalty
);
let r_spc_only = compute_reward(2, 2, 1, true, true, false, 0);
let r_spc_and_latency = compute_reward(2, 2, 1, true, true, true, 0);
let stacked_penalty = r_spc_only - r_spc_and_latency;
assert_eq!(
stacked_penalty, 0.3,
"Latency penalty should stack independently with SPC penalty: \
penalty={:.4}",
stacked_penalty
);
}
fn make_chart_data(values: &[f64]) -> Vec<ChartData> {
if values.is_empty() {
return vec![];
}
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
let std_dev = variance.sqrt();
values
.iter()
.map(|&v| ChartData {
timestamp: String::new(),
value: v,
ucl: mean + 3.0 * std_dev,
cl: mean,
lcl: (mean - 3.0 * std_dev).max(0.0),
subgroup_data: None,
})
.collect()
}
#[test]
fn test_mape_k_chain_spc_violations_reduce_reward() {
let r_zero_alerts = compute_reward(2, 2, 0, true, true, false, 0);
let r_few_alerts = compute_reward(2, 2, 3, true, true, false, 0);
let r_many_alerts = compute_reward(2, 2, 8, true, true, false, 0);
assert!(
r_zero_alerts > r_few_alerts,
"0 SPC alerts should yield higher reward than 3: {:.4} > {:.4}",
r_zero_alerts,
r_few_alerts
);
assert!(
r_few_alerts >= r_many_alerts,
"3 SPC alerts should yield reward ≥ 8 alerts: {:.4} >= {:.4}",
r_few_alerts,
r_many_alerts
);
}
#[test]
fn test_mape_k_chain_spc_shift_detected_and_propagates() {
let baseline_values: Vec<f64> = vec![5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0];
let baseline_chart = make_chart_data(&baseline_values);
let baseline_causes = check_western_electric_rules(&baseline_chart);
assert!(
baseline_causes.is_empty(),
"Uniform data at mean should produce no SPC alerts, got: {:?}",
baseline_causes
);
let shifted_values: Vec<f64> = vec![0.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0];
let shifted_chart = make_chart_data(&shifted_values);
let shifted_causes = check_western_electric_rules(&shifted_chart);
assert!(
!shifted_causes.is_empty(),
"10-point series with 9 trailing above-mean values must trigger Rule 2 (shift), got: {:?}",
shifted_causes
);
let r_baseline = compute_reward(2, 2, 0, true, true, false, 0);
let r_shifted = compute_reward(2, 2, shifted_causes.len(), true, true, false, 0);
assert!(
r_shifted < r_baseline,
"SPC shift alerts must reduce reward: r_baseline={:.4}, r_shifted={:.4}, causes={}",
r_baseline,
r_shifted,
shifted_causes.len()
);
}
#[test]
fn test_mape_k_chain_degraded_health_drives_non_continue_actions() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new_with_seed(42);
let features = [0.8, 0.5, 0.3, 0.75, 0.0, 1.0, 0.5, 0.0];
let critical = make_test_state(3); let still_critical = make_test_state(3);
let mut seen_actions: std::collections::HashSet<String> = std::collections::HashSet::new();
for _ in 0..30 {
let (action, _reward) = orch.run_cycle(
&features,
&critical,
&still_critical,
8,
false,
false,
false,
);
seen_actions.insert(action);
}
let non_continue: Vec<&String> = seen_actions
.iter()
.filter(|a| a.as_str() != "Continue")
.collect();
assert!(
!non_continue.is_empty(),
"30 degraded cycles with 8 SPC alerts should produce at least one non-Continue action. \
Seen actions: {:?}",
seen_actions
);
}
#[test]
fn test_mape_k_chain_circuit_breaker_reduces_reward() {
let r_circuit_open = compute_reward(2, 2, 0, false, true, false, 0);
let r_circuit_closed = compute_reward(2, 2, 0, true, true, false, 0);
assert!(
r_circuit_closed > r_circuit_open,
"Closed circuit (allowed) must yield higher reward than open (blocked): \
closed={:.4}, open={:.4}",
r_circuit_closed,
r_circuit_open
);
}
#[test]
fn snapshot_reward_function_shape() {
let cases = vec![
(
"normal_healthy",
compute_reward(0, 0, 0, true, true, false, 0),
),
(
"degraded_recovering",
compute_reward(3, 2, 0, true, true, false, 0),
),
(
"critical_worsening",
compute_reward(2, 3, 5, false, false, false, 0),
),
(
"terminal_transition",
compute_reward(3, 4, 0, true, true, false, 0),
),
(
"latency_penalty",
compute_reward(2, 2, 0, true, true, true, 0),
),
];
let rounded: Vec<(&str, f64)> = cases
.iter()
.map(|(name, r)| (*name, ((*r as f64) * 10_000.0).round() / 10_000.0))
.collect();
insta::assert_debug_snapshot!("reward_function_shape", rounded);
}
#[test]
fn snapshot_spc_shift_causes_debug_format() {
let values: Vec<f64> = vec![0.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0];
let chart = make_chart_data(&values);
let causes = check_western_electric_rules(&chart);
let summary = format!("alert_count={}, causes={:?}", causes.len(), causes);
insta::assert_debug_snapshot!("spc_shift_causes", summary);
}
#[test]
fn snapshot_orchestrator_telemetry_after_10_cycles() {
let mut orch = wasm4pm::rl_orchestrator::RlOrchestrator::new_with_seed(42);
let features = [0.2, 0.3, 0.2, 0.25, 1.0, 0.0, 0.5, 0.01];
let healthy = make_test_state(0);
let still_healthy = make_test_state(0);
for _ in 0..10 {
orch.run_cycle(&features, &healthy, &still_healthy, 0, true, true, false);
}
let t = orch.telemetry();
let summary = format!(
"cycle_count={} agent={:?} last_spc={} consec_success={}",
t.cycle_count, t.active_agent_name, t.last_spc_alert_count, t.consecutive_successes
);
insta::assert_debug_snapshot!("orchestrator_telemetry_10_cycles", summary);
}