execution_engine/
events.rs

1//! Event system for Execution Engine
2//!
3//! Pluggable event handlers for real-time execution updates.
4//! See docs/types.md for event type reference.
5
6use crate::types::{ExecutionResult, ExecutionStatus};
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12/// Events emitted during execution
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(tag = "event_type", rename_all = "snake_case")]
15pub enum ExecutionEvent {
16    /// Execution started
17    Started {
18        execution_id: Uuid,
19        command: String,
20        timestamp: DateTime<Utc>,
21    },
22
23    /// Standard output line
24    Stdout {
25        execution_id: Uuid,
26        line: String,
27        timestamp: DateTime<Utc>,
28    },
29
30    /// Standard error line
31    Stderr {
32        execution_id: Uuid,
33        line: String,
34        timestamp: DateTime<Utc>,
35    },
36
37    /// Execution completed
38    Completed {
39        execution_id: Uuid,
40        result: ExecutionResult,
41        timestamp: DateTime<Utc>,
42    },
43
44    /// Execution failed
45    Failed {
46        execution_id: Uuid,
47        error: String,
48        timestamp: DateTime<Utc>,
49    },
50
51    /// Execution cancelled
52    Cancelled {
53        execution_id: Uuid,
54        timestamp: DateTime<Utc>,
55    },
56
57    /// Execution timeout
58    Timeout {
59        execution_id: Uuid,
60        timeout_ms: u64,
61        timestamp: DateTime<Utc>,
62    },
63
64    /// Plan progress update
65    Progress {
66        plan_id: Uuid,
67        completed: usize,
68        total: usize,
69        current_command: Option<String>,
70        timestamp: DateTime<Utc>,
71    },
72
73    /// Status changed
74    StatusChanged {
75        execution_id: Uuid,
76        old_status: ExecutionStatus,
77        new_status: ExecutionStatus,
78        timestamp: DateTime<Utc>,
79    },
80}
81
82impl ExecutionEvent {
83    /// Get the execution ID for this event (if applicable)
84    #[must_use]
85    pub fn execution_id(&self) -> Option<Uuid> {
86        match self {
87            ExecutionEvent::Started { execution_id, .. }
88            | ExecutionEvent::Stdout { execution_id, .. }
89            | ExecutionEvent::Stderr { execution_id, .. }
90            | ExecutionEvent::Completed { execution_id, .. }
91            | ExecutionEvent::Failed { execution_id, .. }
92            | ExecutionEvent::Cancelled { execution_id, .. }
93            | ExecutionEvent::Timeout { execution_id, .. }
94            | ExecutionEvent::StatusChanged { execution_id, .. } => Some(*execution_id),
95            ExecutionEvent::Progress { .. } => None,
96        }
97    }
98
99    /// Get the plan ID for this event (if applicable)
100    #[must_use]
101    pub fn plan_id(&self) -> Option<Uuid> {
102        match self {
103            ExecutionEvent::Progress { plan_id, .. } => Some(*plan_id),
104            _ => None,
105        }
106    }
107
108    /// Get the timestamp for this event
109    #[must_use]
110    pub fn timestamp(&self) -> DateTime<Utc> {
111        match self {
112            ExecutionEvent::Started { timestamp, .. }
113            | ExecutionEvent::Stdout { timestamp, .. }
114            | ExecutionEvent::Stderr { timestamp, .. }
115            | ExecutionEvent::Completed { timestamp, .. }
116            | ExecutionEvent::Failed { timestamp, .. }
117            | ExecutionEvent::Cancelled { timestamp, .. }
118            | ExecutionEvent::Timeout { timestamp, .. }
119            | ExecutionEvent::Progress { timestamp, .. }
120            | ExecutionEvent::StatusChanged { timestamp, .. } => *timestamp,
121        }
122    }
123
124    /// Check if this is a terminal event (execution finished)
125    #[must_use]
126    pub fn is_terminal(&self) -> bool {
127        matches!(
128            self,
129            ExecutionEvent::Completed { .. }
130                | ExecutionEvent::Failed { .. }
131                | ExecutionEvent::Cancelled { .. }
132                | ExecutionEvent::Timeout { .. }
133        )
134    }
135
136    /// Get event type name
137    #[must_use]
138    pub fn event_type_name(&self) -> &str {
139        match self {
140            ExecutionEvent::Started { .. } => "started",
141            ExecutionEvent::Stdout { .. } => "stdout",
142            ExecutionEvent::Stderr { .. } => "stderr",
143            ExecutionEvent::Completed { .. } => "completed",
144            ExecutionEvent::Failed { .. } => "failed",
145            ExecutionEvent::Cancelled { .. } => "cancelled",
146            ExecutionEvent::Timeout { .. } => "timeout",
147            ExecutionEvent::Progress { .. } => "progress",
148            ExecutionEvent::StatusChanged { .. } => "status_changed",
149        }
150    }
151}
152
153/// Event handler trait
154///
155/// Implement this trait to receive execution events.
156#[async_trait]
157pub trait EventHandler: Send + Sync {
158    /// Handle an execution event
159    async fn handle_event(&self, event: ExecutionEvent);
160
161    /// Handle error in event processing
162    async fn handle_error(&self, error: String) {
163        eprintln!("Event handler error: {error}");
164    }
165}
166
167/// No-op event handler (does nothing)
168pub struct NoopEventHandler;
169
170#[async_trait]
171impl EventHandler for NoopEventHandler {
172    async fn handle_event(&self, _event: ExecutionEvent) {
173        // Do nothing
174    }
175}
176
177/// Logging event handler (logs to stderr)
178pub struct LoggingEventHandler {
179    pub verbose: bool,
180}
181
182impl LoggingEventHandler {
183    #[must_use]
184    pub fn new(verbose: bool) -> Self {
185        Self { verbose }
186    }
187}
188
189#[async_trait]
190impl EventHandler for LoggingEventHandler {
191    async fn handle_event(&self, event: ExecutionEvent) {
192        if self.verbose {
193            eprintln!(
194                "[{}] Event: {} - {:?}",
195                event.timestamp().format("%Y-%m-%d %H:%M:%S"),
196                event.event_type_name(),
197                event
198            );
199        } else {
200            match event {
201                ExecutionEvent::Started { command, .. } => {
202                    eprintln!("Started: {command}");
203                }
204                ExecutionEvent::Completed { execution_id, .. } => {
205                    eprintln!("Completed: {execution_id}");
206                }
207                ExecutionEvent::Failed { error, .. } => {
208                    eprintln!("Failed: {error}");
209                }
210                _ => {}
211            }
212        }
213    }
214}
215
216/// Multi-handler that broadcasts to multiple handlers
217pub struct MultiEventHandler {
218    handlers: Vec<Box<dyn EventHandler>>,
219}
220
221impl MultiEventHandler {
222    #[must_use]
223    pub fn new() -> Self {
224        Self {
225            handlers: Vec::new(),
226        }
227    }
228
229    pub fn add_handler(&mut self, handler: Box<dyn EventHandler>) {
230        self.handlers.push(handler);
231    }
232}
233
234impl Default for MultiEventHandler {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240#[async_trait]
241impl EventHandler for MultiEventHandler {
242    async fn handle_event(&self, event: ExecutionEvent) {
243        for handler in &self.handlers {
244            handler.handle_event(event.clone()).await;
245        }
246    }
247
248    async fn handle_error(&self, error: String) {
249        for handler in &self.handlers {
250            handler.handle_error(error.clone()).await;
251        }
252    }
253}
254
255// ============================================================================
256// Tests
257// ============================================================================
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::types::ExecutionResult;
263    use std::time::Duration;
264
265    #[test]
266    fn test_execution_event_execution_id() {
267        let id = Uuid::new_v4();
268        let event = ExecutionEvent::Started {
269            execution_id: id,
270            command: "test".to_string(),
271            timestamp: Utc::now(),
272        };
273        assert_eq!(event.execution_id(), Some(id));
274
275        let plan_id = Uuid::new_v4();
276        let event = ExecutionEvent::Progress {
277            plan_id,
278            completed: 1,
279            total: 5,
280            current_command: None,
281            timestamp: Utc::now(),
282        };
283        assert_eq!(event.execution_id(), None);
284        assert_eq!(event.plan_id(), Some(plan_id));
285    }
286
287    #[test]
288    fn test_execution_event_is_terminal() {
289        let id = Uuid::new_v4();
290
291        let event = ExecutionEvent::Started {
292            execution_id: id,
293            command: "test".to_string(),
294            timestamp: Utc::now(),
295        };
296        assert!(!event.is_terminal());
297
298        let event = ExecutionEvent::Stdout {
299            execution_id: id,
300            line: "output".to_string(),
301            timestamp: Utc::now(),
302        };
303        assert!(!event.is_terminal());
304
305        let result = ExecutionResult {
306            id,
307            status: ExecutionStatus::Completed,
308            success: true,
309            exit_code: 0,
310            stdout: "".to_string(),
311            stderr: "".to_string(),
312            duration: Duration::from_secs(1),
313            started_at: Utc::now(),
314            completed_at: Some(Utc::now()),
315            error: None,
316        };
317
318        let event = ExecutionEvent::Completed {
319            execution_id: id,
320            result,
321            timestamp: Utc::now(),
322        };
323        assert!(event.is_terminal());
324
325        let event = ExecutionEvent::Failed {
326            execution_id: id,
327            error: "error".to_string(),
328            timestamp: Utc::now(),
329        };
330        assert!(event.is_terminal());
331
332        let event = ExecutionEvent::Cancelled {
333            execution_id: id,
334            timestamp: Utc::now(),
335        };
336        assert!(event.is_terminal());
337
338        let event = ExecutionEvent::Timeout {
339            execution_id: id,
340            timeout_ms: 5000,
341            timestamp: Utc::now(),
342        };
343        assert!(event.is_terminal());
344    }
345
346    #[test]
347    fn test_event_type_name() {
348        let id = Uuid::new_v4();
349
350        let event = ExecutionEvent::Started {
351            execution_id: id,
352            command: "test".to_string(),
353            timestamp: Utc::now(),
354        };
355        assert_eq!(event.event_type_name(), "started");
356
357        let event = ExecutionEvent::Stdout {
358            execution_id: id,
359            line: "output".to_string(),
360            timestamp: Utc::now(),
361        };
362        assert_eq!(event.event_type_name(), "stdout");
363
364        let event = ExecutionEvent::StatusChanged {
365            execution_id: id,
366            old_status: ExecutionStatus::Pending,
367            new_status: ExecutionStatus::Running,
368            timestamp: Utc::now(),
369        };
370        assert_eq!(event.event_type_name(), "status_changed");
371    }
372
373    #[tokio::test]
374    async fn test_noop_event_handler() {
375        let handler = NoopEventHandler;
376        let event = ExecutionEvent::Started {
377            execution_id: Uuid::new_v4(),
378            command: "test".to_string(),
379            timestamp: Utc::now(),
380        };
381        handler.handle_event(event).await;
382        // Should not panic
383    }
384
385    #[tokio::test]
386    async fn test_logging_event_handler() {
387        let handler = LoggingEventHandler::new(false);
388        let event = ExecutionEvent::Started {
389            execution_id: Uuid::new_v4(),
390            command: "test".to_string(),
391            timestamp: Utc::now(),
392        };
393        handler.handle_event(event).await;
394        // Should log to stderr
395    }
396
397    #[tokio::test]
398    async fn test_multi_event_handler() {
399        let mut multi = MultiEventHandler::new();
400        multi.add_handler(Box::new(NoopEventHandler));
401        multi.add_handler(Box::new(LoggingEventHandler::new(false)));
402
403        let event = ExecutionEvent::Started {
404            execution_id: Uuid::new_v4(),
405            command: "test".to_string(),
406            timestamp: Utc::now(),
407        };
408        multi.handle_event(event).await;
409        // Should broadcast to all handlers
410    }
411
412    #[test]
413    fn test_event_serialization() {
414        let event = ExecutionEvent::Started {
415            execution_id: Uuid::new_v4(),
416            command: "echo hello".to_string(),
417            timestamp: Utc::now(),
418        };
419
420        let json = serde_json::to_string(&event).unwrap();
421        assert!(json.contains("started"));
422        assert!(json.contains("echo hello"));
423
424        let deserialized: ExecutionEvent = serde_json::from_str(&json).unwrap();
425        match deserialized {
426            ExecutionEvent::Started { command, .. } => {
427                assert_eq!(command, "echo hello");
428            }
429            _ => panic!("Wrong event type"),
430        }
431    }
432}