use std::collections::HashSet;
use std::fs;
use wasm4pm::models::EventLog;
use wasm4pm::rl_orchestrator::compute_health_state;
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 has_activity_repetition(trace: &wasm4pm::models::Trace, activity_key: &str) -> bool {
let mut seen = HashSet::new();
for event in &trace.events {
if let Some(wasm4pm::models::AttributeValue::String(name)) =
event.attributes.get(activity_key)
{
if seen.contains(name) {
return true;
}
seen.insert(name.clone());
}
}
false
}
fn compute_rework_ratio(log: &EventLog, activity_key: &str) -> f32 {
let trace_count = log.traces.len();
if trace_count == 0 {
return 0.0;
}
let rework_count = log
.traces
.iter()
.filter(|trace| has_activity_repetition(trace, activity_key))
.count();
rework_count as f32 / trace_count as f32
}
fn count_rework_traces(log: &EventLog, activity_key: &str) -> usize {
log.traces
.iter()
.filter(|trace| has_activity_repetition(trace, activity_key))
.count()
}
#[test]
fn test_real_log_detects_rework_traces() {
let log = load_event_log_json("running-example.json");
let total_traces = log.traces.len();
let rework_traces = count_rework_traces(&log, "activity");
assert!(
rework_traces > 0,
"running-example.json should have at least 1 trace with rework (activity repetition), \
but found 0 out of {} traces",
total_traces,
);
let rework_ratio = compute_rework_ratio(&log, "activity");
assert!(
rework_ratio > 0.0,
"Rework ratio should be positive for running-example.json, got {}",
rework_ratio,
);
}
#[test]
fn test_single_activity_trace_no_rework() {
let json = r#"{
"attributes": {},
"traces": [{
"attributes": {"case:concept:name": {"tag": "String", "value": "1"}},
"events": [
{"attributes": {"activity": {"tag": "String", "value": "A"}}},
{"attributes": {"activity": {"tag": "String", "value": "B"}}},
{"attributes": {"activity": {"tag": "String", "value": "C"}}}
]
}]
}"#;
let log: EventLog = serde_json::from_str(json).unwrap();
assert_eq!(count_rework_traces(&log, "activity"), 0);
assert_eq!(compute_rework_ratio(&log, "activity"), 0.0);
}
#[test]
fn test_loop_trace_detects_rework() {
let json = r#"{
"attributes": {},
"traces": [{
"attributes": {"case:concept:name": {"tag": "String", "value": "1"}},
"events": [
{"attributes": {"activity": {"tag": "String", "value": "register request"}}},
{"attributes": {"activity": {"tag": "String", "value": "examine thoroughly"}}},
{"attributes": {"activity": {"tag": "String", "value": "check ticket"}}},
{"attributes": {"activity": {"tag": "String", "value": "decide"}}},
{"attributes": {"activity": {"tag": "String", "value": "reject request"}}},
{"attributes": {"activity": {"tag": "String", "value": "examine thoroughly"}}},
{"attributes": {"activity": {"tag": "String", "value": "check ticket"}}}
]
}]
}"#;
let log: EventLog = serde_json::from_str(json).unwrap();
assert_eq!(
count_rework_traces(&log, "activity"),
1,
"Loop trace should be detected"
);
assert!(
compute_rework_ratio(&log, "activity") > 0.0,
"Loop trace should produce positive rework ratio"
);
}
#[test]
fn test_real_log_health_state_from_actual_metrics() {
let log = load_event_log_json("running-example.json");
let event_count: u64 = log.traces.iter().map(|t| t.events.len() as u64).sum();
let trace_count = log.traces.len() as u64;
let mut activity_set = HashSet::new();
for trace in &log.traces {
for event in &trace.events {
if let Some(wasm4pm::models::AttributeValue::String(name)) =
event.attributes.get("activity")
{
activity_set.insert(name.clone());
}
}
}
let unique_activities = activity_set.len() as u64;
let health = compute_health_state(event_count, trace_count, unique_activities);
assert_eq!(
health, 0,
"Real running-example log should be Normal (health=0), got {}\n\
events={}, traces={}, activities={}",
health, event_count, trace_count, unique_activities,
);
}
#[test]
fn test_health_degrades_for_trivial_log() {
let json = r#"{
"attributes": {},
"traces": [{
"attributes": {"case:concept:name": {"tag": "String", "value": "1"}},
"events": [
{"attributes": {"activity": {"tag": "String", "value": "A"}}},
{"attributes": {"activity": {"tag": "String", "value": "A"}}},
{"attributes": {"activity": {"tag": "String", "value": "A"}}}
]
}]
}"#;
let _log: EventLog = serde_json::from_str(json).unwrap();
let health = compute_health_state(3, 1, 1);
assert_eq!(
health, 2,
"Trivial log (1 activity, < 5 events) should be Degraded"
);
}
#[test]
fn test_health_critical_for_no_traces() {
assert_eq!(
compute_health_state(10, 0, 3),
3,
"No traces should be Critical"
);
}
#[test]
fn test_health_failed_for_empty_log() {
assert_eq!(
compute_health_state(0, 0, 0),
4,
"Empty log should be Failed"
);
assert_eq!(
compute_health_state(5, 5, 0),
4,
"No activities should be Failed"
);
}
#[test]
fn test_real_log_rework_ratio_produces_distinct_rl_states() {
let log = load_event_log_json("running-example.json");
let (event_count, trace_count, unique_activities, rework_ratio) = (
log.traces
.iter()
.map(|t| t.events.len() as u64)
.sum::<u64>(),
log.traces.len() as u64,
{
let mut s = HashSet::new();
for trace in &log.traces {
for event in &trace.events {
if let Some(wasm4pm::models::AttributeValue::String(n)) =
event.attributes.get("activity")
{
s.insert(n.clone());
}
}
}
s.len() as u64
},
compute_rework_ratio(&log, "activity"),
);
let features = [
(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),
0.0,
0.0,
1.0,
1.0,
0.0,
];
let health_level = 0u8;
let state_zero = RlState::from_features(&features, health_level, 0.0);
let state_real = RlState::from_features(&features, health_level, rework_ratio);
if rework_ratio > 0.0 {
assert_ne!(
state_zero.rework_ratio_q, state_real.rework_ratio_q,
"RlState.rework_ratio_q should differ when rework_ratio changes \
(0.0 vs {} from real log)",
rework_ratio,
);
} else {
let state_with_rework = RlState::from_features(&features, health_level, 0.5);
assert_ne!(
state_zero.rework_ratio_q, state_with_rework.rework_ratio_q,
"RlState.rework_ratio_q with 0.0 should differ from 0.5",
);
}
}