Skip to main content

ralph_core/diagnostics/
errors.rs

1use chrono::Utc;
2use serde::Serialize;
3use std::fs::{File, OpenOptions};
4use std::io::{self, BufWriter, Write};
5use std::path::Path;
6
7#[derive(Debug, Serialize)]
8pub struct ErrorEntry {
9    ts: String,
10    iteration: u32,
11    hat: String,
12    error_type: String,
13    message: String,
14    context: serde_json::Value,
15}
16
17#[derive(Debug)]
18pub enum DiagnosticError {
19    ParseError {
20        source: String,
21        message: String,
22        input: String,
23    },
24    ValidationFailure {
25        rule: String,
26        message: String,
27        evidence: String,
28    },
29    BackendError {
30        backend: String,
31        message: String,
32    },
33    Timeout {
34        operation: String,
35        duration_ms: u64,
36    },
37    MalformedEvent {
38        line: String,
39        error: String,
40    },
41}
42
43impl DiagnosticError {
44    fn error_type(&self) -> &str {
45        match self {
46            Self::ParseError { .. } => "parse_error",
47            Self::ValidationFailure { .. } => "validation_failure",
48            Self::BackendError { .. } => "backend_error",
49            Self::Timeout { .. } => "timeout",
50            Self::MalformedEvent { .. } => "malformed_event",
51        }
52    }
53
54    fn message(&self) -> String {
55        match self {
56            Self::ParseError { message, .. } => message.clone(),
57            Self::ValidationFailure { message, .. } => message.clone(),
58            Self::BackendError { message, .. } => message.clone(),
59            Self::Timeout { operation, .. } => format!("Operation timed out: {}", operation),
60            Self::MalformedEvent { error, .. } => error.clone(),
61        }
62    }
63
64    fn context(&self) -> serde_json::Value {
65        match self {
66            Self::ParseError {
67                source,
68                message: _,
69                input,
70            } => serde_json::json!({
71                "source": source,
72                "input": input,
73            }),
74            Self::ValidationFailure {
75                rule,
76                message: _,
77                evidence,
78            } => serde_json::json!({
79                "rule": rule,
80                "evidence": evidence,
81            }),
82            Self::BackendError {
83                backend,
84                message: _,
85            } => serde_json::json!({
86                "backend": backend,
87            }),
88            Self::Timeout {
89                operation,
90                duration_ms,
91            } => serde_json::json!({
92                "operation": operation,
93                "duration_ms": duration_ms,
94            }),
95            Self::MalformedEvent { line, error: _ } => serde_json::json!({
96                "line": line,
97            }),
98        }
99    }
100}
101
102pub struct ErrorLogger {
103    file: BufWriter<File>,
104    iteration: u32,
105    hat: String,
106}
107
108impl ErrorLogger {
109    pub fn new(session_dir: &Path) -> io::Result<Self> {
110        let file_path = session_dir.join("errors.jsonl");
111        let file = OpenOptions::new()
112            .create(true)
113            .append(true)
114            .open(file_path)?;
115
116        Ok(Self {
117            file: BufWriter::new(file),
118            iteration: 0,
119            hat: String::from("unknown"),
120        })
121    }
122
123    pub fn set_context(&mut self, iteration: u32, hat: &str) {
124        self.iteration = iteration;
125        self.hat = hat.to_string();
126    }
127
128    pub fn log(&mut self, error: DiagnosticError) {
129        let entry = ErrorEntry {
130            ts: Utc::now().to_rfc3339(),
131            iteration: self.iteration,
132            hat: self.hat.clone(),
133            error_type: error.error_type().to_string(),
134            message: error.message(),
135            context: error.context(),
136        };
137
138        if let Ok(json) = serde_json::to_string(&entry) {
139            let _ = writeln!(self.file, "{}", json);
140            let _ = self.file.flush();
141        }
142    }
143
144    pub fn flush(&mut self) {
145        let _ = self.file.flush();
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::fs;
153    use tempfile::TempDir;
154
155    #[test]
156    fn test_error_logger_creates_file() {
157        let temp_dir = TempDir::new().unwrap();
158        let session_dir = temp_dir.path();
159
160        let logger = ErrorLogger::new(session_dir);
161        assert!(logger.is_ok());
162
163        let file_path = session_dir.join("errors.jsonl");
164        assert!(file_path.exists());
165    }
166
167    #[test]
168    fn test_all_error_types_serialize() {
169        let temp_dir = TempDir::new().unwrap();
170        let session_dir = temp_dir.path();
171        let mut logger = ErrorLogger::new(session_dir).unwrap();
172        logger.set_context(1, "ralph");
173
174        let errors = vec![
175            DiagnosticError::ParseError {
176                source: "agent_output".to_string(),
177                message: "Invalid JSON".to_string(),
178                input: "{invalid".to_string(),
179            },
180            DiagnosticError::ValidationFailure {
181                rule: "tests_required".to_string(),
182                message: "Missing test evidence".to_string(),
183                evidence: "tests: missing".to_string(),
184            },
185            DiagnosticError::BackendError {
186                backend: "claude".to_string(),
187                message: "API error".to_string(),
188            },
189            DiagnosticError::Timeout {
190                operation: "agent_execution".to_string(),
191                duration_ms: 30000,
192            },
193            DiagnosticError::MalformedEvent {
194                line: "<event topic=".to_string(),
195                error: "Incomplete tag".to_string(),
196            },
197        ];
198
199        for error in errors {
200            logger.log(error);
201        }
202
203        let file_path = session_dir.join("errors.jsonl");
204        let content = fs::read_to_string(file_path).unwrap();
205        let lines: Vec<&str> = content.lines().collect();
206
207        assert_eq!(lines.len(), 5);
208
209        // Verify each line is valid JSON
210        for line in lines {
211            let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
212            assert!(parsed.get("ts").is_some());
213            assert_eq!(parsed.get("iteration").unwrap(), 1);
214            assert_eq!(parsed.get("hat").unwrap(), "ralph");
215            assert!(parsed.get("error_type").is_some());
216            assert!(parsed.get("message").is_some());
217            assert!(parsed.get("context").is_some());
218        }
219    }
220
221    #[test]
222    fn test_error_logger_integration() {
223        let temp_dir = TempDir::new().unwrap();
224        let session_dir = temp_dir.path();
225        let mut logger = ErrorLogger::new(session_dir).unwrap();
226
227        logger.set_context(1, "builder");
228        logger.log(DiagnosticError::ValidationFailure {
229            rule: "lint_pass".to_string(),
230            message: "Lint failed".to_string(),
231            evidence: "lint: fail".to_string(),
232        });
233
234        logger.set_context(2, "validator");
235        logger.log(DiagnosticError::ParseError {
236            source: "event_parser".to_string(),
237            message: "Malformed event".to_string(),
238            input: "<event>".to_string(),
239        });
240
241        let file_path = session_dir.join("errors.jsonl");
242        let content = fs::read_to_string(file_path).unwrap();
243        let lines: Vec<&str> = content.lines().collect();
244
245        assert_eq!(lines.len(), 2);
246
247        let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
248        assert_eq!(first.get("iteration").unwrap(), 1);
249        assert_eq!(first.get("hat").unwrap(), "builder");
250        assert_eq!(first.get("error_type").unwrap(), "validation_failure");
251
252        let second: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
253        assert_eq!(second.get("iteration").unwrap(), 2);
254        assert_eq!(second.get("hat").unwrap(), "validator");
255        assert_eq!(second.get("error_type").unwrap(), "parse_error");
256    }
257}