From d98cce05ce75b554d819426ec081e0bfa9d1308c Mon Sep 17 00:00:00 2001 From: Greyforge Admin Date: Wed, 20 May 2026 00:43:22 -0400 Subject: [PATCH] Match log levels exactly --- src/cortex-cli/src/logs_cmd.rs | 13 +++- src/cortex-cli/tests/logs_level_filter.rs | 83 +++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/cortex-cli/tests/logs_level_filter.rs diff --git a/src/cortex-cli/src/logs_cmd.rs b/src/cortex-cli/src/logs_cmd.rs index f525efc3c..21a0377f1 100644 --- a/src/cortex-cli/src/logs_cmd.rs +++ b/src/cortex-cli/src/logs_cmd.rs @@ -81,6 +81,15 @@ fn format_size(bytes: u64) -> String { } } +fn parse_log_level(line: &str) -> Option<&str> { + line.split(|c: char| !c.is_ascii_alphanumeric()) + .find(|part| matches!(*part, "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR")) +} + +fn line_matches_level(line: &str, level: &str) -> bool { + parse_log_level(&line.to_uppercase()).is_some_and(|line_level| line_level == level) +} + impl LogsCli { /// Run the logs command. pub async fn run(self) -> Result<()> { @@ -174,7 +183,7 @@ impl LogsCli { let level_upper = level.to_uppercase(); lines .iter() - .filter(|line| line.to_uppercase().contains(&level_upper)) + .filter(|line| line_matches_level(line, &level_upper)) .copied() .collect() } else { @@ -240,7 +249,7 @@ impl LogsCli { Ok(_) => { // Apply level filter if specified if let Some(ref level) = level_filter - && !line.to_uppercase().contains(&level.to_uppercase()) + && !line_matches_level(&line, &level.to_uppercase()) { continue; } diff --git a/src/cortex-cli/tests/logs_level_filter.rs b/src/cortex-cli/tests/logs_level_filter.rs new file mode 100644 index 000000000..ce0c84907 --- /dev/null +++ b/src/cortex-cli/tests/logs_level_filter.rs @@ -0,0 +1,83 @@ +use std::fs; +use std::process::Command; + +use tempfile::tempdir; + +fn combined_output(output: &std::process::Output) -> String { + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +#[test] +fn logs_level_filter_matches_exact_log_level() { + let home = tempdir().unwrap(); + let cache = tempdir().unwrap(); + let logs_dir = cache.path().join("cortex").join("logs"); + fs::create_dir_all(&logs_dir).unwrap(); + fs::write( + logs_dir.join("cortex.log"), + [ + "2026-05-20T00:00:00Z INFO startup ok", + "2026-05-20T00:00:01Z WARN contains ERROR in the message", + "2026-05-20T00:00:02Z ERROR actual failure", + "", + ] + .join("\n"), + ) + .unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_Cortex")) + .args(["logs", "--level", "error", "--lines", "10"]) + .env("HOME", home.path()) + .env("XDG_CACHE_HOME", cache.path()) + .env_remove("CORTEX_HOME") + .output() + .unwrap(); + + assert!( + output.status.success(), + "logs --level failed:\n{}", + combined_output(&output) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("ERROR actual failure"), "output:\n{stdout}"); + assert!(!stdout.contains("WARN contains ERROR"), "output:\n{stdout}"); + assert!(!stdout.contains("INFO startup"), "output:\n{stdout}"); +} + +#[test] +fn logs_level_filter_does_not_match_partial_level() { + let home = tempdir().unwrap(); + let cache = tempdir().unwrap(); + let logs_dir = cache.path().join("cortex").join("logs"); + fs::create_dir_all(&logs_dir).unwrap(); + fs::write( + logs_dir.join("cortex.log"), + "2026-05-20T00:00:00Z ERROR actual failure\n", + ) + .unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_Cortex")) + .args(["logs", "--level", "err", "--lines", "10"]) + .env("HOME", home.path()) + .env("XDG_CACHE_HOME", cache.path()) + .env_remove("CORTEX_HOME") + .output() + .unwrap(); + + assert!( + output.status.success(), + "logs --level failed:\n{}", + combined_output(&output) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !stdout.contains("ERROR actual failure"), + "output:\n{stdout}" + ); +}