1use serde::{Deserialize, Serialize};
8
9use crate::{AgentEvent, SessionState, contract::replay_trace};
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub struct ScriptedTrace {
14 pub name: String,
15 pub roster_size: usize,
16 pub events: Vec<AgentEvent>,
17}
18
19impl ScriptedTrace {
20 pub fn new(name: impl Into<String>, roster_size: usize, events: Vec<AgentEvent>) -> Self {
21 Self {
22 name: name.into(),
23 roster_size,
24 events,
25 }
26 }
27
28 pub fn replay(&self) -> SessionState {
29 replay_trace(self.roster_size, &self.events)
30 }
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
35pub struct TraceDifference {
36 pub index: usize,
37 pub left: Option<AgentEvent>,
38 pub right: Option<AgentEvent>,
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
45pub struct TraceComparison {
46 pub events_equal: bool,
47 pub final_state_equal: bool,
48 pub first_difference: Option<TraceDifference>,
49}
50
51impl TraceComparison {
52 pub fn equivalent(&self) -> bool {
53 self.events_equal && self.final_state_equal
54 }
55}
56
57pub fn compare_traces(left: &ScriptedTrace, right: &ScriptedTrace) -> TraceComparison {
58 let first_difference = left
59 .events
60 .iter()
61 .zip(&right.events)
62 .position(|(left, right)| left != right)
63 .map(|index| TraceDifference {
64 index,
65 left: left.events.get(index).cloned(),
66 right: right.events.get(index).cloned(),
67 })
68 .or_else(|| {
69 (left.events.len() != right.events.len()).then(|| {
70 let index = left.events.len().min(right.events.len());
71 TraceDifference {
72 index,
73 left: left.events.get(index).cloned(),
74 right: right.events.get(index).cloned(),
75 }
76 })
77 });
78 TraceComparison {
79 events_equal: first_difference.is_none(),
80 final_state_equal: left.roster_size == right.roster_size && left.replay() == right.replay(),
81 first_difference,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use serde_json::json;
88
89 use super::{ScriptedTrace, compare_traces};
90 use crate::{AgentCapabilities, AgentEvent, Mode, TerminalEvent, ToolStatus, ToolUpdate};
91
92 fn shared_events() -> Vec<AgentEvent> {
93 vec![
94 AgentEvent::Ready {
95 slot: 0,
96 capabilities: AgentCapabilities {
97 supports_cancel: true,
98 supports_modes: true,
99 supports_terminals: true,
100 ..AgentCapabilities::default()
101 },
102 },
103 AgentEvent::ModesReplaced {
104 slot: 0,
105 modes: vec![Mode {
106 id: "plan".into(),
107 label: "Plan".into(),
108 }],
109 current_mode: Some("plan".into()),
110 },
111 AgentEvent::Tool {
112 slot: 0,
113 update: ToolUpdate {
114 id: "tool-1".into(),
115 title: "shell".into(),
116 status: ToolStatus::Completed,
117 detail: Some("done".into()),
118 },
119 },
120 AgentEvent::Terminal {
121 slot: 0,
122 event: TerminalEvent::Output {
123 id: "terminal-1".into(),
124 text: "ok".into(),
125 },
126 },
127 AgentEvent::Text {
128 slot: 0,
129 text: "completed".into(),
130 },
131 AgentEvent::TurnComplete { slot: 0 },
132 ]
133 }
134
135 #[test]
136 fn shared_scripted_acp_and_native_traces_compare_equal() {
137 let acp = ScriptedTrace::new("acp-smoke", 1, shared_events());
138 let native = ScriptedTrace::new("native-smoke", 1, shared_events());
139 let comparison = compare_traces(&acp, &native);
140 assert!(comparison.equivalent());
141 assert!(comparison.first_difference.is_none());
142 }
143
144 #[test]
145 fn comparison_reports_first_event_difference_and_state_difference() {
146 let left = ScriptedTrace::new("left", 1, shared_events());
147 let mut right_events = shared_events();
148 right_events[4] = AgentEvent::Text {
149 slot: 0,
150 text: "different".into(),
151 };
152 let right = ScriptedTrace::new("right", 1, right_events);
153 let comparison = compare_traces(&left, &right);
154 assert!(!comparison.equivalent());
155 assert_eq!(comparison.first_difference.expect("difference").index, 4);
156 assert!(!comparison.final_state_equal);
157 }
158
159 #[test]
160 fn trace_fixture_round_trip_and_replay_are_deterministic() {
161 let fixture = ScriptedTrace::new("fixture", 1, shared_events());
162 let encoded = serde_json::to_string(&fixture).expect("serialize");
163 let decoded: ScriptedTrace = serde_json::from_str(&encoded).expect("deserialize");
164 assert_eq!(fixture, decoded);
165 let first = decoded.replay();
166 for _ in 0..10 {
167 assert_eq!(decoded.replay(), first);
168 }
169 }
170
171 #[test]
172 fn comparison_is_json_stable_for_ci_artifacts() {
173 let trace = ScriptedTrace::new("fixture", 1, shared_events());
174 let comparison = compare_traces(&trace, &trace);
175 let value = serde_json::to_value(comparison).expect("json");
176 assert_eq!(value["events_equal"], json!(true));
177 assert_eq!(value["final_state_equal"], json!(true));
178 assert_eq!(value["first_difference"], Value::Null);
179 }
180
181 use serde_json::Value;
182}