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