1use chrono::{DateTime, Utc};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use crate::ids::{EventId, TraceId};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "snake_case")]
25pub enum TraceStatus {
26 Open,
28 Closed,
30 Quarantined,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40pub struct Trace {
41 pub id: TraceId,
43 pub schema_version: u16,
45 pub opened_at: DateTime<Utc>,
47 pub closed_at: Option<DateTime<Utc>>,
50 pub event_ids: Vec<EventId>,
53 pub trace_type: String,
56 pub status: TraceStatus,
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::SCHEMA_VERSION;
64 use chrono::TimeZone;
65
66 fn fixture_trace() -> Trace {
67 Trace {
68 id: "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap(),
69 schema_version: SCHEMA_VERSION,
70 opened_at: Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 0).unwrap(),
71 closed_at: Some(Utc.with_ymd_and_hms(2026, 1, 1, 12, 5, 0).unwrap()),
72 event_ids: vec![
73 "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap(),
74 "evt_01ARZ3NDEKTSV4RRFFQ69G5FAW".parse().unwrap(),
75 ],
76 trace_type: "agent_run".into(),
77 status: TraceStatus::Closed,
78 }
79 }
80
81 #[test]
82 fn trace_serde_round_trip() {
83 let t = fixture_trace();
84 let j = serde_json::to_value(&t).expect("serialize");
85 let back: Trace = serde_json::from_value(j.clone()).expect("deserialize");
86 assert_eq!(t, back);
87
88 let obj = j.as_object().expect("trace serializes as object");
89 for k in [
90 "id",
91 "schema_version",
92 "opened_at",
93 "closed_at",
94 "event_ids",
95 "trace_type",
96 "status",
97 ] {
98 assert!(obj.contains_key(k), "trace JSON missing field `{k}`");
99 }
100 assert_eq!(obj["status"], serde_json::json!("closed"));
101 }
102
103 #[test]
104 fn trace_status_wire_strings() {
105 assert_eq!(
106 serde_json::to_value(TraceStatus::Open).unwrap(),
107 serde_json::json!("open")
108 );
109 assert_eq!(
110 serde_json::to_value(TraceStatus::Closed).unwrap(),
111 serde_json::json!("closed")
112 );
113 assert_eq!(
114 serde_json::to_value(TraceStatus::Quarantined).unwrap(),
115 serde_json::json!("quarantined")
116 );
117 }
118
119 #[test]
120 fn open_trace_round_trips_with_null_closed_at() {
121 let mut t = fixture_trace();
122 t.status = TraceStatus::Open;
123 t.closed_at = None;
124 let j = serde_json::to_value(&t).unwrap();
125 assert_eq!(j["closed_at"], serde_json::Value::Null);
126 let back: Trace = serde_json::from_value(j).unwrap();
127 assert_eq!(t, back);
128 }
129}