Skip to main content

punch_types/
event.rs

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/// Events emitted by the Punch system.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case", tag = "kind")]
12pub enum PunchEvent {
13    /// A new Fighter has been spawned.
14    FighterSpawned { fighter_id: FighterId, name: String },
15    /// A Fighter sent or received a message.
16    FighterMessage {
17        fighter_id: FighterId,
18        bout_id: Uuid,
19        role: String,
20        content_preview: String,
21    },
22    /// A Gorilla has been unleashed (activated).
23    GorillaUnleashed { gorilla_id: GorillaId, name: String },
24    /// A Gorilla has been paused.
25    GorillaPaused {
26        gorilla_id: GorillaId,
27        reason: String,
28    },
29    /// A tool (move) was executed.
30    ToolExecuted {
31        agent_id: String,
32        tool_name: String,
33        success: bool,
34        duration_ms: u64,
35    },
36    /// A bout (session/conversation) has started.
37    BoutStarted {
38        bout_id: Uuid,
39        fighter_id: FighterId,
40    },
41    /// A bout has ended.
42    BoutEnded {
43        bout_id: Uuid,
44        fighter_id: FighterId,
45        messages_exchanged: u64,
46    },
47    /// A combo (workflow) has been triggered.
48    ComboTriggered {
49        combo_name: String,
50        triggered_by: String,
51    },
52    /// A troop has been formed.
53    TroopFormed {
54        troop_id: TroopId,
55        name: String,
56        member_count: usize,
57    },
58    /// A troop has been disbanded.
59    TroopDisbanded { troop_id: TroopId, name: String },
60    /// An error occurred in the system.
61    Error { source: String, message: String },
62}
63
64/// A timestamped event with metadata.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct EventPayload {
67    /// Unique identifier for this event instance.
68    pub id: Uuid,
69    /// When the event occurred.
70    pub timestamp: DateTime<Utc>,
71    /// The event data.
72    pub event: PunchEvent,
73    /// Optional correlation ID for tracing related events.
74    pub correlation_id: Option<Uuid>,
75}
76
77impl EventPayload {
78    /// Create a new `EventPayload` with the current timestamp.
79    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    /// Attach a correlation ID for event tracing.
89    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}