1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::fighter::FighterId;
6use crate::gorilla::GorillaId;
7use crate::troop::TroopId;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case", tag = "kind")]
12pub enum PunchEvent {
13 FighterSpawned { fighter_id: FighterId, name: String },
15 FighterMessage {
17 fighter_id: FighterId,
18 bout_id: Uuid,
19 role: String,
20 content_preview: String,
21 },
22 GorillaUnleashed { gorilla_id: GorillaId, name: String },
24 GorillaPaused {
26 gorilla_id: GorillaId,
27 reason: String,
28 },
29 ToolExecuted {
31 agent_id: String,
32 tool_name: String,
33 success: bool,
34 duration_ms: u64,
35 },
36 BoutStarted {
38 bout_id: Uuid,
39 fighter_id: FighterId,
40 },
41 BoutEnded {
43 bout_id: Uuid,
44 fighter_id: FighterId,
45 messages_exchanged: u64,
46 },
47 ComboTriggered {
49 combo_name: String,
50 triggered_by: String,
51 },
52 TroopFormed {
54 troop_id: TroopId,
55 name: String,
56 member_count: usize,
57 },
58 TroopDisbanded { troop_id: TroopId, name: String },
60 Error { source: String, message: String },
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct EventPayload {
67 pub id: Uuid,
69 pub timestamp: DateTime<Utc>,
71 pub event: PunchEvent,
73 pub correlation_id: Option<Uuid>,
75}
76
77impl EventPayload {
78 pub fn new(event: PunchEvent) -> Self {
80 Self {
81 id: Uuid::new_v4(),
82 timestamp: Utc::now(),
83 event,
84 correlation_id: None,
85 }
86 }
87
88 pub fn with_correlation(mut self, correlation_id: Uuid) -> Self {
90 self.correlation_id = Some(correlation_id);
91 self
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_fighter_spawned_serde() {
101 let event = PunchEvent::FighterSpawned {
102 fighter_id: FighterId(Uuid::nil()),
103 name: "TestFighter".to_string(),
104 };
105 let json = serde_json::to_string(&event).expect("serialize");
106 assert!(json.contains("\"kind\":\"fighter_spawned\""));
107 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
108 match deser {
109 PunchEvent::FighterSpawned { name, .. } => assert_eq!(name, "TestFighter"),
110 _ => panic!("wrong variant"),
111 }
112 }
113
114 #[test]
115 fn test_fighter_message_serde() {
116 let event = PunchEvent::FighterMessage {
117 fighter_id: FighterId(Uuid::nil()),
118 bout_id: Uuid::nil(),
119 role: "user".to_string(),
120 content_preview: "Hello".to_string(),
121 };
122 let json = serde_json::to_string(&event).expect("serialize");
123 assert!(json.contains("\"kind\":\"fighter_message\""));
124 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
125 match deser {
126 PunchEvent::FighterMessage {
127 content_preview, ..
128 } => assert_eq!(content_preview, "Hello"),
129 _ => panic!("wrong variant"),
130 }
131 }
132
133 #[test]
134 fn test_gorilla_unleashed_serde() {
135 let event = PunchEvent::GorillaUnleashed {
136 gorilla_id: GorillaId(Uuid::nil()),
137 name: "AlphaGorilla".to_string(),
138 };
139 let json = serde_json::to_string(&event).expect("serialize");
140 assert!(json.contains("\"kind\":\"gorilla_unleashed\""));
141 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
142 match deser {
143 PunchEvent::GorillaUnleashed { name, .. } => assert_eq!(name, "AlphaGorilla"),
144 _ => panic!("wrong variant"),
145 }
146 }
147
148 #[test]
149 fn test_tool_executed_serde() {
150 let event = PunchEvent::ToolExecuted {
151 agent_id: "agent-1".to_string(),
152 tool_name: "web_fetch".to_string(),
153 success: true,
154 duration_ms: 150,
155 };
156 let json = serde_json::to_string(&event).expect("serialize");
157 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
158 match deser {
159 PunchEvent::ToolExecuted {
160 success,
161 duration_ms,
162 ..
163 } => {
164 assert!(success);
165 assert_eq!(duration_ms, 150);
166 }
167 _ => panic!("wrong variant"),
168 }
169 }
170
171 #[test]
172 fn test_bout_started_serde() {
173 let event = PunchEvent::BoutStarted {
174 bout_id: Uuid::nil(),
175 fighter_id: FighterId(Uuid::nil()),
176 };
177 let json = serde_json::to_string(&event).expect("serialize");
178 assert!(json.contains("\"kind\":\"bout_started\""));
179 }
180
181 #[test]
182 fn test_bout_ended_serde() {
183 let event = PunchEvent::BoutEnded {
184 bout_id: Uuid::nil(),
185 fighter_id: FighterId(Uuid::nil()),
186 messages_exchanged: 42,
187 };
188 let json = serde_json::to_string(&event).expect("serialize");
189 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
190 match deser {
191 PunchEvent::BoutEnded {
192 messages_exchanged, ..
193 } => assert_eq!(messages_exchanged, 42),
194 _ => panic!("wrong variant"),
195 }
196 }
197
198 #[test]
199 fn test_combo_triggered_serde() {
200 let event = PunchEvent::ComboTriggered {
201 combo_name: "deploy-pipeline".to_string(),
202 triggered_by: "admin".to_string(),
203 };
204 let json = serde_json::to_string(&event).expect("serialize");
205 assert!(json.contains("\"kind\":\"combo_triggered\""));
206 }
207
208 #[test]
209 fn test_error_event_serde() {
210 let event = PunchEvent::Error {
211 source: "kernel".to_string(),
212 message: "out of memory".to_string(),
213 };
214 let json = serde_json::to_string(&event).expect("serialize");
215 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
216 match deser {
217 PunchEvent::Error { source, message } => {
218 assert_eq!(source, "kernel");
219 assert_eq!(message, "out of memory");
220 }
221 _ => panic!("wrong variant"),
222 }
223 }
224
225 #[test]
226 fn test_event_payload_new() {
227 let event = PunchEvent::FighterSpawned {
228 fighter_id: FighterId(Uuid::nil()),
229 name: "Test".to_string(),
230 };
231 let payload = EventPayload::new(event);
232 assert!(payload.correlation_id.is_none());
233 assert!(payload.timestamp <= Utc::now());
234 }
235
236 #[test]
237 fn test_event_payload_with_correlation() {
238 let event = PunchEvent::Error {
239 source: "test".to_string(),
240 message: "msg".to_string(),
241 };
242 let corr_id = Uuid::new_v4();
243 let payload = EventPayload::new(event).with_correlation(corr_id);
244 assert_eq!(payload.correlation_id, Some(corr_id));
245 }
246
247 #[test]
248 fn test_event_payload_serde_roundtrip() {
249 let event = PunchEvent::ToolExecuted {
250 agent_id: "a1".to_string(),
251 tool_name: "read_file".to_string(),
252 success: false,
253 duration_ms: 0,
254 };
255 let payload = EventPayload::new(event);
256 let json = serde_json::to_string(&payload).expect("serialize");
257 let deser: EventPayload = serde_json::from_str(&json).expect("deserialize");
258 assert_eq!(deser.id, payload.id);
259 }
260
261 #[test]
262 fn test_gorilla_paused_serde() {
263 let event = PunchEvent::GorillaPaused {
264 gorilla_id: GorillaId(Uuid::nil()),
265 reason: "rate limited".to_string(),
266 };
267 let json = serde_json::to_string(&event).expect("serialize");
268 assert!(json.contains("\"kind\":\"gorilla_paused\""));
269 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
270 match deser {
271 PunchEvent::GorillaPaused { reason, .. } => assert_eq!(reason, "rate limited"),
272 _ => panic!("wrong variant"),
273 }
274 }
275
276 #[test]
277 fn test_event_clone() {
278 let event = PunchEvent::FighterSpawned {
279 fighter_id: FighterId(Uuid::nil()),
280 name: "Clone".to_string(),
281 };
282 let cloned = event.clone();
283 let json1 = serde_json::to_string(&event).unwrap();
284 let json2 = serde_json::to_string(&cloned).unwrap();
285 assert_eq!(json1, json2);
286 }
287}