#![allow(clippy::all, dead_code)]
use std::fs;
use wasm4pm::models::EventLog;
use wasm4pm::rl_orchestrator::{AgentType, RlOrchestrator};
use wasm4pm::RlState;
const FIXTURES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
fn load_event_log_json(name: &str) -> EventLog {
let path = format!("{FIXTURES_DIR}/{name}");
let json_str = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to load fixture {}: {}", path, e));
serde_json::from_str(&json_str)
.unwrap_or_else(|e| panic!("Failed to parse event log JSON from {}: {}", path, e))
}
fn compute_log_metrics(log: &EventLog, activity_key: &str) -> (u64, u64, u64, f32) {
let trace_count = log.traces.len() as u64;
let event_count: u64 = log.traces.iter().map(|t| t.events.len() as u64).sum();
let mut activity_set = std::collections::HashSet::new();
let mut rework_count = 0u64;
for trace in &log.traces {
let mut seen = std::collections::HashSet::new();
for event in &trace.events {
if let Some(wasm4pm::models::AttributeValue::String(name)) =
event.attributes.get(activity_key)
{
activity_set.insert(name.clone());
if seen.contains(name) {
rework_count += 1;
}
seen.insert(name.clone());
}
}
}
let unique_activities = activity_set.len() as u64;
let rework_ratio = if trace_count > 0 {
(rework_count as f32) / (trace_count as f32)
} else {
0.0
};
(event_count, trace_count, unique_activities, rework_ratio)
}
fn build_features_from_metrics(
event_count: u64,
trace_count: u64,
unique_activities: u64,
health_level: u8,
spc_alert_count: usize,
guard_pass: bool,
circuit_allowed: bool,
cycle_count: u64,
) -> [f32; 8] {
[
(event_count as f32 / 10_000.0).min(1.0),
(trace_count as f32 / 1_000.0).min(1.0),
(unique_activities as f32 / 100.0).min(1.0),
(health_level as f32 / 4.0).min(1.0),
(spc_alert_count as f32 / 10.0).min(1.0),
if guard_pass { 1.0 } else { 0.0 },
if circuit_allowed { 1.0 } else { 0.0 },
(cycle_count as f32 / 1_000.0).min(1.0),
]
}
#[test]
fn test_real_log_spc_alerts_lower_reward() {
let log = load_event_log_json("running-example.json");
let (event_count, trace_count, unique_activities, rework_ratio) =
compute_log_metrics(&log, "activity");
let mut orch_no_alerts = RlOrchestrator::new_with_seed(42);
let mut orch_with_alerts = RlOrchestrator::new_with_seed(42);
let health_level = 0;
for i in 0..50 {
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
0,
true,
true,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, health_level, rework_ratio);
orch_no_alerts.run_cycle(&features, &state, &next_state, 0, true, true, false);
}
for i in 0..50 {
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
5,
true,
true,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, health_level, rework_ratio);
orch_with_alerts.run_cycle(&features, &state, &next_state, 5, true, true, false);
}
let reward_no_alerts = orch_no_alerts.telemetry().cumulative_reward;
let reward_with_alerts = orch_with_alerts.telemetry().cumulative_reward;
assert!(
reward_no_alerts > reward_with_alerts,
"Real log: reward with 0 alerts ({:.2}) must exceed reward with 5 alerts ({:.2})\n\
Log metrics: events={}, traces={}, activities={}",
reward_no_alerts,
reward_with_alerts,
event_count,
trace_count,
unique_activities,
);
}
#[test]
fn test_real_log_guard_failure_degrades_reward() {
let log = load_event_log_json("running-example.json");
let (event_count, trace_count, unique_activities, rework_ratio) =
compute_log_metrics(&log, "activity");
let mut orch_success = RlOrchestrator::new_with_seed(100);
let mut orch_failure = RlOrchestrator::new_with_seed(100);
let health_level = 0;
for i in 0..50 {
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
0,
true,
true,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, health_level, rework_ratio);
orch_success.run_cycle(&features, &state, &next_state, 0, true, true, false);
}
for i in 0..50 {
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
0,
false,
false,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, health_level + 1, rework_ratio);
orch_failure.run_cycle(&features, &state, &next_state, 0, false, false, false);
}
let reward_success = orch_success.telemetry().cumulative_reward;
let reward_failure = orch_failure.telemetry().cumulative_reward;
assert!(
reward_success > reward_failure,
"Real log: reward with guard_pass=true ({:.2}) must exceed guard_pass=false ({:.2})",
reward_success,
reward_failure,
);
}
#[test]
fn test_real_log_health_improves_with_consecutive_successes() {
let log = load_event_log_json("running-example.json");
let (event_count, trace_count, unique_activities, rework_ratio) =
compute_log_metrics(&log, "activity");
let mut orch = RlOrchestrator::new_with_seed(200);
let health_level: u8 = 3;
for i in 0..4 {
let next_health = match i {
0..=1 => 3, 2..=3 => 2, _ => 2,
};
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
0,
true,
true,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, next_health, rework_ratio);
orch.run_cycle(&features, &state, &next_state, 0, true, true, false);
let expected_consecutive = i + 1;
assert_eq!(
orch.telemetry().consecutive_successes,
expected_consecutive,
"Cycle {}: consecutive_successes should be {}",
i,
expected_consecutive,
);
}
assert_eq!(
orch.telemetry().last_health_state,
2,
"Real log: health should have improved from 3 to 2 after 4 successful cycles"
);
}
#[test]
fn test_real_log_features_encode_log_characteristics() {
let f = build_features_from_metrics(5000, 10, 6, 0, 0, true, true, 0);
assert!(
(f[0] - 0.5).abs() < 0.01,
"event_count=5000 → feature[0]=0.5, got {}",
f[0]
);
let f = build_features_from_metrics(25000, 10, 6, 0, 0, true, true, 0);
assert!(
(f[0] - 1.0).abs() < 0.01,
"event_count=25000 → feature[0]=1.0 (capped), got {}",
f[0]
);
let f = build_features_from_metrics(100, 500, 6, 0, 0, true, true, 0);
assert!(
(f[1] - 0.5).abs() < 0.01,
"trace_count=500 → feature[1]=0.5, got {}",
f[1]
);
let f = build_features_from_metrics(100, 10, 50, 0, 0, true, true, 0);
assert!(
(f[2] - 0.5).abs() < 0.01,
"unique_activities=50 → feature[2]=0.5, got {}",
f[2]
);
let f = build_features_from_metrics(100, 10, 6, 2, 0, true, true, 0);
assert!(
(f[3] - 0.5).abs() < 0.01,
"health_level=2 → feature[3]=0.5, got {}",
f[3]
);
let f = build_features_from_metrics(100, 10, 6, 0, 5, true, true, 0);
assert!(
(f[4] - 0.5).abs() < 0.01,
"spc_alert_count=5 → feature[4]=0.5, got {}",
f[4]
);
let f_pass = build_features_from_metrics(100, 10, 6, 0, 0, true, true, 0);
let f_fail = build_features_from_metrics(100, 10, 6, 0, 0, false, true, 0);
assert!(
(f_pass[5] - 1.0).abs() < 0.01,
"guard_pass=true → feature[5]=1.0, got {}",
f_pass[5]
);
assert!(
(f_fail[5] - 0.0).abs() < 0.01,
"guard_pass=false → feature[5]=0.0, got {}",
f_fail[5]
);
let f_open = build_features_from_metrics(100, 10, 6, 0, 0, true, false, 0);
assert!(
(f_open[6] - 0.0).abs() < 0.01,
"circuit_allowed=false → feature[6]=0.0, got {}",
f_open[6]
);
let f = build_features_from_metrics(100, 10, 6, 0, 0, true, true, 250);
assert!(
(f[7] - 0.25).abs() < 0.01,
"cycle_count=250 → feature[7]=0.25, got {}",
f[7]
);
let f = build_features_from_metrics(99999, 99999, 999, 4, 999, false, false, 99999);
for (i, val) in f.iter().enumerate() {
assert!(
*val >= 0.0 && *val <= 1.0,
"feature[{}] should be in [0,1], got {}",
i,
val
);
}
let log = load_event_log_json("running-example.json");
let (event_count, trace_count, unique_activities, _) = compute_log_metrics(&log, "activity");
let real_features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
0,
0,
true,
true,
0,
);
let non_zero = real_features.iter().filter(|&&v| v > 0.0).count();
assert!(
non_zero >= 2,
"Real log features should have at least 2 non-zero values, got {}",
non_zero
);
}
#[test]
fn test_all_agents_respond_to_real_log_spc_alerts() {
let log = load_event_log_json("running-example.json");
let (event_count, trace_count, unique_activities, rework_ratio) =
compute_log_metrics(&log, "activity");
let agent_types = [
AgentType::QLearning,
AgentType::SARSA,
AgentType::DoubleQLearning,
AgentType::ExpectedSARSA,
AgentType::REINFORCE,
];
let health_level = 0;
for agent_type in &agent_types {
let mut orch_clean = RlOrchestrator::new_with_seed(500);
let mut orch_alerts = RlOrchestrator::new_with_seed(500);
orch_clean.switch_agent(*agent_type);
orch_alerts.switch_agent(*agent_type);
for i in 0..30 {
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
0,
true,
true,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, health_level, rework_ratio);
orch_clean.run_cycle(&features, &state, &next_state, 0, true, true, false);
}
for i in 0..30 {
let features = build_features_from_metrics(
event_count,
trace_count,
unique_activities,
health_level,
4,
true,
true,
i as u64,
);
let state = RlState::from_features(&features, health_level, rework_ratio);
let next_state = RlState::from_features(&features, health_level, rework_ratio);
orch_alerts.run_cycle(&features, &state, &next_state, 4, true, true, false);
}
let reward_clean = orch_clean.telemetry().cumulative_reward;
let reward_alerts = orch_alerts.telemetry().cumulative_reward;
assert!(
reward_clean > reward_alerts,
"{:?} on real log: reward without alerts ({:.2}) must exceed reward with 4 alerts ({:.2})",
agent_type,
reward_clean,
reward_alerts,
);
}
}
#[test]
fn test_real_log_spc_penalty_bounded() {
use wasm4pm::rl_orchestrator::compute_reward;
let penalty_3_alerts = compute_reward(0, 0, 3, true, true, false, 0);
let penalty_5_alerts = compute_reward(0, 0, 5, true, true, false, 0);
let penalty_10_alerts = compute_reward(0, 0, 10, true, true, false, 0);
assert!(
penalty_5_alerts <= penalty_3_alerts,
"5 alerts penalty ({:.2}) should be <= 3 alerts penalty ({:.2})",
penalty_5_alerts,
penalty_3_alerts,
);
assert!(
(penalty_10_alerts - penalty_5_alerts).abs() < 0.01,
"10 alerts penalty ({:.2}) should equal 5 alerts penalty ({:.2}) — both capped at -1.5",
penalty_10_alerts,
penalty_5_alerts,
);
}