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