Skip to main content

ralph_core/diagnostics/
orchestration.rs

1use 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        // Don't drop logger - verify file has content immediately
130        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}