Skip to main content

nika_event/
emitter.rs

1//! EventEmitter Trait - abstraction for event emission
2//!
3//! Enables dependency injection for testing - real emitters in production,
4//! NoopEmitter or MockEmitter in tests.
5//!
6//! Key types:
7//! - `EventEmitter`: Trait for emitting events
8//! - `NoopEmitter`: Zero-cost no-op implementation for tests
9
10use super::log::{EventKind, EventLog};
11
12/// Trait for emitting events during workflow execution
13///
14/// Enables dependency injection: real EventLog in production,
15/// NoopEmitter or custom mock in tests.
16pub trait EventEmitter: Send + Sync {
17    /// Emit an event and return its ID
18    fn emit(&self, kind: EventKind) -> u64;
19}
20
21/// Implement EventEmitter for EventLog (the real implementation)
22impl EventEmitter for EventLog {
23    fn emit(&self, kind: EventKind) -> u64 {
24        EventLog::emit(self, kind)
25    }
26}
27
28/// No-op emitter for testing (zero allocation, always returns 0)
29#[derive(Debug, Clone, Default)]
30pub struct NoopEmitter;
31
32impl NoopEmitter {
33    /// Create a new NoopEmitter
34    pub fn new() -> Self {
35        Self
36    }
37}
38
39impl EventEmitter for NoopEmitter {
40    fn emit(&self, _kind: EventKind) -> u64 {
41        0 // Always return 0, do nothing
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use serde_json::json;
49    use std::sync::Arc;
50
51    /// Use actual package version in tests to avoid version drift
52    const TEST_VERSION: &str = env!("CARGO_PKG_VERSION");
53
54    // ═══════════════════════════════════════════════════════════════
55    // EventEmitter trait tests
56    // ═══════════════════════════════════════════════════════════════
57
58    #[test]
59    fn event_emitter_trait_is_object_safe() {
60        // Verify the trait can be used as a trait object
61        fn accepts_emitter(_: &dyn EventEmitter) {}
62
63        let log = EventLog::new();
64        accepts_emitter(&log);
65
66        let noop = NoopEmitter::new();
67        accepts_emitter(&noop);
68    }
69
70    #[test]
71    fn event_emitter_trait_works_with_arc() {
72        // Verify it works with Arc (required for concurrent access)
73        let emitter: Arc<dyn EventEmitter> = Arc::new(EventLog::new());
74        let id = emitter.emit(EventKind::WorkflowStarted {
75            task_count: 1,
76            generation_id: "test".to_string(),
77            workflow_hash: "hash".to_string(),
78            nika_version: TEST_VERSION.to_string(),
79        });
80        assert_eq!(id, 0); // First event
81    }
82
83    // ═══════════════════════════════════════════════════════════════
84    // EventLog as EventEmitter tests
85    // ═══════════════════════════════════════════════════════════════
86
87    #[test]
88    fn eventlog_implements_emitter() {
89        let log = EventLog::new();
90        let emitter: &dyn EventEmitter = &log;
91
92        let id = emitter.emit(EventKind::TaskStarted {
93            task_id: Arc::from("test_task"),
94            verb: "infer".into(),
95            inputs: json!({}),
96        });
97
98        assert_eq!(id, 0);
99        assert_eq!(log.len(), 1);
100    }
101
102    #[test]
103    fn eventlog_emitter_returns_monotonic_ids() {
104        let log = EventLog::new();
105        let emitter: &dyn EventEmitter = &log;
106
107        let id1 = emitter.emit(EventKind::WorkflowStarted {
108            task_count: 2,
109            generation_id: "gen1".to_string(),
110            workflow_hash: "hash1".to_string(),
111            nika_version: TEST_VERSION.to_string(),
112        });
113        let id2 = emitter.emit(EventKind::TaskStarted {
114            task_id: Arc::from("task1"),
115            verb: "infer".into(),
116            inputs: json!({}),
117        });
118        let id3 = emitter.emit(EventKind::TaskCompleted {
119            task_id: Arc::from("task1"),
120            output: Arc::new(json!("done")),
121            duration_ms: 100,
122        });
123
124        assert_eq!(id1, 0);
125        assert_eq!(id2, 1);
126        assert_eq!(id3, 2);
127    }
128
129    // ═══════════════════════════════════════════════════════════════
130    // NoopEmitter tests
131    // ═══════════════════════════════════════════════════════════════
132
133    #[test]
134    fn noop_emitter_always_returns_zero() {
135        let noop = NoopEmitter::new();
136
137        let id1 = noop.emit(EventKind::WorkflowStarted {
138            task_count: 5,
139            generation_id: "gen".to_string(),
140            workflow_hash: "hash".to_string(),
141            nika_version: TEST_VERSION.to_string(),
142        });
143        let id2 = noop.emit(EventKind::TaskStarted {
144            task_id: Arc::from("task"),
145            verb: "infer".into(),
146            inputs: json!({}),
147        });
148        let id3 = noop.emit(EventKind::WorkflowCompleted {
149            final_output: Arc::new(json!("output")),
150            total_duration_ms: 1000,
151        });
152
153        assert_eq!(id1, 0);
154        assert_eq!(id2, 0);
155        assert_eq!(id3, 0);
156    }
157
158    #[test]
159    fn noop_emitter_is_clone() {
160        let noop = NoopEmitter::new();
161        let _cloned = noop.clone();
162    }
163
164    #[test]
165    fn noop_emitter_is_default() {
166        let noop = NoopEmitter;
167        assert_eq!(
168            noop.emit(EventKind::WorkflowStarted {
169                task_count: 1,
170                generation_id: "".to_string(),
171                workflow_hash: "".to_string(),
172                nika_version: TEST_VERSION.to_string(),
173            }),
174            0
175        );
176    }
177
178    #[test]
179    fn noop_emitter_is_send_sync() {
180        fn assert_send_sync<T: Send + Sync>() {}
181        assert_send_sync::<NoopEmitter>();
182    }
183
184    // ═══════════════════════════════════════════════════════════════
185    // Generic function tests
186    // ═══════════════════════════════════════════════════════════════
187
188    fn emit_workflow_started<E: EventEmitter>(emitter: &E, task_count: usize) -> u64 {
189        emitter.emit(EventKind::WorkflowStarted {
190            task_count,
191            generation_id: "test-gen".to_string(),
192            workflow_hash: "test-hash".to_string(),
193            nika_version: TEST_VERSION.to_string(),
194        })
195    }
196
197    #[test]
198    fn generic_function_works_with_eventlog() {
199        let log = EventLog::new();
200        let id = emit_workflow_started(&log, 3);
201        assert_eq!(id, 0);
202        assert_eq!(log.len(), 1);
203    }
204
205    #[test]
206    fn generic_function_works_with_noop() {
207        let noop = NoopEmitter::new();
208        let id = emit_workflow_started(&noop, 3);
209        assert_eq!(id, 0);
210    }
211}