use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StepStatus {
Passed,
Failed,
Skipped,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TestEvent {
RunStarted {
total_tests: u32,
},
TestStarted {
test: String,
},
StepStarted {
test: String,
index: u32,
label: String,
},
StepFinished {
test: String,
index: u32,
label: String,
status: StepStatus,
duration_ms: u64,
message: String,
diagnostics: Option<String>,
screenshot: Option<String>,
},
LlmCallStarted {
test: String,
index: u32,
endpoint: String,
model: String,
purpose: String,
},
LlmCallFinished {
test: String,
index: u32,
endpoint: String,
model: String,
purpose: String,
ok: bool,
duration_ms: u64,
input_tokens: u64,
output_tokens: u64,
cached_input_tokens: u64,
cache_creation_input_tokens: u64,
cost: f64,
error: Option<String>,
},
TestFinished {
test: String,
passed: u32,
failed: u32,
skipped: u32,
duration_ms: u64,
cost: f64,
tokens: u64,
input_tokens: u64,
output_tokens: u64,
cached_input_tokens: u64,
cache_creation_input_tokens: u64,
models: Vec<String>,
calls: u64,
},
RunFinished {
tests_passed: u32,
tests_failed: u32,
steps_passed: u32,
steps_failed: u32,
steps_skipped: u32,
total_cost: f64,
total_tokens: u64,
total_input_tokens: u64,
total_output_tokens: u64,
total_cached_input_tokens: u64,
total_cache_creation_input_tokens: u64,
models: Vec<String>,
total_calls: u64,
},
Warning {
message: String,
},
}
#[cfg(test)]
mod tests {
use super::{StepStatus, TestEvent};
#[test]
fn test_step_status_serializes_snake_case() {
assert_eq!(
serde_json::to_value(StepStatus::Passed).unwrap(),
serde_json::json!("passed")
);
assert_eq!(
serde_json::to_value(StepStatus::Failed).unwrap(),
serde_json::json!("failed")
);
assert_eq!(
serde_json::to_value(StepStatus::Skipped).unwrap(),
serde_json::json!("skipped")
);
}
#[test]
fn test_event_serializes_with_type_tag() {
let event = TestEvent::StepFinished {
test: "login".into(),
index: 2,
label: "[click] the button".into(),
status: StepStatus::Failed,
duration_ms: 1234,
message: "element #btn not found".into(),
diagnostics: Some(" │ url: http://x".into()),
screenshot: Some("artifacts/x.png".into()),
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "step_finished");
assert_eq!(value["test"], "login");
assert_eq!(value["index"], 2);
assert_eq!(value["label"], "[click] the button");
assert_eq!(value["status"], "failed");
assert_eq!(value["duration_ms"], 1234);
assert_eq!(value["message"], "element #btn not found");
assert_eq!(value["diagnostics"], " │ url: http://x");
assert_eq!(value["screenshot"], "artifacts/x.png");
}
#[test]
fn test_run_started_shape() {
let value = serde_json::to_value(TestEvent::RunStarted { total_tests: 3 }).unwrap();
assert_eq!(value["type"], "run_started");
assert_eq!(value["total_tests"], 3);
}
#[test]
fn test_llm_call_finished_shape() {
let value = serde_json::to_value(TestEvent::LlmCallFinished {
test: "login".into(),
index: 0,
endpoint: "default".into(),
model: "deepseek".into(),
purpose: "targeting".into(),
ok: false,
duration_ms: 900,
input_tokens: 100,
output_tokens: 0,
cached_input_tokens: 40,
cache_creation_input_tokens: 7,
cost: 0.0012,
error: Some("HTTP 429: slow down".into()),
})
.unwrap();
assert_eq!(value["type"], "llm_call_finished");
assert_eq!(value["endpoint"], "default");
assert_eq!(value["model"], "deepseek");
assert_eq!(value["purpose"], "targeting");
assert_eq!(value["ok"], false);
assert_eq!(value["cached_input_tokens"], 40);
assert_eq!(value["cache_creation_input_tokens"], 7);
assert_eq!(value["cost"], 0.0012);
assert_eq!(value["error"], "HTTP 429: slow down");
}
#[test]
fn test_run_finished_shape() {
let value = serde_json::to_value(TestEvent::RunFinished {
tests_passed: 2,
tests_failed: 1,
steps_passed: 9,
steps_failed: 1,
steps_skipped: 2,
total_cost: 0.05,
total_tokens: 5000,
total_input_tokens: 4000,
total_output_tokens: 1000,
total_cached_input_tokens: 500,
total_cache_creation_input_tokens: 250,
models: vec!["deepseek".into()],
total_calls: 7,
})
.unwrap();
assert_eq!(value["type"], "run_finished");
assert_eq!(value["tests_passed"], 2);
assert_eq!(value["steps_failed"], 1);
assert_eq!(value["total_cost"], 0.05);
assert_eq!(value["total_input_tokens"], 4000);
assert_eq!(value["total_cached_input_tokens"], 500);
assert_eq!(value["total_cache_creation_input_tokens"], 250);
assert_eq!(value["models"][0], "deepseek");
}
#[test]
fn test_warning_shape() {
let value = serde_json::to_value(TestEvent::Warning {
message: "budget soft".into(),
})
.unwrap();
assert_eq!(value["type"], "warning");
assert_eq!(value["message"], "budget soft");
}
}