use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;
use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
pub struct GlobTool;
#[derive(Deserialize)]
struct GlobArgs {
pattern: String,
path: Option<String>,
}
#[async_trait]
impl Tool for GlobTool {
fn definition(&self) -> ToolDef {
ToolDef {
name: "glob",
description: "Find files by name pattern. Returns matching file paths.\n\
Use this when you need to find files by name or extension, NOT by content (use grep for content search).\n\
Pattern examples:\n\
- All Rust files: \"**/*.rs\"\n\
- Vue files in views: \"src/views/**/*.vue\"\n\
- Specific filename anywhere: \"**/config.ts\"\n\
- All files in a folder: \"src/components/*\"\n\
Common use cases:\n\
- Find all view/page files before deciding which to edit.\n\
- Find config or entry files in an unfamiliar project.\n\
- Check what files exist in a directory.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"pattern": { "type": "string", "description": "Glob pattern (e.g. **/*.rs, src/**/*.ts)" },
"path": { "type": "string", "description": "Base directory (default: working directory)" }
},
"required": ["pattern"]
}),
}
}
fn approval(&self, _args: &str) -> ApprovalRequirement {
ApprovalRequirement::AutoApprove
}
fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
let parsed = match serde_json::from_str::<GlobArgs>(args) {
Ok(parsed) => parsed,
Err(_) => return self.approval(args),
};
let working_dir = match ctx.working_dir.try_read() {
Ok(wd) => wd.clone(),
Err(_) => return self.approval(args),
};
let base_dir =
match super::inspect_path_access(parsed.path.as_deref().unwrap_or("."), &working_dir) {
Ok(access) => access.path.to_string_lossy().to_string(),
Err(_) => return self.approval(args),
};
let search_dir = derive_search_dir(&base_dir, &parsed.pattern);
match super::approval_for_path(
&search_dir,
&working_dir,
super::ExternalPathAction::Enumerate,
) {
Ok(approval) => approval,
Err(_) => self.approval(args),
}
}
async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
let parsed: GlobArgs = serde_json::from_str(args)?;
let wd = ctx.working_dir.read().await.clone();
let base_dir = match super::inspect_path_access(parsed.path.as_deref().unwrap_or("."), &wd)
{
Ok(access) => access.path.to_string_lossy().to_string(),
Err(err) => {
return Ok(ToolResult {
call_id: String::new(),
output: err.to_string(),
success: false,
});
}
};
let search_dir = derive_search_dir(&base_dir, &parsed.pattern);
let name_pattern = derive_name_pattern(&parsed.pattern);
let pattern = parsed.pattern.clone();
let output = tokio::task::spawn_blocking(move || {
glob_search(&search_dir, &name_pattern, &wd, &pattern)
})
.await
.unwrap_or_else(|_| format!("No files matching '{}' (search task failed)", parsed.pattern));
Ok(ToolResult {
call_id: String::new(),
output,
success: true,
})
}
}
fn glob_search(
search_dir: &str,
name_pattern: &str,
wd: &std::path::Path,
pattern: &str,
) -> String {
if !std::path::Path::new(search_dir).is_dir() {
let target_basename = std::path::Path::new(search_dir)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let mut dir_matches: Vec<String> = Vec::new();
if !target_basename.is_empty() {
fn find_dir(
dir: &std::path::Path,
target: &str,
depth: usize,
max_depth: usize,
results: &mut Vec<String>,
) {
if depth > max_depth || results.len() >= 20 {
return;
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') || super::should_skip_dir(&name) {
continue;
}
let p = entry.path();
if p.is_dir() {
if name == target {
results.push(p.to_string_lossy().to_string());
}
find_dir(&p, target, depth + 1, max_depth, results);
}
}
}
}
find_dir(wd, &target_basename, 0, 5, &mut dir_matches);
}
let hint = if dir_matches.is_empty() {
String::new()
} else {
dir_matches.sort_by_key(|d| std::cmp::Reverse(super::shared_prefix_len(search_dir, d)));
let shown: Vec<String> = dir_matches
.iter()
.take(3)
.map(|d| format!(" {}", d))
.collect();
format!(
"\n\nSimilar directories found — did you mean one of these?\n{}",
shown.join("\n")
)
};
return format!(
"No files matching '{}' (directory '{}' does not exist){}",
pattern, search_dir, hint
);
}
let mut files: Vec<String> = Vec::new();
let search_path = std::path::Path::new(search_dir);
let walker = ignore::WalkBuilder::new(search_path)
.hidden(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.build();
for entry in walker.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
if should_skip_below(search_path, path) {
continue;
}
if let Some(file_name) = path.file_name() {
let name = file_name.to_string_lossy();
if simple_glob_match(&name, name_pattern) {
files.push(path.to_string_lossy().to_string());
}
}
}
files.sort();
if files.is_empty() {
format!("No files matching '{}'", pattern)
} else {
let total = files.len();
let shown: Vec<&str> = files.iter().take(100).map(|s| s.as_str()).collect();
let mut out = shown.join("\n");
if total > 100 {
out.push_str(&format!("\n\n[{} more files not shown]", total - 100));
}
format!("{} files found:\n{}", total, out)
}
}
fn should_skip_path(path: &std::path::Path) -> bool {
for component in path.components() {
if let std::path::Component::Normal(os_name) = component {
let name = os_name.to_string_lossy();
if super::should_skip_dir(&name) {
return true;
}
}
}
false
}
fn should_skip_below(root: &std::path::Path, path: &std::path::Path) -> bool {
should_skip_path(path.strip_prefix(root).unwrap_or(path))
}
fn simple_glob_match(name: &str, pattern: &str) -> bool {
let pat_parts: Vec<&str> = pattern.split('*').collect();
if pat_parts.len() == 1 {
return name == pattern;
}
let leading_wildcard = pattern.starts_with('*');
let trailing_wildcard = pattern.ends_with('*');
let mut rest = name;
for (i, part) in pat_parts.iter().enumerate() {
if part.is_empty() {
continue;
}
match rest.find(part) {
Some(pos) => {
if i == 0 && !leading_wildcard && pos != 0 {
return false;
}
rest = &rest[pos + part.len()..];
}
None => return false,
}
}
if let Some(last) = pat_parts.last() {
if !last.is_empty() && !trailing_wildcard && !name.ends_with(last) {
return false;
}
}
true
}
fn derive_search_dir(base_dir: &str, pattern: &str) -> String {
if let Some(star_pos) = pattern.find("**/") {
let dir_part = pattern[..star_pos].trim_end_matches('/');
if dir_part.is_empty() {
base_dir.to_string()
} else if std::path::Path::new(dir_part).is_absolute() {
dir_part.to_string()
} else {
std::path::Path::new(base_dir)
.join(dir_part)
.to_string_lossy()
.to_string()
}
} else if let Some(last_slash) = pattern.rfind('/') {
let dir_part = &pattern[..last_slash];
if std::path::Path::new(dir_part).is_absolute() {
dir_part.to_string()
} else {
std::path::Path::new(base_dir)
.join(dir_part)
.to_string_lossy()
.to_string()
}
} else {
base_dir.to_string()
}
}
fn derive_name_pattern(pattern: &str) -> String {
if let Some(star_pos) = pattern.find("**/") {
let after_stars = &pattern[star_pos + 3..];
after_stars
.rsplit('/')
.next()
.unwrap_or(after_stars)
.to_string()
} else if let Some(last_slash) = pattern.rfind('/') {
pattern[last_slash + 1..].to_string()
} else {
pattern.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tool::ToolContext;
use tempfile::TempDir;
#[tokio::test]
async fn glob_suggests_similar_directory_when_search_dir_missing() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("hermes/presentation")).unwrap();
std::fs::create_dir_all(dir.path().join("other/presentation")).unwrap();
std::fs::write(
dir.path().join("hermes/presentation/app.vue"),
"<template></template>",
)
.unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let wrong = dir.path().join("hermes/frontend/presentation");
let args = format!(r#"{{"pattern":"{}/**/*.vue"}}"#, wrong.display());
let r = tool.execute(&args, &ctx).await.unwrap();
assert!(r.success);
assert!(
r.output.contains("does not exist"),
"missing exists-check msg: {}",
r.output
);
assert!(
r.output.contains("Similar directories found"),
"must suggest similar directories: {}",
r.output
);
let hermes_pos = r.output.find("hermes/presentation").unwrap();
let other_pos = r.output.find("other/presentation").unwrap();
assert!(
hermes_pos < other_pos,
"hermes/presentation must outrank other/presentation. output:\n{}",
r.output
);
}
#[tokio::test]
async fn glob_existing_dir_does_not_trigger_hint() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/a.ts"), "export {};").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let args = format!(
r#"{{"pattern":"{}/**/*.ts"}}"#,
dir.path().join("src").display()
);
let r = tool.execute(&args, &ctx).await.unwrap();
assert!(r.success);
assert!(
!r.output.contains("Similar directories found"),
"no hint should fire when dir exists: {}",
r.output
);
}
#[test]
fn derive_name_pattern_chinese_filename() {
assert_eq!(derive_name_pattern("*.txt"), "*.txt");
assert_eq!(derive_name_pattern("**/*.txt"), "*.txt");
assert_eq!(derive_name_pattern("**/测试.txt"), "测试.txt");
assert_eq!(derive_name_pattern("src/**/配置.json"), "配置.json");
assert_eq!(derive_name_pattern("数据/**/*.csv"), "*.csv");
assert_eq!(derive_name_pattern("测试.txt"), "测试.txt");
}
#[test]
fn derive_search_dir_chinese_path() {
let base = "/tmp/workspace";
assert_eq!(derive_search_dir(base, "**/*.txt"), base);
assert_eq!(derive_search_dir(base, "测试/**/*.txt"), format!("{}/测试", base));
assert_eq!(
derive_search_dir(base, "项目/模块/**/*.rs"),
format!("{}/项目/模块", base)
);
}
#[test]
fn derive_name_pattern_mixed_chinese_english() {
assert_eq!(derive_name_pattern("**/report-报告.txt"), "report-报告.txt");
assert_eq!(derive_name_pattern("**/测试_test.py"), "测试_test.py");
assert_eq!(derive_name_pattern("src/组件/**/Button*.vue"), "Button*.vue");
}
#[test]
fn derive_name_pattern_chinese_with_star() {
assert_eq!(derive_name_pattern("**/*测试*"), "*测试*");
assert_eq!(derive_name_pattern("**/测试*.log"), "测试*.log");
assert_eq!(derive_name_pattern("**/*.配置"), "*.配置");
}
#[test]
fn derive_name_pattern_chinese_edge_cases() {
assert_eq!(derive_name_pattern("中文/路径/**/文件.rs"), "文件.rs");
assert_eq!(derive_name_pattern("**/*配置*"), "*配置*");
assert_eq!(derive_name_pattern("模块/**/index.ts"), "index.ts");
assert_eq!(derive_name_pattern("项目/源码/核心/**/*.rs"), "*.rs");
}
#[test]
fn derive_search_dir_chinese_edge_cases() {
let base = "/home/user/project";
assert_eq!(
derive_search_dir(base, "模块/**/index.ts"),
format!("{}/模块", base)
);
assert_eq!(
derive_search_dir(base, "项目/源码/核心/**/*.rs"),
format!("{}/项目/源码/核心", base)
);
assert_eq!(
derive_search_dir(base, "/绝对/路径/**/*.java"),
"/绝对/路径"
);
}
#[test]
fn test_simple_glob_match_star_extension() {
assert!(simple_glob_match("测试.txt", "*.txt"));
assert!(simple_glob_match("test.txt", "*.txt"));
assert!(simple_glob_match("hello.rs", "*.rs"));
assert!(!simple_glob_match("hello.md", "*.txt"));
}
#[test]
fn test_simple_glob_match_exact_name() {
assert!(simple_glob_match("测试.txt", "测试.txt"));
assert!(simple_glob_match("test.txt", "test.txt"));
assert!(!simple_glob_match("其他.txt", "测试.txt"));
}
#[test]
fn test_simple_glob_match_prefix_star() {
assert!(simple_glob_match("report-报告.txt", "*报告.txt"));
assert!(simple_glob_match("最终报告.txt", "*报告.txt"));
assert!(!simple_glob_match("report.txt", "*报告.txt"));
}
#[test]
fn test_simple_glob_match_middle_star() {
assert!(simple_glob_match("测试_test.py", "测试*.py"));
assert!(simple_glob_match("测试v2_test.py", "测试*.py"));
assert!(!simple_glob_match("test.py", "测试*.py"));
}
#[test]
fn test_simple_glob_match_star_only() {
assert!(simple_glob_match("anything.txt", "*"));
assert!(simple_glob_match("测试.txt", "*"));
}
#[test]
fn test_simple_glob_match_trailing_star() {
assert!(simple_glob_match("测试_file.txt", "测试*"));
assert!(simple_glob_match("测试", "测试*"));
assert!(!simple_glob_match("other.txt", "测试*"));
}
#[test]
fn test_simple_glob_match_multiple_stars() {
assert!(simple_glob_match("我的测试文件.txt", "*测试*"));
assert!(simple_glob_match("测试.txt", "*测试*"));
assert!(simple_glob_match("abc测试def", "*测试*"));
assert!(!simple_glob_match("abc.txt", "*测试*"));
}
#[test]
fn test_should_skip_path_node_modules() {
assert!(should_skip_path(std::path::Path::new("/project/node_modules/pkg/file.js")));
}
#[test]
fn test_should_skip_path_git() {
assert!(should_skip_path(std::path::Path::new("/project/.git/HEAD")));
}
#[test]
fn test_should_skip_path_target() {
assert!(should_skip_path(std::path::Path::new("/project/target/debug/app")));
}
#[test]
fn test_should_skip_path_normal() {
assert!(!should_skip_path(std::path::Path::new("/project/src/main.rs")));
assert!(!should_skip_path(std::path::Path::new("/project/测试.txt")));
}
#[test]
fn test_should_skip_path_venv_prefix() {
assert!(should_skip_path(std::path::Path::new("/project/.venv-test/lib/python.py")));
}
#[tokio::test]
async fn glob_finds_chinese_filename() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("test.txt"), "english file").unwrap();
std::fs::write(dir.path().join("测试.txt"), "chinese file").unwrap();
std::fs::write(dir.path().join("README.md"), "readme").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let args = r#"{"pattern":"*.txt"}"#;
let r = tool.execute(args, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("测试.txt"),
"glob must find Chinese-named file '测试.txt'. output: {}",
r.output
);
assert!(
r.output.contains("test.txt"),
"glob must find English-named file 'test.txt'. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_finds_chinese_filename_recursive() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("源码")).unwrap();
std::fs::write(dir.path().join("源码/主程序.rs"), "fn main() {}").unwrap();
std::fs::write(dir.path().join("源码/utils.rs"), "pub fn helper() {}").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let args = r#"{"pattern":"**/*.rs"}"#;
let r = tool.execute(args, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("主程序.rs"),
"glob must find Chinese-named Rust file '主程序.rs'. output: {}",
r.output
);
assert!(
r.output.contains("utils.rs"),
"glob must find English-named Rust file 'utils.rs'. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_finds_files_under_chinese_directory() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("组件")).unwrap();
std::fs::create_dir_all(dir.path().join("components")).unwrap();
std::fs::write(dir.path().join("组件/按钮.vue"), "<template></template>").unwrap();
std::fs::write(dir.path().join("components/Button.vue"), "<template></template>").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let args = r#"{"pattern":"**/*.vue"}"#;
let r = tool.execute(args, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("按钮.vue"),
"glob must find file under Chinese directory. output: {}",
r.output
);
assert!(
r.output.contains("Button.vue"),
"glob must find file under English directory. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_finds_files_when_explicitly_targeting_skip_listed_dotdir() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".atomcode/skills/coding")).unwrap();
std::fs::write(
dir.path().join(".atomcode/skills/coding/SKILL.md"),
"# skill",
)
.unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool
.execute(r#"{"pattern":".atomcode/skills/**/*.md"}"#, &ctx)
.await
.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("SKILL.md"),
"glob must find files under an explicitly targeted .atomcode/. output: {}",
r.output
);
let r2 = tool
.execute(r#"{"pattern":"**/*.md","path":".atomcode/skills"}"#, &ctx)
.await
.unwrap();
assert!(r2.success, "glob should succeed: {}", r2.output);
assert!(
r2.output.contains("SKILL.md"),
"glob must find files when path explicitly points into .atomcode/. output: {}",
r2.output
);
}
#[tokio::test]
async fn glob_broad_search_still_skips_dotatomcode() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".atomcode/skills")).unwrap();
std::fs::write(dir.path().join(".atomcode/skills/internal.md"), "x").unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/visible.md"), "y").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool.execute(r#"{"pattern":"**/*.md"}"#, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("visible.md"),
"broad glob must find visible files. output: {}",
r.output
);
assert!(
!r.output.contains("internal.md"),
"broad glob must still skip .atomcode/. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_pattern_with_chinese_name() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("测试.txt"), "chinese content").unwrap();
std::fs::write(dir.path().join("test.txt"), "english content").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let args = r#"{"pattern":"**/测试.txt"}"#;
let r = tool.execute(args, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("测试.txt"),
"glob must find '测试.txt' when searching for it by name. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_chinese_subdir_pattern() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("文档")).unwrap();
std::fs::create_dir_all(dir.path().join("docs")).unwrap();
std::fs::write(dir.path().join("文档/说明.md"), "# 说明").unwrap();
std::fs::write(dir.path().join("文档/指南.md"), "# 指南").unwrap();
std::fs::write(dir.path().join("docs/README.md"), "# README").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let args = r#"{"pattern":"文档/**/*.md"}"#;
let r = tool.execute(args, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("说明.md"),
"glob must find '说明.md' under '文档' dir. output: {}",
r.output
);
assert!(
r.output.contains("指南.md"),
"glob must find '指南.md' under '文档' dir. output: {}",
r.output
);
assert!(
!r.output.contains("README.md"),
"glob must NOT find files from 'docs' dir when pattern is '文档/**/*.md'. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_finds_chinese_file_in_chinese_subdirectory() {
let dir = TempDir::new().unwrap();
let subdir_name = "TE微型直线导轨 滑块标准型[TE7C1R-129-G]";
std::fs::create_dir_all(dir.path().join(subdir_name)).unwrap();
std::fs::write(
dir.path().join(format!("{}/测试.txt", subdir_name)),
"test content",
)
.unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool.execute(r#"{"pattern":"**/*.txt"}"#, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("测试.txt"),
"glob must find '测试.txt' in Chinese subdirectory. output: {}",
r.output
);
let r = tool.execute(r#"{"pattern":"**/测试.txt"}"#, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("测试.txt"),
"glob must find '测试.txt' by exact Chinese name. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_non_recursive_pattern_finds_root_files_only() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("test.txt"), "root file").unwrap();
std::fs::create_dir_all(dir.path().join("子目录")).unwrap();
std::fs::write(dir.path().join("子目录/测试.txt"), "nested file").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool.execute(r#"{"pattern":"*.txt"}"#, &ctx).await.unwrap();
assert!(r.success, "glob should succeed: {}", r.output);
assert!(
r.output.contains("test.txt"),
"glob must find 'test.txt' in root. output: {}",
r.output
);
assert!(
r.output.contains("测试.txt"),
"glob must find '测试.txt' in subdirectory. output: {}",
r.output
);
}
#[tokio::test]
async fn glob_mixed_chinese_english_filenames() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("index.ts"), "export {};").unwrap();
std::fs::write(dir.path().join("索引.ts"), "export {};").unwrap();
std::fs::write(dir.path().join("app.config.js"), "module.exports = {};").unwrap();
std::fs::write(dir.path().join("配置.js"), "module.exports = {};").unwrap();
std::fs::write(dir.path().join("utils-helper.py"), "def help(): pass").unwrap();
std::fs::write(dir.path().join("工具_辅助.py"), "def help(): pass").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool.execute(r#"{"pattern":"*.ts"}"#, &ctx).await.unwrap();
assert!(r.output.contains("索引.ts"), "must find '索引.ts': {}", r.output);
assert!(r.output.contains("index.ts"), "must find 'index.ts': {}", r.output);
let r = tool.execute(r#"{"pattern":"*.py"}"#, &ctx).await.unwrap();
assert!(r.output.contains("工具_辅助.py"), "must find '工具_辅助.py': {}", r.output);
assert!(r.output.contains("utils-helper.py"), "must find 'utils-helper.py': {}", r.output);
}
#[tokio::test]
async fn glob_unicode_filenames_japanese_korean_emoji() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("テスト.txt"), "japanese").unwrap();
std::fs::write(dir.path().join("테스트.txt"), "korean").unwrap();
std::fs::write(dir.path().join("🎉party.txt"), "emoji").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool.execute(r#"{"pattern":"*.txt"}"#, &ctx).await.unwrap();
assert!(r.output.contains("テスト.txt"), "must find Japanese filename: {}", r.output);
assert!(r.output.contains("테스트.txt"), "must find Korean filename: {}", r.output);
assert!(r.output.contains("🎉party.txt"), "must find emoji filename: {}", r.output);
}
#[test]
fn glob_uses_ignore_crate_not_find_command() {
let source = include_str!("glob.rs");
assert!(
source.contains("ignore::WalkBuilder"),
"glob.rs must use `ignore::WalkBuilder` for cross-platform file search (Issue #350)."
);
}
#[tokio::test]
async fn glob_skips_node_modules_and_target() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("node_modules/pkg")).unwrap();
std::fs::create_dir_all(dir.path().join("target/debug")).unwrap();
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}").unwrap();
std::fs::write(dir.path().join("node_modules/pkg/index.js"), "export {};").unwrap();
std::fs::write(dir.path().join("target/debug/output.txt"), "binary").unwrap();
let ctx = ToolContext::new(dir.path().to_path_buf());
let tool = GlobTool;
let r = tool.execute(r#"{"pattern":"**/*"}"#, &ctx).await.unwrap();
assert!(
r.output.contains("main.rs"),
"must find source files. output: {}",
r.output
);
assert!(
!r.output.contains("node_modules"),
"must skip node_modules. output: {}",
r.output
);
assert!(
!r.output.contains("target"),
"must skip target. output: {}",
r.output
);
}
}