use serde_json::json;
use super::{
ToolRuntime,
args::ListFilesArgs,
contract::{metadata_key as meta, tool_name},
result::{ToolResult, ToolResultDisplay},
};
const MAX_FILES: usize = 1000;
impl ToolRuntime {
pub(super) fn list_files(&self, args: ListFilesArgs) -> anyhow::Result<ToolResult> {
let path = self.resolve_existing_path(
&args.path,
super::fs::ExistingPathPolicy::list_files(self.list_files_absolute_paths),
)?;
if !path.is_dir() {
anyhow::bail!("list_files path must be a directory");
}
let listing = self.fs_cache.read_dir_listing(&path)?;
let mut entries = Vec::new();
let mut file_count = 0usize;
let mut directory_count = 0usize;
let mut truncated = false;
for entry in listing.entries {
if entry.file_type.is_file() || (args.include_directories && entry.file_type.is_dir()) {
if entries.len() == MAX_FILES {
truncated = true;
break;
}
if entry.file_type.is_dir() {
directory_count += 1;
} else {
file_count += 1;
}
entries.push(entry.name);
}
}
entries.sort();
Ok(ToolResult {
tool_name: tool_name::LIST_FILES.to_string(),
success: true,
content: entries.join("\n"),
metadata: json!({
(meta::PATH): args.path,
(meta::FILES): file_count,
(meta::DIRECTORIES): directory_count,
(meta::TRUNCATED): truncated
}),
display: ToolResultDisplay::default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ToolRuntime;
use std::fs;
#[test]
fn list_files_repeated_call_uses_cache_without_output_change() {
let cwd = tempfile::TempDir::new().unwrap();
fs::write(cwd.path().join("b.txt"), "content").unwrap();
let runtime = ToolRuntime::new(cwd.path()).unwrap();
let first = runtime
.list_files(ListFilesArgs {
path: ".".to_string(),
include_directories: false,
})
.unwrap();
let second = runtime
.list_files(ListFilesArgs {
path: ".".to_string(),
include_directories: false,
})
.unwrap();
let listing = runtime.fs_cache.read_dir_listing(cwd.path()).unwrap();
assert_eq!(first.content, "b.txt");
assert_eq!(first.content, second.content);
assert_eq!(first.metadata, second.metadata);
assert!(listing.cache_hit);
}
#[test]
fn list_files_includes_directories_when_enabled() {
let cwd = tempfile::TempDir::new().unwrap();
fs::write(cwd.path().join("b.txt"), "content").unwrap();
fs::create_dir(cwd.path().join("a_dir")).unwrap();
let runtime = ToolRuntime::new(cwd.path()).unwrap();
let files_only = runtime
.list_files(ListFilesArgs {
path: ".".to_string(),
include_directories: false,
})
.unwrap();
assert_eq!(files_only.content, "b.txt");
assert_eq!(files_only.metadata[meta::FILES], 1);
assert_eq!(files_only.metadata[meta::DIRECTORIES], 0);
let with_directories = runtime
.list_files(ListFilesArgs {
path: ".".to_string(),
include_directories: true,
})
.unwrap();
assert_eq!(with_directories.content, "a_dir\nb.txt");
assert_eq!(with_directories.metadata[meta::FILES], 1);
assert_eq!(with_directories.metadata[meta::DIRECTORIES], 1);
}
#[test]
fn list_files_allows_absolute_outside_cwd_when_setting_true() {
let cwd = tempfile::TempDir::new().unwrap();
let outside = tempfile::TempDir::new().unwrap();
fs::write(outside.path().join("visible.txt"), "content").unwrap();
fs::create_dir(outside.path().join("hidden_dir")).unwrap();
let mut runtime = ToolRuntime::new(cwd.path()).unwrap();
runtime.list_files_absolute_paths = true;
let result = runtime
.list_files(ListFilesArgs {
path: outside.path().to_string_lossy().into_owned(),
include_directories: false,
})
.unwrap();
assert_eq!(result.content, "visible.txt");
}
#[test]
fn list_files_rejects_absolute_outside_cwd_when_setting_false() {
let cwd = tempfile::TempDir::new().unwrap();
let outside = tempfile::TempDir::new().unwrap();
let mut runtime = ToolRuntime::new(cwd.path()).unwrap();
runtime.list_files_absolute_paths = false;
let error = runtime
.list_files(ListFilesArgs {
path: outside.path().to_string_lossy().into_owned(),
include_directories: false,
})
.unwrap_err()
.to_string();
assert!(error.contains("tools.list_files.absolute_paths"), "{error}");
}
}