use std::sync as path_std_sync;
use super::*;
fn args(limit: CborValue) -> CborValue {
CborValue::Map(vec![
(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*".to_owned()),
),
(CborValue::Text("limit".to_owned()), limit),
])
}
fn find_request(limit: usize) -> FindRequest {
FindRequest {
pattern: "*".to_owned(),
path: PathBuf::from("."),
limit,
display_args: "fixture".to_owned(),
}
}
fn map_text<'a>(result: &'a CborValue, field: &str) -> Option<&'a str> {
let CborValue::Map(entries) = result else {
return None;
};
entries.iter().find_map(|(key, value)| match (key, value) {
(CborValue::Text(key), CborValue::Text(value)) if key == field => Some(value.as_str()),
_ => None,
})
}
fn map_int(result: &CborValue, field: &str) -> Option<i64> {
let CborValue::Map(entries) = result else {
return None;
};
entries.iter().find_map(|(key, value)| match (key, value) {
(CborValue::Text(key), CborValue::Integer(value)) if key == field => {
i128::from(*value).try_into().ok()
}
_ => None,
})
}
fn map_bool(result: &CborValue, field: &str) -> Option<bool> {
let CborValue::Map(entries) = result else {
return None;
};
entries.iter().find_map(|(key, value)| match (key, value) {
(CborValue::Text(key), CborValue::Bool(value)) if key == field => Some(*value),
_ => None,
})
}
fn fixed_width_paths(count: usize, suffix: &str) -> Vec<String> {
(0..count)
.map(|index| format!("{index:03}{suffix}"))
.collect()
}
#[test]
fn find_rejects_wrong_type_limit() {
let err = run_find(&args(CborValue::Text("10".to_owned())))
.expect_err("string limit should be rejected");
assert_eq!(err.message, "argument `limit` must be an integer");
}
#[test]
fn find_rejects_wrong_type_path() {
let args = CborValue::Map(vec![
(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*".to_owned()),
),
(
CborValue::Text("path".to_owned()),
CborValue::Integer(1.into()),
),
]);
let err = run_find(&args).expect_err("integer path should be rejected");
assert_eq!(err.message, "argument `path` must be a string");
}
#[test]
fn find_parse_retains_typed_root_and_display() {
let omitted = parse_find_request(&CborValue::Map(vec![(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*.rs".to_owned()),
)]))
.expect("omitted path parses");
assert_eq!(omitted.path, PathBuf::from("."));
assert_eq!(omitted.display_args, "*.rs in .");
let explicit_path = "./search-root/suffix";
let explicit = parse_find_request(&CborValue::Map(vec![
(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*.rs".to_owned()),
),
(
CborValue::Text("path".to_owned()),
CborValue::Text(explicit_path.to_owned()),
),
]))
.expect("explicit path parses");
assert_eq!(explicit.path, PathBuf::from(explicit_path));
assert_eq!(explicit.display_args, "*.rs in ./search-root/suffix");
}
#[test]
fn find_rejects_non_positive_limit() {
let err =
run_find(&args(CborValue::Integer(0.into()))).expect_err("zero limit should be rejected");
assert_eq!(err.message, "limit must be >= 1");
}
#[test]
fn find_limit_bounds_collected_matches() {
let tempdir = tempfile::TempDir::new().expect("tempdir");
for name in ["alpha.txt", "beta.txt", "gamma.txt"] {
std::fs::write(tempdir.path().join(name), "x").expect("write file");
}
let args = CborValue::Map(vec![
(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*.txt".to_owned()),
),
(
CborValue::Text("path".to_owned()),
CborValue::Text(tempdir.path().display().to_string()),
),
(
CborValue::Text("limit".to_owned()),
CborValue::Integer(1.into()),
),
]);
let result = run_find(&args).expect("find").result;
let CborValue::Map(entries) = &result else {
panic!("expected result map");
};
let output = entries
.iter()
.find_map(|(key, value)| match (key, value) {
(CborValue::Text(key), CborValue::Text(value)) if key == "output" => Some(value),
_ => None,
})
.expect("output");
let matches: i64 = entries
.iter()
.find_map(|(key, value)| match (key, value) {
(CborValue::Text(key), CborValue::Integer(value)) if key == "matches" => {
i128::from(*value).try_into().ok()
}
_ => None,
})
.expect("matches");
let limit_reached = entries.iter().any(|(key, value)| {
matches!(
(key, value),
(CborValue::Text(key), CborValue::Bool(true)) if key == "limit_reached"
)
});
assert_eq!(
output.lines().take_while(|line| !line.is_empty()).count(),
1
);
assert_eq!(matches, 1);
assert!(limit_reached);
assert!(output.contains("1 results limit reached"));
assert_eq!(map_bool(&result, "truncated"), None);
assert_eq!(map_text(&result, "full_output_path"), None);
assert_eq!(map_bool(&result, "saved_output_unavailable"), None);
}
#[test]
fn find_rejects_limit_above_output_cap() {
let err = run_find(&args(CborValue::Integer(
(MAX_FIND_LIMIT as i64 + 1).into(),
)))
.expect_err("limit over cap");
assert_eq!(err.message, format!("limit must be <= {MAX_FIND_LIMIT}"));
}
#[test]
fn find_max_limit_notice_asks_to_refine() {
let notice = limit_reached_notice(MAX_FIND_LIMIT);
assert!(notice.contains("Maximum limit reached"));
assert!(!notice.contains(&format!("limit={}", MAX_FIND_LIMIT * 2)));
}
#[test]
fn find_cancellable_stops_on_early_cancel_request() {
let tempdir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(tempdir.path().join("alpha.txt"), "x").expect("write file");
let args = CborValue::Map(vec![
(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*.txt".to_owned()),
),
(
CborValue::Text("path".to_owned()),
CborValue::Text(tempdir.path().display().to_string()),
),
]);
let (cancel_tx, cancel_rx) = path_std_sync::mpsc::channel();
cancel_tx.send(()).expect("send cancel");
let result = run_find_cancellable(&args, Some(&cancel_rx)).expect("find result");
assert!(matches!(result, CancellableToolRun::Cancelled));
}
#[test]
fn find_collect_stops_when_cancelled_during_traversal() {
let tempdir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(tempdir.path().join("alpha.txt"), "x").expect("write alpha");
std::fs::write(tempdir.path().join("beta.txt"), "x").expect("write beta");
let request = FindRequest {
pattern: "*.txt".to_owned(),
path: tempdir.path().to_owned(),
limit: DEFAULT_FIND_LIMIT,
display_args: "test".to_owned(),
};
let search = prepare_find_search(&request).expect("search");
let mut checks = 0usize;
let result = collect_find_matches(&search, &mut || {
checks += 1;
1 < checks
})
.expect("find collection");
assert!(result.is_none());
}
#[test]
fn find_escapes_control_characters_in_paths() {
let tempdir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(tempdir.path().join("line\nbreak.txt"), "x").expect("write file");
let args = CborValue::Map(vec![
(
CborValue::Text("pattern".to_owned()),
CborValue::Text("*.txt".to_owned()),
),
(
CborValue::Text("path".to_owned()),
CborValue::Text(tempdir.path().display().to_string()),
),
]);
let result = run_find(&args).expect("find").result;
let CborValue::Map(entries) = result else {
panic!("expected result map");
};
let output = entries
.iter()
.find_map(|(key, value)| match (key, value) {
(CborValue::Text(key), CborValue::Text(value)) if key == "output" => Some(value),
_ => None,
})
.expect("output");
assert_eq!(output, "line\\nbreak.txt");
}
#[test]
fn find_notice_overflow_preserves_whole_records_and_exact_artifact() {
let matches = fixed_width_paths(101, &"x".repeat(98));
let selected = matches[..100].to_vec();
let full_output = selected.join("\n");
assert_eq!(full_output.len(), 10_199);
let rendered = render_find_output(find_request(100), matches).result;
let output = map_text(&rendered, "output").expect("output");
let visible_records = output
.split_once("\n\n[")
.expect("notices")
.0
.lines()
.collect::<Vec<_>>();
assert!(output.len() <= MAX_OUTPUT_BYTES);
assert!(output.contains("100 results limit reached"));
assert!(output.contains("10 KiB/2000 line visible output limit reached."));
assert!(visible_records.iter().all(|record| {
*record == "(truncated)" || selected.iter().any(|selected| selected == record)
}));
assert_eq!(map_int(&rendered, "matches"), Some(100));
assert_eq!(map_bool(&rendered, "limit_reached"), Some(true));
assert_eq!(map_bool(&rendered, "truncated"), Some(true));
assert_eq!(map_int(&rendered, "total_lines"), Some(100));
assert_eq!(map_int(&rendered, "total_bytes"), Some(10_199));
if let Some(path) = map_text(&rendered, "full_output_path") {
assert_eq!(
std::fs::read_to_string(path).expect("saved find output"),
full_output
);
} else {
assert_eq!(map_bool(&rendered, "saved_output_unavailable"), Some(true));
}
}
#[test]
fn find_native_output_at_notice_boundary_fits_without_sentinel() {
let matches = fixed_width_paths(100, &"x".repeat(98));
let expected = matches.join("\n");
let rendered = render_find_output(find_request(100), matches).result;
assert_eq!(map_text(&rendered, "output"), Some(expected.as_str()));
assert_eq!(map_bool(&rendered, "limit_reached"), None);
assert_eq!(map_bool(&rendered, "truncated"), None);
assert_eq!(map_text(&rendered, "full_output_path"), None);
}
#[test]
fn find_preexisting_overflow_preserves_native_recovery_output() {
let matches = fixed_width_paths(101, &"x".repeat(118));
let expected = matches[..100].join("\n");
assert!(MAX_OUTPUT_BYTES < expected.len());
let rendered = render_find_output(find_request(100), matches).result;
let output = map_text(&rendered, "output").expect("output");
assert!(output.len() <= MAX_OUTPUT_BYTES);
assert!(output.contains("100 results limit reached"));
assert!(output.contains("10 KiB/2000 line visible output limit reached."));
assert_eq!(map_bool(&rendered, "truncated"), Some(true));
assert_eq!(
map_int(&rendered, "total_bytes"),
Some(expected.len() as i64)
);
if let Some(path) = map_text(&rendered, "full_output_path") {
assert_eq!(
std::fs::read_to_string(path).expect("saved find output"),
expected
);
} else {
assert_eq!(map_bool(&rendered, "saved_output_unavailable"), Some(true));
}
}
#[test]
fn find_notice_overflow_preserves_multibyte_record_boundaries() {
let matches = fixed_width_paths(101, &"é".repeat(49));
let selected = matches[..100].to_vec();
assert_eq!(selected.join("\n").len(), 10_199);
let rendered = render_find_output(find_request(100), matches).result;
let output = map_text(&rendered, "output").expect("output");
let visible_records = output
.split_once("\n\n[")
.expect("notices")
.0
.lines()
.collect::<Vec<_>>();
assert!(output.len() <= MAX_OUTPUT_BYTES);
assert!(visible_records.iter().all(|record| {
*record == "(truncated)" || selected.iter().any(|selected| selected == record)
}));
assert_eq!(map_bool(&rendered, "truncated"), Some(true));
}