use super::*;
#[test]
fn grep_preserves_context_output() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("a.txt"), "one\nmatch\nthree\n").unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["match"],"path":".","limit":10,"context":1,"mode":"raw"}),
);
assert!(result.content.contains("a.txt-1-one"));
assert!(result.content.contains("a.txt:2:match"));
assert!(result.content.contains("a.txt-3-three"));
assert!(!result.content.contains("a.txt:1:one"));
}
#[test]
fn grep_stops_at_limit_without_scanning_all_matches() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("a.txt"), "match\nmatch\nlater match\n").unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["match"],"path":".","limit":2,"mode":"raw"}),
);
assert_eq!(result.content.lines().count(), 2);
assert_eq!(result.metadata["engine"], "native");
assert_eq!(result.metadata["matches_returned"], 2);
assert_eq!(result.metadata["truncated"], true);
assert_eq!(result.metadata["timed_out"], false);
assert_eq!(result.metadata["limit_reason"], "matches");
assert!(result.metadata["files_scanned"].as_u64().unwrap() >= 1);
assert!(result.metadata["bytes_scanned"].as_u64().unwrap() > 0);
assert_eq!(result.metadata["scan_complete"], true);
assert_eq!(result.metadata["matches_seen"], 3);
assert_eq!(result.metadata["matches_seen_exact"], false);
assert_eq!(result.metadata["raw_lookahead_reached"], true);
assert_eq!(result.metadata["has_more"], true);
assert!(result.content.contains("a.txt:1:match"));
assert!(result.content.contains("a.txt:2:match"));
}
#[test]
fn grep_skips_binary_files() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("bin.dat"), b"early match\n\0hidden").unwrap();
fs::write(temp.path().join("a.txt"), "visible match\n").unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["match"],"path":".","limit":10,"mode":"raw"}),
);
assert!(result.content.contains("visible match"));
assert!(!result.content.contains("early match"));
assert!(!result.content.contains("hidden"));
}
#[test]
fn grep_finds_matching_lines_with_native_engine() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("a.txt"), "alpha\nbeta\n").unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["beta"],"path":".","limit":10,"mode":"raw"}),
);
assert!(result.success, "{}", result.content);
assert!(result.content.contains("a.txt:2:beta"));
assert_eq!(result.metadata["engine"], "native");
assert!(result.metadata.get("exit_code").is_none());
}
#[test]
fn find_and_grep_exclude_p4ignored_files() {
let (temp, runtime) = runtime();
fs::write(temp.path().join(".p4ignore"), "ignored.txt\n").unwrap();
fs::write(temp.path().join("ignored.txt"), "unique-match\n").unwrap();
fs::write(temp.path().join("visible.txt"), "unique-match\n").unwrap();
let find = runtime.dispatch("find", json!({"query":"txt","limit":10}));
assert!(find.success, "{}", find.content);
assert!(find.content.contains("visible.txt"), "{}", find.content);
assert!(!find.content.contains("ignored.txt"), "{}", find.content);
let grep = runtime.dispatch(
"grep",
json!({"patterns":["unique-match"],"path":".","limit":10,"mode":"raw"}),
);
assert!(grep.success, "{}", grep.content);
assert!(grep.content.contains("visible.txt"), "{}", grep.content);
assert!(!grep.content.contains("ignored.txt"), "{}", grep.content);
}
#[test]
fn grep_accepts_legacy_pattern_as_flat_raw_compatibility() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("legacy.txt"), "needle\n").unwrap();
let result = runtime.dispatch("grep", json!({"pattern":"needle", "limit":10}));
assert!(result.success, "{}", result.content);
assert_eq!(result.content, "legacy.txt:1:needle");
assert_eq!(result.metadata["mode"], "raw");
assert_eq!(result.metadata["output_format"], "raw_v1");
}
#[test]
fn grep_canonical_patterns_use_or_semantics_and_grouped_output() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("a.txt"), "alpha beta\n").unwrap();
fs::write(temp.path().join("b.txt"), "beta\n").unwrap();
let result = runtime.dispatch("grep", json!({"patterns":["alpha", "beta"], "limit":10}));
assert!(result.success, "{}", result.content);
assert_eq!(
result.content,
"a.txt\n[match] 1:alpha beta\nb.txt\n[match] 1:beta"
);
assert_eq!(result.metadata["patterns_count"], 2);
assert_eq!(result.metadata["mode"], "ranked");
assert_eq!(result.metadata["output_format"], "grouped_v1");
assert_eq!(result.metadata["matches_seen"], 2);
assert_eq!(result.metadata["matches_returned"], 2);
assert_eq!(result.metadata["matches_seen_exact"], true);
assert_eq!(result.metadata["scan_complete"], true);
assert_eq!(result.metadata["truncated"], false);
}
#[test]
fn grep_ranked_per_file_cap_is_reported_separately_from_scan_count() {
let (temp, runtime) = runtime();
fs::write(
temp.path().join("a.txt"),
(1..=6).map(|_| "needle\n").collect::<String>(),
)
.unwrap();
let result = runtime.dispatch("grep", json!({"patterns":["needle"], "limit":10}));
assert!(result.success, "{}", result.content);
assert_eq!(result.metadata["matches_seen"], 6);
assert_eq!(result.metadata["matches_returned"], 5);
assert_eq!(result.metadata["per_file_limit_reached"], true);
assert_eq!(result.metadata["truncated"], true);
assert!(
result.metadata["truncation_reasons"]
.as_array()
.unwrap()
.iter()
.any(|reason| reason == "per_file")
);
}
#[test]
fn grep_grouped_output_deduplicates_context_and_keeps_ranked_labels() {
let (temp, runtime) = runtime();
fs::write(
temp.path().join("a.rs"),
"fn needle() {}\nshared context\nuse needle;\n",
)
.unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":10,"context":1}),
);
assert!(result.success, "{}", result.content);
assert_eq!(
result.content,
"a.rs\n[def-like] 1:fn needle() {}\n[import] 3:use needle;\n2:shared context"
);
assert_eq!(result.content.matches("a.rs").count(), 1);
assert_eq!(result.content.matches("2:shared context").count(), 1);
assert_eq!(result.metadata["files_returned"], 1);
}
#[test]
fn grep_grouped_context_stays_with_each_file() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("a.txt"), "a context\nneedle a\n").unwrap();
fs::write(temp.path().join("b.txt"), "b context\nneedle b\n").unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":10,"context":1}),
);
assert!(result.success, "{}", result.content);
assert_eq!(
result.content,
"a.txt\n[match] 2:needle a\n1:a context\nb.txt\n[match] 2:needle b\n1:b context"
);
}
#[test]
fn grep_limit_counts_primary_matches_not_context_rows() {
let (temp, runtime) = runtime();
fs::write(
temp.path().join("a.txt"),
"before\nneedle first\nafter\nneedle second\n",
)
.unwrap();
let result = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":1,"context":1,"mode":"raw"}),
);
assert!(result.success, "{}", result.content);
assert_eq!(
result.content,
"a.txt-1-before\na.txt:2:needle first\na.txt-3-after"
);
assert_eq!(result.metadata["matches_returned"], 1);
assert_eq!(result.metadata["output_lines_returned"], 3);
assert_eq!(result.metadata["has_more"], true);
assert_eq!(result.metadata["next_offset"], 1);
}
#[test]
fn grep_raw_pagination_dispatches_primary_offsets() {
let (temp, runtime) = runtime();
for (name, text) in [
("a.txt", "needle first\n"),
("b.txt", "needle second\n"),
("c.txt", "needle third\n"),
] {
fs::write(temp.path().join(name), text).unwrap();
}
let first = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":2,"mode":"raw"}),
);
assert!(first.success, "{}", first.content);
assert_eq!(first.content, "a.txt:1:needle first\nb.txt:1:needle second");
assert_eq!(first.metadata["matches_returned"], 2);
assert_eq!(first.metadata["has_more"], true);
assert_eq!(first.metadata["next_offset"], 2);
let second = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":2,"offset":2,"mode":"raw"}),
);
assert!(second.success, "{}", second.content);
assert_eq!(second.content, "c.txt:1:needle third");
assert_eq!(second.metadata["matches_returned"], 1);
assert_eq!(second.metadata["has_more"], false);
assert_eq!(second.metadata["next_offset"], serde_json::Value::Null);
assert_eq!(second.metadata["matches_seen"], 3);
assert_eq!(second.metadata["matches_seen_exact"], true);
}
#[test]
fn grep_reports_workspace_walk_omissions_as_incomplete() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("visible.txt"), "needle\n").unwrap();
let runtime = runtime.with_forced_workspace_walk_diagnostics(1, 2);
let result = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":10,"mode":"raw"}),
);
assert!(result.success, "{}", result.content);
assert_eq!(result.content, "visible.txt:1:needle");
assert_eq!(result.metadata["walk_errors"], 1);
assert_eq!(result.metadata["walk_entries_omitted"], 2);
assert_eq!(result.metadata["scan_complete"], false);
assert_eq!(result.metadata["matches_seen_exact"], false);
assert_eq!(result.metadata["has_more"], serde_json::Value::Null);
assert_eq!(result.metadata["next_offset"], serde_json::Value::Null);
assert!(
result.metadata["truncation_reasons"]
.as_array()
.unwrap()
.iter()
.any(|reason| reason == "walk_errors")
);
}
#[test]
fn grep_uses_separate_utf8_safe_primary_and_context_caps() {
let (temp, runtime) = runtime();
let primary = format!("{}needle{}", "a".repeat(3_000), "b".repeat(3_000));
let context = "c".repeat(6_000);
fs::write(
temp.path().join("long.txt"),
format!("{context}\n{primary}\n"),
)
.unwrap();
let ranked = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":10,"context":1}),
);
assert!(ranked.success, "{}", ranked.content);
let ranked_primary = ranked
.content
.lines()
.find_map(|line| line.strip_prefix("[match] 2:"))
.expect("ranked primary missing");
let ranked_context = ranked
.content
.lines()
.find_map(|line| line.strip_prefix("1:"))
.expect("ranked context missing");
assert!(ranked_primary.len() <= 240);
assert!(ranked_primary.contains("needle"));
assert!(ranked_context.len() <= 160);
let raw = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":10,"context":1,"mode":"raw"}),
);
assert!(raw.success, "{}", raw.content);
let raw_primary = raw
.content
.lines()
.find_map(|line| line.strip_prefix("long.txt:2:"))
.expect("raw primary missing");
let raw_context = raw
.content
.lines()
.find_map(|line| line.strip_prefix("long.txt-1-"))
.expect("raw context missing");
assert!(raw_primary.len() <= 4096);
assert!(raw_primary.contains("needle"));
assert!(raw_context.len() <= 160);
}
#[test]
fn grep_output_metadata_matches_capped_content() {
let (temp, runtime) = runtime();
let long_match = format!("{}needle{}", "x".repeat(3_000), "y".repeat(3_000));
for index in 0..20 {
fs::write(temp.path().join(format!("f{index:02}.txt")), &long_match).unwrap();
}
let result = runtime.dispatch(
"grep",
json!({"patterns":["needle"],"limit":20,"mode":"raw"}),
);
assert!(result.success, "{}", result.content);
let lines = result.content.lines().collect::<Vec<_>>();
let emitted_files = lines
.iter()
.filter_map(|line| line.split_once(":1:"))
.map(|(path, _)| path)
.collect::<std::collections::BTreeSet<_>>();
assert!(result.content.len() <= 65_536);
assert_eq!(result.metadata["output_byte_limit_reached"], true);
assert_eq!(result.metadata["truncated"], true);
assert_eq!(result.metadata["output_bytes"], result.content.len());
assert_eq!(result.metadata["output_lines_returned"], lines.len());
assert_eq!(result.metadata["matches_returned"], lines.len());
assert_eq!(result.metadata["files_returned"], emitted_files.len());
assert_eq!(result.metadata["matches_returned"], emitted_files.len());
}
#[test]
fn grep_output_cap_pagination_has_complete_non_overlapping_pages() {
let (temp, runtime) = runtime();
let expected = (0..40)
.map(|index| format!("match-{index:02}.txt"))
.collect::<Vec<_>>();
for (index, name) in expected.iter().enumerate() {
fs::write(
temp.path().join(name),
format!("needle-{index:02} {}", "x".repeat(8_000)),
)
.unwrap();
}
let mut offset = 0usize;
let mut identities = Vec::new();
let mut page_count = 0usize;
let mut saw_output_cap = false;
loop {
let result = runtime.dispatch(
"grep",
json!({
"patterns": ["needle"],
"limit": expected.len(),
"offset": offset,
"mode": "raw"
}),
);
assert!(result.success, "{}", result.content);
let page_identities = result
.content
.lines()
.map(|line| {
line.split_once(":1:")
.map(|(path, _)| path.to_owned())
.unwrap_or_else(|| panic!("missing raw identity in {line:?}"))
})
.collect::<Vec<_>>();
let emitted = result.metadata["matches_returned"]
.as_u64()
.expect("matches_returned metadata") as usize;
assert_eq!(emitted, page_identities.len());
assert_eq!(result.metadata["output_bytes"], result.content.len());
assert!(result.content.len() <= 65_536);
for identity in page_identities {
assert!(
!identities.contains(&identity),
"overlapping grep page identity: {identity}"
);
identities.push(identity);
}
if result.metadata["output_byte_limit_reached"] == true {
saw_output_cap = true;
assert!(result.content.len() > 60_000);
}
let has_more = result.metadata["has_more"]
.as_bool()
.expect("has_more metadata");
if !has_more {
assert_eq!(result.metadata["next_offset"], serde_json::Value::Null);
break;
}
let next_offset = result.metadata["next_offset"]
.as_u64()
.expect("next_offset metadata") as usize;
assert_eq!(next_offset, offset + emitted);
assert!(next_offset > offset, "grep pagination did not advance");
offset = next_offset;
page_count += 1;
assert!(
page_count <= expected.len(),
"grep pagination did not terminate"
);
}
assert!(saw_output_cap, "fixture did not reach the output cap");
assert_eq!(identities, expected);
}
#[test]
fn find_rejects_empty_query_and_invalid_limit() {
let (_temp, runtime) = runtime();
for args in [
json!({"query":""}),
json!({"query":" "}),
json!({"query":"lib", "limit":0}),
json!({"query":"lib", "limit":crate::tools::args::FIND_MAX_LIMIT + 1}),
json!({"query":"lib", "unknown":true}),
] {
let result = runtime.dispatch("find", args);
assert!(
!result.success,
"accepted invalid find args: {}",
result.content
);
}
}
#[test]
fn find_returns_fuzzy_relative_file_paths() {
let (temp, runtime) = runtime();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::create_dir_all(temp.path().join("docs")).unwrap();
fs::write(temp.path().join("src/lib.rs"), "").unwrap();
fs::write(temp.path().join("docs/readme.md"), "").unwrap();
let result = runtime.dispatch("find", json!({"query":"lib", "limit":10}));
assert!(result.success, "{}", result.content);
assert!(
result.content.lines().any(|line| line == "src/lib.rs"),
"{}",
result.content
);
assert!(
!result
.content
.contains(temp.path().to_string_lossy().as_ref())
);
assert_eq!(result.metadata["engine"], "rust");
assert_eq!(
result.metadata["matches_returned"].as_u64().unwrap() as usize,
result.content.lines().count()
);
}
#[test]
fn find_directory_kind_returns_directory_paths_with_slash() {
let (temp, runtime) = runtime();
fs::create_dir_all(temp.path().join("src/components")).unwrap();
fs::write(temp.path().join("src/components/button.rs"), "").unwrap();
let result = runtime.dispatch(
"find",
json!({"query":"components", "kind":"directories", "limit":10}),
);
assert!(result.success, "{}", result.content);
assert!(
result.content.lines().any(|line| line == "src/components/"),
"{}",
result.content
);
}
#[test]
fn find_mixed_kind_returns_files_and_directories() {
let (temp, runtime) = runtime();
fs::create_dir_all(temp.path().join("src/widget")).unwrap();
fs::write(temp.path().join("src/widget.rs"), "").unwrap();
fs::write(temp.path().join("src/widget/mod.rs"), "").unwrap();
let result = runtime.dispatch(
"find",
json!({"query":"widget", "kind":"mixed", "limit":20}),
);
assert!(result.success, "{}", result.content);
assert!(
result.content.lines().any(|line| line == "src/widget.rs"),
"{}",
result.content
);
assert!(
result.content.lines().any(|line| line == "src/widget/"),
"{}",
result.content
);
}
#[test]
fn find_path_filter_scopes_results_to_directory() {
let (temp, runtime) = runtime();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::create_dir_all(temp.path().join("docs")).unwrap();
fs::write(temp.path().join("src/main.rs"), "").unwrap();
fs::write(temp.path().join("docs/main.md"), "").unwrap();
let result = runtime.dispatch("find", json!({"query":"main", "path":"src", "limit":20}));
assert!(result.success, "{}", result.content);
assert!(
result.content.lines().any(|line| line == "src/main.rs"),
"{}",
result.content
);
assert!(
!result.content.contains("docs/main.md"),
"{}",
result.content
);
}
#[test]
fn find_path_filter_searches_filtered_universe_before_limit() {
let (temp, runtime) = runtime();
fs::create_dir_all(temp.path().join("scoped/deep/nested")).unwrap();
for index in 0..25 {
fs::write(temp.path().join(format!("needle_{index:02}.rs")), "").unwrap();
}
fs::write(temp.path().join("scoped/deep/nested/needle_target.rs"), "").unwrap();
let result = runtime.dispatch(
"find",
json!({"query":"needle", "path":"scoped", "limit":1}),
);
assert!(result.success, "{}", result.content);
assert_eq!(
result.content.lines().collect::<Vec<_>>(),
vec!["scoped/deep/nested/needle_target.rs"]
);
assert_eq!(result.metadata["matches_returned"], 1);
assert_eq!(result.metadata["total_matched"], 1);
assert_eq!(result.metadata["truncated"], false);
}
#[test]
fn find_path_filter_offset_pages_filtered_results() {
let (temp, runtime) = runtime();
fs::create_dir_all(temp.path().join("scoped")).unwrap();
for index in 0..25 {
fs::write(temp.path().join(format!("needle_{index:02}.rs")), "").unwrap();
}
for name in ["alpha", "beta", "gamma"] {
fs::write(temp.path().join(format!("scoped/needle-{name}.rs")), "").unwrap();
}
let first_page = runtime.dispatch(
"find",
json!({"query":"needle", "path":"scoped", "limit":3}),
);
assert!(first_page.success, "{}", first_page.content);
let first_page_paths = first_page.content.lines().collect::<Vec<_>>();
assert_eq!(first_page_paths.len(), 3, "{}", first_page.content);
assert_eq!(first_page.metadata["total_matched"], 3);
let offset_page = runtime.dispatch(
"find",
json!({"query":"needle", "path":"scoped", "limit":1, "offset":1}),
);
assert!(offset_page.success, "{}", offset_page.content);
assert_eq!(
offset_page.content.lines().collect::<Vec<_>>(),
vec![first_page_paths[1]]
);
assert_eq!(offset_page.metadata["matches_returned"], 1);
assert_eq!(offset_page.metadata["total_matched"], 3);
assert_eq!(offset_page.metadata["truncated"], true);
}
#[test]
fn find_rejects_file_path_and_outside_absolute_path_when_disabled() {
let (temp, _runtime) = runtime();
fs::write(temp.path().join("file.txt"), "").unwrap();
let outside = TempDir::new().unwrap();
fs::write(outside.path().join("outside.txt"), "").unwrap();
let restricted = ToolRuntime::new_with_settings(
temp.path(),
ToolSettings {
find: FindToolSettings {
absolute_paths: false,
},
..ToolSettings::default()
},
)
.unwrap();
let file_result = restricted.dispatch("find", json!({"query":"file", "path":"file.txt"}));
assert!(!file_result.success);
assert!(
file_result
.content
.contains("find path must be a directory")
);
let outside_result =
restricted.dispatch("find", json!({"query":"outside", "path": outside.path()}));
assert!(!outside_result.success);
assert!(
outside_result.content.contains("escapes cwd"),
"{}",
outside_result.content
);
}