ralph_core/diagnostics/
orchestration.rs1use serde::{Deserialize, Serialize};
2use std::fs::{File, OpenOptions};
3use std::io::{BufWriter, Write};
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct OrchestrationEntry {
8 pub timestamp: String,
9 pub iteration: u32,
10 pub hat: String,
11 pub event: OrchestrationEvent,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum OrchestrationEvent {
17 IterationStarted,
18 HatSelected { hat: String, reason: String },
19 EventPublished { topic: String },
20 BackpressureTriggered { reason: String },
21 LoopTerminated { reason: String },
22 TaskAbandoned { reason: String },
23}
24
25pub struct OrchestrationLogger {
26 writer: BufWriter<File>,
27}
28
29impl OrchestrationLogger {
30 pub fn new(session_dir: &Path) -> std::io::Result<Self> {
31 let file = OpenOptions::new()
32 .create(true)
33 .append(true)
34 .open(session_dir.join("orchestration.jsonl"))?;
35 Ok(Self {
36 writer: BufWriter::new(file),
37 })
38 }
39
40 pub fn log(
41 &mut self,
42 iteration: u32,
43 hat: &str,
44 event: OrchestrationEvent,
45 ) -> std::io::Result<()> {
46 let entry = OrchestrationEntry {
47 timestamp: chrono::Local::now().to_rfc3339(),
48 iteration,
49 hat: hat.to_string(),
50 event,
51 };
52 serde_json::to_writer(&mut self.writer, &entry)?;
53 self.writer.write_all(b"\n")?;
54 self.writer.flush()?;
55 Ok(())
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use std::io::{BufRead, BufReader};
63 use tempfile::TempDir;
64
65 #[test]
66 fn test_all_event_types_serialize() {
67 let events = vec![
68 OrchestrationEvent::IterationStarted,
69 OrchestrationEvent::HatSelected {
70 hat: "ralph".to_string(),
71 reason: "pending_events".to_string(),
72 },
73 OrchestrationEvent::EventPublished {
74 topic: "build.start".to_string(),
75 },
76 OrchestrationEvent::BackpressureTriggered {
77 reason: "tests failed".to_string(),
78 },
79 OrchestrationEvent::LoopTerminated {
80 reason: "completion_promise".to_string(),
81 },
82 OrchestrationEvent::TaskAbandoned {
83 reason: "max_iterations".to_string(),
84 },
85 ];
86
87 for event in events {
88 let json = serde_json::to_string(&event).unwrap();
89 let _: OrchestrationEvent = serde_json::from_str(&json).unwrap();
90 }
91 }
92
93 #[test]
94 fn test_iteration_and_hat_captured() {
95 let temp_dir = TempDir::new().unwrap();
96 let mut logger = OrchestrationLogger::new(temp_dir.path()).unwrap();
97
98 logger
99 .log(
100 5,
101 "builder",
102 OrchestrationEvent::HatSelected {
103 hat: "builder".to_string(),
104 reason: "tasks_ready".to_string(),
105 },
106 )
107 .unwrap();
108
109 drop(logger);
110
111 let file = File::open(temp_dir.path().join("orchestration.jsonl")).unwrap();
112 let reader = BufReader::new(file);
113 let line = reader.lines().next().unwrap().unwrap();
114 let entry: OrchestrationEntry = serde_json::from_str(&line).unwrap();
115
116 assert_eq!(entry.iteration, 5);
117 assert_eq!(entry.hat, "builder");
118 }
119
120 #[test]
121 fn test_immediate_flush() {
122 let temp_dir = TempDir::new().unwrap();
123 let mut logger = OrchestrationLogger::new(temp_dir.path()).unwrap();
124
125 logger
126 .log(1, "ralph", OrchestrationEvent::IterationStarted)
127 .unwrap();
128
129 let file = File::open(temp_dir.path().join("orchestration.jsonl")).unwrap();
131 let reader = BufReader::new(file);
132 let lines: Vec<_> = reader.lines().collect();
133 assert_eq!(lines.len(), 1);
134 }
135}