fabro/lib/foundation/fabro-api/tests/run_failure_round_trip.rs
2026-07-23 17:59:34 -04:00

118 lines
4 KiB
Rust

use std::any::{TypeId, type_name};
use fabro_api::types::{
Conclusion as ApiConclusion, ExecOutputTail as ApiExecOutputTail,
FailureCategory as ApiFailureCategory, FailureDetail as ApiFailureDetail,
FailureSignature as ApiFailureSignature, RunFailure as ApiRunFailure,
};
use fabro_types::{
Conclusion, ExecOutputTail, FailureCategory, FailureDetail, FailureReason, FailureSignature,
RunFailure, StageOutcome,
};
use serde::Serialize;
use serde_json::{Value, json};
#[test]
fn run_failure_family_reuses_domain_types() {
assert_same_type::<ApiConclusion, Conclusion>();
assert_same_type::<ApiRunFailure, RunFailure>();
assert_same_type::<ApiFailureDetail, FailureDetail>();
assert_same_type::<ApiFailureCategory, FailureCategory>();
assert_same_type::<ApiFailureSignature, FailureSignature>();
assert_same_type::<ApiExecOutputTail, ExecOutputTail>();
}
#[test]
fn run_failure_json_matches_openapi_shape() {
assert_json(
RunFailure {
reason: FailureReason::SandboxInitFailed,
detail: {
let mut detail = FailureDetail::new(
"Failed to initialize sandbox",
FailureCategory::TransientInfra,
);
detail.causes = vec!["connection refused".to_string()];
detail.signature =
Some(FailureSignature("init|transient_infra|docker".to_string()));
detail.exec_output_tail = Some(ExecOutputTail {
stdout: None,
stderr: Some("last stderr line".to_string()),
stdout_truncated: false,
stderr_truncated: true,
});
detail
},
},
json!({
"reason": "sandbox_init_failed",
"detail": {
"message": "Failed to initialize sandbox",
"causes": ["connection refused"],
"category": "transient_infra",
"signature": "init|transient_infra|docker",
"exec_output_tail": {
"stderr": "last stderr line",
"stderr_truncated": true
}
}
}),
);
}
#[test]
fn conclusion_json_uses_failure_object() {
assert_json(
Conclusion {
timestamp: chrono::DateTime::parse_from_rfc3339("2026-05-13T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc),
status: StageOutcome::Failed {
retry_requested: false,
},
timing: fabro_types::RunTiming::wall_only(42),
failure: Some(RunFailure {
reason: FailureReason::WorkflowError,
detail: FailureDetail::new("boom", FailureCategory::Deterministic),
}),
final_git_commit_sha: None,
stages: Vec::new(),
billing: None,
total_retries: 0,
diff: Default::default(),
},
json!({
"timestamp": "2026-05-13T12:00:00Z",
"status": "failed",
"timing": {
"wall_time_ms": 42,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"failure": {
"reason": "workflow_error",
"detail": {
"message": "boom",
"category": "deterministic"
}
},
"total_retries": 0,
"diff": {}
}),
);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should reuse {}",
type_name::<T>(),
type_name::<U>()
);
}
fn assert_json<T: Serialize>(value: T, expected: Value) {
assert_eq!(serde_json::to_value(value).unwrap(), expected);
}