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