diff --git a/server/src/log_buffer.rs b/server/src/log_buffer.rs index a06f05d..f0810c8 100644 --- a/server/src/log_buffer.rs +++ b/server/src/log_buffer.rs @@ -53,6 +53,17 @@ impl LogEntry { pub fn formatted(&self) -> String { format!("{} [{}] {}", self.timestamp, self.level.as_str(), self.message) } + + /// Format with ANSI color codes for terminal output. + /// WARN is yellow, ERROR is red, INFO has no color. + fn colored_formatted(&self) -> String { + let line = self.formatted(); + match self.level { + LogLevel::Warn => format!("\x1b[33m{line}\x1b[0m"), + LogLevel::Error => format!("\x1b[31m{line}\x1b[0m"), + LogLevel::Info => line, + } + } } pub struct LogBuffer { @@ -74,8 +85,7 @@ impl LogBuffer { timestamp, message, }; - let line = entry.formatted(); - eprintln!("{line}"); + eprintln!("{}", entry.colored_formatted()); if let Ok(mut buf) = self.entries.lock() { if buf.len() >= CAPACITY { buf.pop_front(); @@ -295,4 +305,62 @@ mod tests { assert_eq!(LogLevel::from_str_ci("info"), Some(LogLevel::Info)); assert_eq!(LogLevel::from_str_ci("DEBUG"), None); } + + #[test] + fn colored_formatted_warn_has_yellow_ansi() { + let entry = LogEntry { + level: LogLevel::Warn, + timestamp: "2026-01-01T00:00:00Z".into(), + message: "test warning".into(), + }; + let colored = entry.colored_formatted(); + assert!(colored.starts_with("\x1b[33m"), "WARN should start with yellow ANSI code"); + assert!(colored.ends_with("\x1b[0m"), "WARN should end with ANSI reset"); + assert!(colored.contains("[WARN]")); + assert!(colored.contains("test warning")); + } + + #[test] + fn colored_formatted_error_has_red_ansi() { + let entry = LogEntry { + level: LogLevel::Error, + timestamp: "2026-01-01T00:00:00Z".into(), + message: "test error".into(), + }; + let colored = entry.colored_formatted(); + assert!(colored.starts_with("\x1b[31m"), "ERROR should start with red ANSI code"); + assert!(colored.ends_with("\x1b[0m"), "ERROR should end with ANSI reset"); + assert!(colored.contains("[ERROR]")); + assert!(colored.contains("test error")); + } + + #[test] + fn colored_formatted_info_has_no_ansi() { + let entry = LogEntry { + level: LogLevel::Info, + timestamp: "2026-01-01T00:00:00Z".into(), + message: "test info".into(), + }; + let colored = entry.colored_formatted(); + assert!(!colored.contains("\x1b["), "INFO should have no ANSI escape codes"); + assert!(colored.contains("[INFO]")); + assert!(colored.contains("test info")); + } + + #[test] + fn ring_buffer_entries_have_no_ansi_codes() { + let buf = fresh_buffer(); + buf.push_entry(LogLevel::Info, "info msg".into()); + buf.push_entry(LogLevel::Warn, "warn msg".into()); + buf.push_entry(LogLevel::Error, "error msg".into()); + + let recent = buf.get_recent(10, None, None); + assert_eq!(recent.len(), 3); + for line in &recent { + assert!( + !line.contains("\x1b["), + "Ring buffer entry should not contain ANSI codes: {line}" + ); + } + } }