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 McpServerStarted { server_name: String },
62 McpServerStopped { server_name: String },
64 HeartbeatExecuted {
66 fighter_id: FighterId,
67 task_description: String,
68 },
69 Error { source: String, message: String },
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EventPayload {
76 pub id: Uuid,
78 pub timestamp: DateTime<Utc>,
80 pub event: PunchEvent,
82 pub correlation_id: Option<Uuid>,
84}
85
86impl EventPayload {
87 pub fn new(event: PunchEvent) -> Self {
89 Self {
90 id: Uuid::new_v4(),
91 timestamp: Utc::now(),
92 event,
93 correlation_id: None,
94 }
95 }
96
97 pub fn with_correlation(mut self, correlation_id: Uuid) -> Self {
99 self.correlation_id = Some(correlation_id);
100 self
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_fighter_spawned_serde() {
110 let event = PunchEvent::FighterSpawned {
111 fighter_id: FighterId(Uuid::nil()),
112 name: "TestFighter".to_string(),
113 };
114 let json = serde_json::to_string(&event).expect("serialize");
115 assert!(json.contains("\"kind\":\"fighter_spawned\""));
116 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
117 match deser {
118 PunchEvent::FighterSpawned { name, .. } => assert_eq!(name, "TestFighter"),
119 _ => panic!("wrong variant"),
120 }
121 }
122
123 #[test]
124 fn test_fighter_message_serde() {
125 let event = PunchEvent::FighterMessage {
126 fighter_id: FighterId(Uuid::nil()),
127 bout_id: Uuid::nil(),
128 role: "user".to_string(),
129 content_preview: "Hello".to_string(),
130 };
131 let json = serde_json::to_string(&event).expect("serialize");
132 assert!(json.contains("\"kind\":\"fighter_message\""));
133 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
134 match deser {
135 PunchEvent::FighterMessage {
136 content_preview, ..
137 } => assert_eq!(content_preview, "Hello"),
138 _ => panic!("wrong variant"),
139 }
140 }
141
142 #[test]
143 fn test_gorilla_unleashed_serde() {
144 let event = PunchEvent::GorillaUnleashed {
145 gorilla_id: GorillaId(Uuid::nil()),
146 name: "AlphaGorilla".to_string(),
147 };
148 let json = serde_json::to_string(&event).expect("serialize");
149 assert!(json.contains("\"kind\":\"gorilla_unleashed\""));
150 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
151 match deser {
152 PunchEvent::GorillaUnleashed { name, .. } => assert_eq!(name, "AlphaGorilla"),
153 _ => panic!("wrong variant"),
154 }
155 }
156
157 #[test]
158 fn test_tool_executed_serde() {
159 let event = PunchEvent::ToolExecuted {
160 agent_id: "agent-1".to_string(),
161 tool_name: "web_fetch".to_string(),
162 success: true,
163 duration_ms: 150,
164 };
165 let json = serde_json::to_string(&event).expect("serialize");
166 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
167 match deser {
168 PunchEvent::ToolExecuted {
169 success,
170 duration_ms,
171 ..
172 } => {
173 assert!(success);
174 assert_eq!(duration_ms, 150);
175 }
176 _ => panic!("wrong variant"),
177 }
178 }
179
180 #[test]
181 fn test_bout_started_serde() {
182 let event = PunchEvent::BoutStarted {
183 bout_id: Uuid::nil(),
184 fighter_id: FighterId(Uuid::nil()),
185 };
186 let json = serde_json::to_string(&event).expect("serialize");
187 assert!(json.contains("\"kind\":\"bout_started\""));
188 }
189
190 #[test]
191 fn test_bout_ended_serde() {
192 let event = PunchEvent::BoutEnded {
193 bout_id: Uuid::nil(),
194 fighter_id: FighterId(Uuid::nil()),
195 messages_exchanged: 42,
196 };
197 let json = serde_json::to_string(&event).expect("serialize");
198 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
199 match deser {
200 PunchEvent::BoutEnded {
201 messages_exchanged, ..
202 } => assert_eq!(messages_exchanged, 42),
203 _ => panic!("wrong variant"),
204 }
205 }
206
207 #[test]
208 fn test_combo_triggered_serde() {
209 let event = PunchEvent::ComboTriggered {
210 combo_name: "deploy-pipeline".to_string(),
211 triggered_by: "admin".to_string(),
212 };
213 let json = serde_json::to_string(&event).expect("serialize");
214 assert!(json.contains("\"kind\":\"combo_triggered\""));
215 }
216
217 #[test]
218 fn test_error_event_serde() {
219 let event = PunchEvent::Error {
220 source: "kernel".to_string(),
221 message: "out of memory".to_string(),
222 };
223 let json = serde_json::to_string(&event).expect("serialize");
224 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
225 match deser {
226 PunchEvent::Error { source, message } => {
227 assert_eq!(source, "kernel");
228 assert_eq!(message, "out of memory");
229 }
230 _ => panic!("wrong variant"),
231 }
232 }
233
234 #[test]
235 fn test_event_payload_new() {
236 let event = PunchEvent::FighterSpawned {
237 fighter_id: FighterId(Uuid::nil()),
238 name: "Test".to_string(),
239 };
240 let payload = EventPayload::new(event);
241 assert!(payload.correlation_id.is_none());
242 assert!(payload.timestamp <= Utc::now());
243 }
244
245 #[test]
246 fn test_event_payload_with_correlation() {
247 let event = PunchEvent::Error {
248 source: "test".to_string(),
249 message: "msg".to_string(),
250 };
251 let corr_id = Uuid::new_v4();
252 let payload = EventPayload::new(event).with_correlation(corr_id);
253 assert_eq!(payload.correlation_id, Some(corr_id));
254 }
255
256 #[test]
257 fn test_event_payload_serde_roundtrip() {
258 let event = PunchEvent::ToolExecuted {
259 agent_id: "a1".to_string(),
260 tool_name: "read_file".to_string(),
261 success: false,
262 duration_ms: 0,
263 };
264 let payload = EventPayload::new(event);
265 let json = serde_json::to_string(&payload).expect("serialize");
266 let deser: EventPayload = serde_json::from_str(&json).expect("deserialize");
267 assert_eq!(deser.id, payload.id);
268 }
269
270 #[test]
271 fn test_gorilla_paused_serde() {
272 let event = PunchEvent::GorillaPaused {
273 gorilla_id: GorillaId(Uuid::nil()),
274 reason: "rate limited".to_string(),
275 };
276 let json = serde_json::to_string(&event).expect("serialize");
277 assert!(json.contains("\"kind\":\"gorilla_paused\""));
278 let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
279 match deser {
280 PunchEvent::GorillaPaused { reason, .. } => assert_eq!(reason, "rate limited"),
281 _ => panic!("wrong variant"),
282 }
283 }
284
285 #[test]
286 fn test_event_clone() {
287 let event = PunchEvent::FighterSpawned {
288 fighter_id: FighterId(Uuid::nil()),
289 name: "Clone".to_string(),
290 };
291 let cloned = event.clone();
292 let json1 = serde_json::to_string(&event).unwrap();
293 let json2 = serde_json::to_string(&cloned).unwrap();
294 assert_eq!(json1, json2);
295 }
296}