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 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}