1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CuenvEvent {
13 pub id: Uuid,
15 pub correlation_id: Uuid,
17 pub timestamp: DateTime<Utc>,
19 pub source: EventSource,
21 pub category: EventCategory,
23}
24
25impl CuenvEvent {
26 #[must_use]
28 pub fn new(correlation_id: Uuid, source: EventSource, category: EventCategory) -> Self {
29 Self {
30 id: Uuid::new_v4(),
31 correlation_id,
32 timestamp: Utc::now(),
33 source,
34 category,
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct EventSource {
42 pub target: String,
44 pub file: Option<String>,
46 pub line: Option<u32>,
48}
49
50impl EventSource {
51 #[must_use]
53 pub fn new(target: impl Into<String>) -> Self {
54 Self {
55 target: target.into(),
56 file: None,
57 line: None,
58 }
59 }
60
61 #[must_use]
63 pub fn with_location(target: impl Into<String>, file: impl Into<String>, line: u32) -> Self {
64 Self {
65 target: target.into(),
66 file: Some(file.into()),
67 line: Some(line),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(tag = "type", content = "data")]
75pub enum EventCategory {
76 Task(TaskEvent),
78 Ci(CiEvent),
80 Command(CommandEvent),
82 Interactive(InteractiveEvent),
84 System(SystemEvent),
86 Output(OutputEvent),
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "event", content = "data")]
93pub enum TaskEvent {
94 Started {
96 name: String,
98 command: String,
100 hermetic: bool,
102 },
103 CacheHit {
105 name: String,
107 cache_key: String,
109 },
110 CacheMiss {
112 name: String,
114 },
115 Output {
117 name: String,
119 stream: Stream,
121 content: String,
123 },
124 Completed {
126 name: String,
128 success: bool,
130 exit_code: Option<i32>,
132 duration_ms: u64,
134 },
135 GroupStarted {
137 name: String,
139 sequential: bool,
141 task_count: usize,
143 },
144 GroupCompleted {
146 name: String,
148 success: bool,
150 duration_ms: u64,
152 },
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(tag = "event", content = "data")]
158pub enum CiEvent {
159 ContextDetected {
161 provider: String,
163 event_type: String,
165 ref_name: String,
167 },
168 ChangedFilesFound {
170 count: usize,
172 },
173 ProjectsDiscovered {
175 count: usize,
177 },
178 ProjectSkipped {
180 path: String,
182 reason: String,
184 },
185 TaskExecuting {
187 project: String,
189 task: String,
191 },
192 TaskResult {
194 project: String,
196 task: String,
198 success: bool,
200 error: Option<String>,
202 },
203 ReportGenerated {
205 path: String,
207 },
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(tag = "event", content = "data")]
213pub enum CommandEvent {
214 Started {
216 command: String,
218 args: Vec<String>,
220 },
221 Progress {
223 command: String,
225 progress: f32,
227 message: String,
229 },
230 Completed {
232 command: String,
234 success: bool,
236 duration_ms: u64,
238 },
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(tag = "event", content = "data")]
244pub enum InteractiveEvent {
245 PromptRequested {
247 prompt_id: String,
249 message: String,
251 options: Vec<String>,
253 },
254 PromptResolved {
256 prompt_id: String,
258 response: String,
260 },
261 WaitProgress {
263 target: String,
265 elapsed_secs: u64,
267 },
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(tag = "event", content = "data")]
273pub enum SystemEvent {
274 SupervisorLog {
276 tag: String,
278 message: String,
280 },
281 Shutdown,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(tag = "event", content = "data")]
288pub enum OutputEvent {
289 Stdout {
291 content: String,
293 },
294 Stderr {
296 content: String,
298 },
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303pub enum Stream {
304 Stdout,
306 Stderr,
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_event_creation() {
316 let event = CuenvEvent::new(
317 Uuid::new_v4(),
318 EventSource::new("cuenv::test"),
319 EventCategory::Output(OutputEvent::Stdout {
320 content: "test".to_string(),
321 }),
322 );
323
324 assert!(!event.id.is_nil());
325 assert_eq!(event.source.target, "cuenv::test");
326 }
327
328 #[test]
329 fn test_event_serialization() {
330 let event = CuenvEvent::new(
331 Uuid::new_v4(),
332 EventSource::new("cuenv::task"),
333 EventCategory::Task(TaskEvent::Started {
334 name: "build".to_string(),
335 command: "cargo build".to_string(),
336 hermetic: true,
337 }),
338 );
339
340 let json = serde_json::to_string(&event).unwrap();
341 assert!(json.contains("cuenv::task"));
342 assert!(json.contains("build"));
343
344 let parsed: CuenvEvent = serde_json::from_str(&json).unwrap();
345 assert_eq!(parsed.id, event.id);
346 }
347
348 #[test]
349 fn test_event_source_with_location() {
350 let source = EventSource::with_location("cuenv::task", "src/main.rs", 42);
351 assert_eq!(source.target, "cuenv::task");
352 assert_eq!(source.file, Some("src/main.rs".to_string()));
353 assert_eq!(source.line, Some(42));
354 }
355
356 #[test]
357 fn test_event_source_new() {
358 let source = EventSource::new("cuenv::ci");
359 assert_eq!(source.target, "cuenv::ci");
360 assert!(source.file.is_none());
361 assert!(source.line.is_none());
362 }
363
364 #[test]
365 fn test_task_event_cache_hit() {
366 let event = TaskEvent::CacheHit {
367 name: "test".to_string(),
368 cache_key: "abc123".to_string(),
369 };
370 let json = serde_json::to_string(&event).unwrap();
371 assert!(json.contains("CacheHit"));
372 assert!(json.contains("abc123"));
373 }
374
375 #[test]
376 fn test_task_event_cache_miss() {
377 let event = TaskEvent::CacheMiss {
378 name: "test".to_string(),
379 };
380 let json = serde_json::to_string(&event).unwrap();
381 assert!(json.contains("CacheMiss"));
382 }
383
384 #[test]
385 fn test_task_event_output() {
386 let event = TaskEvent::Output {
387 name: "build".to_string(),
388 stream: Stream::Stdout,
389 content: "compiling...".to_string(),
390 };
391 let json = serde_json::to_string(&event).unwrap();
392 assert!(json.contains("Output"));
393 assert!(json.contains("Stdout"));
394 }
395
396 #[test]
397 fn test_task_event_completed() {
398 let event = TaskEvent::Completed {
399 name: "build".to_string(),
400 success: true,
401 exit_code: Some(0),
402 duration_ms: 1500,
403 };
404 let json = serde_json::to_string(&event).unwrap();
405 assert!(json.contains("Completed"));
406 assert!(json.contains("1500"));
407 }
408
409 #[test]
410 fn test_task_event_group_started() {
411 let event = TaskEvent::GroupStarted {
412 name: "tests".to_string(),
413 sequential: false,
414 task_count: 5,
415 };
416 let json = serde_json::to_string(&event).unwrap();
417 assert!(json.contains("GroupStarted"));
418 assert!(json.contains('5'));
419 }
420
421 #[test]
422 fn test_task_event_group_completed() {
423 let event = TaskEvent::GroupCompleted {
424 name: "tests".to_string(),
425 success: true,
426 duration_ms: 3000,
427 };
428 let json = serde_json::to_string(&event).unwrap();
429 assert!(json.contains("GroupCompleted"));
430 }
431
432 #[test]
433 fn test_ci_event_context_detected() {
434 let event = CiEvent::ContextDetected {
435 provider: "github".to_string(),
436 event_type: "push".to_string(),
437 ref_name: "main".to_string(),
438 };
439 let json = serde_json::to_string(&event).unwrap();
440 assert!(json.contains("ContextDetected"));
441 assert!(json.contains("github"));
442 }
443
444 #[test]
445 fn test_ci_event_changed_files_found() {
446 let event = CiEvent::ChangedFilesFound { count: 10 };
447 let json = serde_json::to_string(&event).unwrap();
448 assert!(json.contains("ChangedFilesFound"));
449 assert!(json.contains("10"));
450 }
451
452 #[test]
453 fn test_ci_event_projects_discovered() {
454 let event = CiEvent::ProjectsDiscovered { count: 3 };
455 let json = serde_json::to_string(&event).unwrap();
456 assert!(json.contains("ProjectsDiscovered"));
457 }
458
459 #[test]
460 fn test_ci_event_project_skipped() {
461 let event = CiEvent::ProjectSkipped {
462 path: "/project".to_string(),
463 reason: "no changes".to_string(),
464 };
465 let json = serde_json::to_string(&event).unwrap();
466 assert!(json.contains("ProjectSkipped"));
467 assert!(json.contains("no changes"));
468 }
469
470 #[test]
471 fn test_ci_event_task_executing() {
472 let event = CiEvent::TaskExecuting {
473 project: "/app".to_string(),
474 task: "build".to_string(),
475 };
476 let json = serde_json::to_string(&event).unwrap();
477 assert!(json.contains("TaskExecuting"));
478 }
479
480 #[test]
481 fn test_ci_event_task_result() {
482 let event = CiEvent::TaskResult {
483 project: "/app".to_string(),
484 task: "build".to_string(),
485 success: false,
486 error: Some("build failed".to_string()),
487 };
488 let json = serde_json::to_string(&event).unwrap();
489 assert!(json.contains("TaskResult"));
490 assert!(json.contains("build failed"));
491 }
492
493 #[test]
494 fn test_ci_event_report_generated() {
495 let event = CiEvent::ReportGenerated {
496 path: "/reports/ci.json".to_string(),
497 };
498 let json = serde_json::to_string(&event).unwrap();
499 assert!(json.contains("ReportGenerated"));
500 }
501
502 #[test]
503 fn test_command_event_started() {
504 let event = CommandEvent::Started {
505 command: "sync".to_string(),
506 args: vec!["--force".to_string()],
507 };
508 let json = serde_json::to_string(&event).unwrap();
509 assert!(json.contains("Started"));
510 assert!(json.contains("--force"));
511 }
512
513 #[test]
514 fn test_command_event_progress() {
515 let event = CommandEvent::Progress {
516 command: "sync".to_string(),
517 progress: 0.5,
518 message: "halfway there".to_string(),
519 };
520 let json = serde_json::to_string(&event).unwrap();
521 assert!(json.contains("Progress"));
522 assert!(json.contains("0.5"));
523 }
524
525 #[test]
526 fn test_command_event_completed() {
527 let event = CommandEvent::Completed {
528 command: "sync".to_string(),
529 success: true,
530 duration_ms: 500,
531 };
532 let json = serde_json::to_string(&event).unwrap();
533 assert!(json.contains("Completed"));
534 }
535
536 #[test]
537 fn test_interactive_event_prompt_requested() {
538 let event = InteractiveEvent::PromptRequested {
539 prompt_id: "p1".to_string(),
540 message: "Choose an option".to_string(),
541 options: vec!["a".to_string(), "b".to_string()],
542 };
543 let json = serde_json::to_string(&event).unwrap();
544 assert!(json.contains("PromptRequested"));
545 assert!(json.contains("Choose an option"));
546 }
547
548 #[test]
549 fn test_interactive_event_prompt_resolved() {
550 let event = InteractiveEvent::PromptResolved {
551 prompt_id: "p1".to_string(),
552 response: "a".to_string(),
553 };
554 let json = serde_json::to_string(&event).unwrap();
555 assert!(json.contains("PromptResolved"));
556 }
557
558 #[test]
559 fn test_interactive_event_wait_progress() {
560 let event = InteractiveEvent::WaitProgress {
561 target: "lock".to_string(),
562 elapsed_secs: 30,
563 };
564 let json = serde_json::to_string(&event).unwrap();
565 assert!(json.contains("WaitProgress"));
566 assert!(json.contains("30"));
567 }
568
569 #[test]
570 fn test_system_event_supervisor_log() {
571 let event = SystemEvent::SupervisorLog {
572 tag: "coordinator".to_string(),
573 message: "started".to_string(),
574 };
575 let json = serde_json::to_string(&event).unwrap();
576 assert!(json.contains("SupervisorLog"));
577 }
578
579 #[test]
580 fn test_system_event_shutdown() {
581 let event = SystemEvent::Shutdown;
582 let json = serde_json::to_string(&event).unwrap();
583 assert!(json.contains("Shutdown"));
584 }
585
586 #[test]
587 fn test_output_event_stdout() {
588 let event = OutputEvent::Stdout {
589 content: "hello".to_string(),
590 };
591 let json = serde_json::to_string(&event).unwrap();
592 assert!(json.contains("Stdout"));
593 assert!(json.contains("hello"));
594 }
595
596 #[test]
597 fn test_output_event_stderr() {
598 let event = OutputEvent::Stderr {
599 content: "error".to_string(),
600 };
601 let json = serde_json::to_string(&event).unwrap();
602 assert!(json.contains("Stderr"));
603 }
604
605 #[test]
606 fn test_stream_enum() {
607 assert_eq!(Stream::Stdout, Stream::Stdout);
608 assert_ne!(Stream::Stdout, Stream::Stderr);
609
610 let stdout_json = serde_json::to_string(&Stream::Stdout).unwrap();
611 let stderr_json = serde_json::to_string(&Stream::Stderr).unwrap();
612
613 assert!(stdout_json.contains("Stdout"));
614 assert!(stderr_json.contains("Stderr"));
615 }
616
617 #[test]
618 fn test_event_category_all_variants() {
619 let categories = vec![
620 EventCategory::Task(TaskEvent::CacheMiss {
621 name: "test".to_string(),
622 }),
623 EventCategory::Ci(CiEvent::ProjectsDiscovered { count: 1 }),
624 EventCategory::Command(CommandEvent::Started {
625 command: "sync".to_string(),
626 args: vec![],
627 }),
628 EventCategory::Interactive(InteractiveEvent::WaitProgress {
629 target: "lock".to_string(),
630 elapsed_secs: 0,
631 }),
632 EventCategory::System(SystemEvent::Shutdown),
633 EventCategory::Output(OutputEvent::Stdout {
634 content: "out".to_string(),
635 }),
636 ];
637
638 for cat in categories {
639 let json = serde_json::to_string(&cat).unwrap();
640 let parsed: EventCategory = serde_json::from_str(&json).unwrap();
641 let json2 = serde_json::to_string(&parsed).unwrap();
643 assert_eq!(json, json2);
644 }
645 }
646
647 #[test]
648 fn test_cuenv_event_clone() {
649 let event = CuenvEvent::new(
650 Uuid::new_v4(),
651 EventSource::new("cuenv::test"),
652 EventCategory::System(SystemEvent::Shutdown),
653 );
654 let cloned = event.clone();
655 assert_eq!(event.id, cloned.id);
656 assert_eq!(event.correlation_id, cloned.correlation_id);
657 }
658
659 #[test]
660 fn test_cuenv_event_debug() {
661 let event = CuenvEvent::new(
662 Uuid::new_v4(),
663 EventSource::new("cuenv::test"),
664 EventCategory::System(SystemEvent::Shutdown),
665 );
666 let debug_str = format!("{event:?}");
667 assert!(debug_str.contains("CuenvEvent"));
668 }
669}