#![expect(clippy::unwrap_used)]
#![expect(clippy::create_dir)]
#![expect(clippy::case_sensitive_file_extension_comparisons)]
use coding_agent_tools::types::SortOrder;
use filetime::FileTime;
use filetime::set_file_mtime;
use std::fs;
use std::time::Duration;
use std::time::SystemTime;
use tempfile::TempDir;
fn run_glob(
root: &str,
pattern: &str,
ignore_globs: Vec<String>,
include_hidden: bool,
sort: SortOrder,
head_limit: usize,
offset: usize,
) -> Result<coding_agent_tools::types::GlobOutput, agentic_tools_core::ToolError> {
let cfg = coding_agent_tools::glob::GlobConfig {
root: root.to_string(),
pattern: pattern.to_string(),
ignore_globs,
include_hidden,
sort,
head_limit,
offset,
};
coding_agent_tools::glob::run(cfg)
}
fn setup_test_dir() -> TempDir {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("alpha.txt"), "content").unwrap();
fs::write(tmp.path().join("beta.rs"), "content").unwrap();
fs::write(tmp.path().join("gamma.py"), "content").unwrap();
fs::create_dir(tmp.path().join("subdir")).unwrap();
fs::write(tmp.path().join("subdir/delta.txt"), "content").unwrap();
fs::write(tmp.path().join("subdir/epsilon.rs"), "content").unwrap();
fs::create_dir(tmp.path().join("subdir/nested")).unwrap();
fs::write(tmp.path().join("subdir/nested/zeta.txt"), "content").unwrap();
fs::write(tmp.path().join(".hidden"), "content").unwrap();
tmp
}
#[test]
fn test_glob_basic_pattern() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "*.txt", vec![], false, SortOrder::Name, 500, 0).unwrap();
assert!(
result.entries.iter().any(|p| p == "alpha.txt"),
"Should find alpha.txt"
);
}
#[test]
fn test_glob_recursive_pattern() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "**/*.txt", vec![], false, SortOrder::Name, 500, 0).unwrap();
assert!(
result.entries.iter().any(|p| p == "alpha.txt"),
"Should find alpha.txt"
);
assert!(
result.entries.iter().any(|p| p.contains("delta.txt")),
"Should find subdir/delta.txt"
);
assert!(
result.entries.iter().any(|p| p.contains("zeta.txt")),
"Should find subdir/nested/zeta.txt"
);
}
#[test]
fn test_glob_sort_by_name() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "*.txt", vec![], false, SortOrder::Name, 500, 0).unwrap();
let sorted: Vec<String> = {
let mut v = result.entries.clone();
v.sort_by_key(|a| a.to_lowercase());
v
};
assert_eq!(result.entries, sorted, "Should be alphabetically sorted");
}
#[test]
fn test_glob_sort_by_mtime() {
let tmp = TempDir::new().unwrap();
let now = SystemTime::now();
fs::write(tmp.path().join("old.txt"), "old").unwrap();
let old_time = FileTime::from_system_time(now - Duration::from_secs(3600));
set_file_mtime(tmp.path().join("old.txt"), old_time).unwrap();
fs::write(tmp.path().join("mid.txt"), "mid").unwrap();
let mid_time = FileTime::from_system_time(now - Duration::from_secs(1800));
set_file_mtime(tmp.path().join("mid.txt"), mid_time).unwrap();
fs::write(tmp.path().join("new.txt"), "new").unwrap();
let new_time = FileTime::from_system_time(now);
set_file_mtime(tmp.path().join("new.txt"), new_time).unwrap();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "*.txt", vec![], false, SortOrder::Mtime, 500, 0).unwrap();
assert_eq!(result.entries.len(), 3);
assert_eq!(result.entries[0], "new.txt", "Newest should be first");
assert_eq!(result.entries[1], "mid.txt", "Middle should be second");
assert_eq!(result.entries[2], "old.txt", "Oldest should be last");
}
#[test]
fn test_glob_ignore_patterns() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(
&root,
"**/*",
vec!["*.rs".to_string()], false,
SortOrder::Name,
500,
0,
)
.unwrap();
for path in &result.entries {
assert!(!path.ends_with(".rs"), "Should not match .rs files: {path}");
}
}
#[test]
fn test_glob_include_hidden() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result_no_hidden = run_glob(
&root,
"*",
vec![],
false, SortOrder::Name,
500,
0,
)
.unwrap();
let result_with_hidden = run_glob(
&root,
"*",
vec![],
true, SortOrder::Name,
500,
0,
)
.unwrap();
let has_hidden_no = result_no_hidden.entries.iter().any(|p| p.starts_with('.'));
let has_hidden_yes = result_with_hidden
.entries
.iter()
.any(|p| p.starts_with('.'));
assert!(!has_hidden_no, "Hidden file should not appear without flag");
assert!(has_hidden_yes, "Hidden file should appear with flag");
}
#[test]
fn test_glob_pagination() {
let tmp = TempDir::new().unwrap();
for i in 0..10 {
fs::write(tmp.path().join(format!("file{i:02}.txt")), "content").unwrap();
}
let root = tmp.path().to_string_lossy().to_string();
let result1 = run_glob(
&root,
"*.txt",
vec![],
false,
SortOrder::Name,
3, 0, )
.unwrap();
assert_eq!(result1.entries.len(), 3);
assert!(result1.has_more, "Should have more results");
let result2 = run_glob(
&root,
"*.txt",
vec![],
false,
SortOrder::Name,
3,
3, )
.unwrap();
assert_eq!(result2.entries.len(), 3);
assert!(result2.has_more);
for entry in &result1.entries {
assert!(
!result2.entries.contains(entry),
"Should not have overlapping results"
);
}
}
#[test]
fn test_glob_invalid_pattern() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(
&root,
"[invalid", vec![],
false,
SortOrder::Name,
500,
0,
);
assert!(result.is_err(), "Should fail with invalid pattern");
let err = result.unwrap_err();
assert!(
err.to_string().contains("Invalid glob pattern"),
"Error should mention invalid pattern"
);
}
#[test]
fn test_glob_no_matches() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(
&root,
"*.nonexistent",
vec![],
false,
SortOrder::Name,
500,
0,
)
.unwrap();
assert!(result.entries.is_empty(), "Should have no matches");
assert!(!result.has_more);
}
#[test]
fn test_glob_nonexistent_path() {
let result = run_glob(
"/nonexistent/path/12345",
"*.txt",
vec![],
false,
SortOrder::Name,
500,
0,
);
assert!(result.is_err(), "Should fail with nonexistent path");
let err = result.unwrap_err();
assert!(
err.to_string().contains("does not exist"),
"Error should mention path does not exist"
);
}
#[test]
fn test_glob_matches_directories() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "**/*", vec![], false, SortOrder::Name, 500, 0).unwrap();
assert!(
result
.entries
.iter()
.any(|p| p == "subdir" || p.ends_with("subdir")),
"Should match directories"
);
}
#[test]
fn test_glob_specific_extension() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "**/*.rs", vec![], false, SortOrder::Name, 500, 0).unwrap();
assert!(!result.entries.is_empty(), "Should find .rs files");
for path in &result.entries {
assert!(path.ends_with(".rs"), "Should only match .rs files: {path}");
}
}
#[test]
fn test_glob_builtin_ignores() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join("node_modules")).unwrap();
fs::write(tmp.path().join("node_modules/package.json"), "{}").unwrap();
fs::write(tmp.path().join("app.js"), "content").unwrap();
let root = tmp.path().to_string_lossy().to_string();
let result = run_glob(&root, "**/*", vec![], false, SortOrder::Name, 500, 0).unwrap();
for path in &result.entries {
assert!(
!path.contains("node_modules"),
"Should not match node_modules: {path}"
);
}
assert!(
result.entries.iter().any(|p| p == "app.js"),
"Should find app.js"
);
}