use exeora_cli::{protocol::ToolName, tools::ToolEngine};
use serde_json::{Value, json};
use std::fs;
use tempfile::TempDir;
use tokio_util::sync::CancellationToken;
const LINES: &[&str] = &[
"let zeta_001_beacon = compute();", "let ZETA_002_BEACON = compute();", "foobar and foobaz on one line", "xy and zy", "aa bb cc", " indent with a leading space", "\u{feff}indent behind a zero width space", "café au lait", "CAFÉ AU LAIT", "start of the line", "the line's end", "wrap <div id=\"x\"> here", "this is about time", "a zone of quiet", "the ANGLE marker", ];
fn fixture() -> TempDir {
let root = TempDir::new().unwrap();
fs::write(
root.path().join("sample.txt"),
format!("{}\n", LINES.join("\n")),
)
.unwrap();
root
}
async fn lines_matching(pattern: &str, case_insensitive: bool) -> Vec<u64> {
let root = fixture();
let engine = ToolEngine::new().unwrap();
let result: Value = engine
.execute(
root.path(),
ToolName::Grep,
json!({"pattern": pattern, "caseInsensitive": case_insensitive, "maxResults": 200}),
CancellationToken::new(),
)
.await
.unwrap();
result["matches"]
.as_array()
.unwrap()
.iter()
.map(|entry| entry["line"].as_u64().unwrap())
.collect()
}
#[tokio::test]
async fn alternation_is_prefiltered_without_losing_a_case_fold() {
assert_eq!(
lines_matching(r"(?:zeta|omega|kappa|sigma)_[0-9]{3}_beacon", false).await,
vec![1]
);
assert_eq!(
lines_matching(r"(?:zeta|omega|kappa|sigma)_[0-9]{3}_beacon", true).await,
vec![1, 2]
);
}
#[tokio::test]
async fn javascript_only_syntax_falls_back_to_the_engine() {
assert_eq!(lines_matching(r"foo(?=bar)", false).await, vec![3]);
assert_eq!(lines_matching(r"(?<=x)y", false).await, vec![4]);
assert_eq!(lines_matching(r"(\w)\1", false).await, vec![1, 2, 3, 5]);
}
#[tokio::test]
async fn escapes_rust_reads_as_syntax_stay_literal() {
assert_eq!(lines_matching(r"\<div", false).await, vec![12]);
assert_eq!(lines_matching(r"\about", false).await, vec![13]);
assert_eq!(lines_matching(r"\zone", false).await, vec![14]);
assert_eq!(lines_matching(r"\ANGLE", false).await, vec![15]);
}
#[tokio::test]
async fn a_whitespace_class_is_declined_rather_than_narrowed() {
assert_eq!(lines_matching(r"\sindent", false).await, vec![6, 7]);
}
#[tokio::test]
async fn a_non_ascii_literal_is_declined_when_folding_is_asked_for() {
assert_eq!(lines_matching("café", false).await, vec![8]);
assert_eq!(lines_matching("café", true).await, vec![8, 9]);
}
#[tokio::test]
async fn anchors_and_empty_matches_survive() {
assert_eq!(lines_matching("^start", false).await, vec![10]);
assert_eq!(lines_matching("end$", false).await, vec![11]);
assert_eq!(
lines_matching("x*", false).await.len(),
LINES.len(),
"an empty-matching pattern must still match every line"
);
}