Skip to main content

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            stdout_overflow_file: None,
317            stderr_overflow_file: None,
318        };
319
320        let event = ExecutionEvent::Completed {
321            execution_id: id,
322            result,
323            timestamp: Utc::now(),
324        };
325        assert!(event.is_terminal());
326
327        let event = ExecutionEvent::Failed {
328            execution_id: id,
329            error: "error".to_string(),
330            timestamp: Utc::now(),
331        };
332        assert!(event.is_terminal());
333
334        let event = ExecutionEvent::Cancelled {
335            execution_id: id,
336            timestamp: Utc::now(),
337        };
338        assert!(event.is_terminal());
339
340        let event = ExecutionEvent::Timeout {
341            execution_id: id,
342            timeout_ms: 5000,
343            timestamp: Utc::now(),
344        };
345        assert!(event.is_terminal());
346    }
347
348    #[test]
349    fn test_event_type_name() {
350        let id = Uuid::new_v4();
351
352        let event = ExecutionEvent::Started {
353            execution_id: id,
354            command: "test".to_string(),
355            timestamp: Utc::now(),
356        };
357        assert_eq!(event.event_type_name(), "started");
358
359        let event = ExecutionEvent::Stdout {
360            execution_id: id,
361            line: "output".to_string(),
362            timestamp: Utc::now(),
363        };
364        assert_eq!(event.event_type_name(), "stdout");
365
366        let event = ExecutionEvent::StatusChanged {
367            execution_id: id,
368            old_status: ExecutionStatus::Pending,
369            new_status: ExecutionStatus::Running,
370            timestamp: Utc::now(),
371        };
372        assert_eq!(event.event_type_name(), "status_changed");
373    }
374
375    #[tokio::test]
376    async fn test_noop_event_handler() {
377        let handler = NoopEventHandler;
378        let event = ExecutionEvent::Started {
379            execution_id: Uuid::new_v4(),
380            command: "test".to_string(),
381            timestamp: Utc::now(),
382        };
383        handler.handle_event(event).await;
384        // Should not panic
385    }
386
387    #[tokio::test]
388    async fn test_logging_event_handler() {
389        let handler = LoggingEventHandler::new(false);
390        let event = ExecutionEvent::Started {
391            execution_id: Uuid::new_v4(),
392            command: "test".to_string(),
393            timestamp: Utc::now(),
394        };
395        handler.handle_event(event).await;
396        // Should log to stderr
397    }
398
399    #[tokio::test]
400    async fn test_multi_event_handler() {
401        let mut multi = MultiEventHandler::new();
402        multi.add_handler(Box::new(NoopEventHandler));
403        multi.add_handler(Box::new(LoggingEventHandler::new(false)));
404
405        let event = ExecutionEvent::Started {
406            execution_id: Uuid::new_v4(),
407            command: "test".to_string(),
408            timestamp: Utc::now(),
409        };
410        multi.handle_event(event).await;
411        // Should broadcast to all handlers
412    }
413
414    #[test]
415    fn test_event_serialization() {
416        let event = ExecutionEvent::Started {
417            execution_id: Uuid::new_v4(),
418            command: "echo hello".to_string(),
419            timestamp: Utc::now(),
420        };
421
422        let json = serde_json::to_string(&event).unwrap();
423        assert!(json.contains("started"));
424        assert!(json.contains("echo hello"));
425
426        let deserialized: ExecutionEvent = serde_json::from_str(&json).unwrap();
427        match deserialized {
428            ExecutionEvent::Started { command, .. } => {
429                assert_eq!(command, "echo hello");
430            }
431            _ => panic!("Wrong event type"),
432        }
433    }
434}