#![expect(clippy::unwrap_used)]
#![expect(clippy::create_dir)]
#![expect(clippy::needless_collect)]
#![expect(clippy::case_sensitive_file_extension_comparisons)]
use coding_agent_tools::paths::to_abs_string;
use coding_agent_tools::types::Depth;
use coding_agent_tools::types::Show;
use std::str::FromStr;
#[test]
fn types_exist() {
let _: Depth = Depth::new(1).unwrap();
let _show: Show = Default::default();
}
mod depth_tests {
use super::*;
#[test]
fn depth_validates_bounds() {
assert!(Depth::new(0).is_ok());
assert!(Depth::new(1).is_ok());
assert!(Depth::new(10).is_ok());
assert!(Depth::new(11).is_err());
assert!(Depth::new(255).is_err());
}
#[test]
fn depth_as_u8() {
let d = Depth::new(5).unwrap();
assert_eq!(d.as_u8(), 5);
}
#[test]
fn depth_serde_roundtrip() {
let d: Depth = serde_json::from_str("5").unwrap();
assert_eq!(d.as_u8(), 5);
let json = serde_json::to_string(&d).unwrap();
assert_eq!(json, "5");
}
#[test]
fn depth_serde_rejects_invalid() {
let result: Result<Depth, _> = serde_json::from_str("11");
assert!(result.is_err());
}
#[test]
fn depth_default_is_zero() {
let d: Depth = Default::default();
assert_eq!(d.as_u8(), 0);
}
#[test]
fn depth_json_schema_has_constraints() {
use schemars::schema_for;
let schema = schema_for!(Depth);
let json = serde_json::to_string_pretty(&schema).unwrap();
assert!(json.contains("minimum"));
assert!(json.contains("maximum"));
assert!(json.contains('0'));
assert!(json.contains("10"));
}
}
mod show_tests {
use super::*;
#[test]
fn show_parses_from_str() {
assert!(matches!(Show::from_str("all").unwrap(), Show::All));
assert!(matches!(Show::from_str("ALL").unwrap(), Show::All));
assert!(matches!(Show::from_str("files").unwrap(), Show::Files));
assert!(matches!(Show::from_str("FILES").unwrap(), Show::Files));
assert!(matches!(Show::from_str("dirs").unwrap(), Show::Dirs));
assert!(matches!(Show::from_str("directories").unwrap(), Show::Dirs));
}
#[test]
fn show_rejects_invalid() {
assert!(Show::from_str("invalid").is_err());
assert!(Show::from_str("").is_err());
}
#[test]
fn show_serde_lowercase() {
let json = serde_json::to_string(&Show::Files).unwrap();
assert_eq!(json, r#""files""#);
let json = serde_json::to_string(&Show::Dirs).unwrap();
assert_eq!(json, r#""dirs""#);
let show: Show = serde_json::from_str(r#""all""#).unwrap();
assert!(matches!(show, Show::All));
}
#[test]
fn show_default_is_all() {
let show: Show = Default::default();
assert!(matches!(show, Show::All));
}
}
mod path_tests {
use super::*;
use std::path::Path;
#[test]
fn path_to_abs_string_makes_absolute() {
let abs = to_abs_string("foo/bar").unwrap();
assert!(
Path::new(&abs).is_absolute(),
"expected absolute path, got: {abs}"
);
}
#[test]
fn path_existing_dir_canonicalizes() {
let abs = to_abs_string(".").unwrap();
assert!(Path::new(&abs).is_absolute());
assert!(!abs.ends_with("/."));
}
#[test]
fn path_absolute_stays_absolute() {
let abs = to_abs_string("/tmp/nonexistent/test/path").unwrap();
assert!(Path::new(&abs).is_absolute());
}
}
mod text_format_tests {
use agentic_tools_core::fmt::TextFormat;
use agentic_tools_core::fmt::TextOptions;
use coding_agent_tools::types::EntryKind;
use coding_agent_tools::types::LsEntry;
use coding_agent_tools::types::LsOutput;
#[test]
fn format_header_has_trailing_slash() {
let output = LsOutput {
root: "/home/user/project".into(),
entries: vec![],
has_more: false,
warnings: vec![],
};
let text = output.fmt_text(&TextOptions::default());
assert!(text.starts_with("/home/user/project/"));
}
#[test]
fn format_entries_indented() {
let output = LsOutput {
root: "/test".into(),
entries: vec![
LsEntry {
path: "file.txt".into(),
kind: EntryKind::File,
},
LsEntry {
path: "dir".into(),
kind: EntryKind::Dir,
},
],
has_more: false,
warnings: vec![],
};
let text = output.fmt_text(&TextOptions::default());
assert!(text.contains(" file.txt"));
assert!(text.contains(" dir/"));
}
#[test]
fn format_truncation_notice() {
let output = LsOutput {
root: "/test".into(),
entries: vec![],
has_more: true,
warnings: vec![],
};
let text = output.fmt_text(&TextOptions::default());
assert!(text.contains("truncated"));
assert!(text.contains("call again with same params"));
}
#[test]
fn format_warnings() {
let output = LsOutput {
root: "/test".into(),
entries: vec![],
has_more: false,
warnings: vec!["Permission denied: secret/".into()],
};
let text = output.fmt_text(&TextOptions::default());
assert!(text.contains("Note: Permission denied: secret/"));
}
#[test]
fn format_complete_output() {
let output = LsOutput {
root: "/project".into(),
entries: vec![
LsEntry {
path: "src".into(),
kind: EntryKind::Dir,
},
LsEntry {
path: "README.md".into(),
kind: EntryKind::File,
},
],
has_more: true,
warnings: vec!["Skipped: node_modules".into()],
};
let text = output.fmt_text(&TextOptions::default());
let header_pos = text.find("/project/").unwrap();
let src_pos = text.find(" src/").unwrap();
let readme_pos = text.find(" README.md").unwrap();
let truncated_pos = text.find("truncated").unwrap();
let note_pos = text.find("Note:").unwrap();
assert!(header_pos < src_pos);
assert!(src_pos < readme_pos);
assert!(readme_pos < truncated_pos);
assert!(truncated_pos < note_pos);
}
}
mod walker_integration_tests {
use coding_agent_tools::types::EntryKind;
use coding_agent_tools::types::Show;
use coding_agent_tools::walker::BUILTIN_IGNORES;
use coding_agent_tools::walker::WalkConfig;
use coding_agent_tools::walker::list;
use std::fs;
use tempfile::TempDir;
fn create_test_tree() -> TempDir {
let dir = TempDir::new().unwrap();
let root = dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::create_dir(root.join("tests")).unwrap();
fs::create_dir(root.join(".hidden_dir")).unwrap();
fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(root.join("src/lib.rs"), "// lib").unwrap();
fs::write(root.join("tests/test.rs"), "// test").unwrap();
fs::write(root.join("README.md"), "# README").unwrap();
fs::write(root.join(".hidden_file"), "secret").unwrap();
fs::write(root.join(".hidden_dir/secret.txt"), "very secret").unwrap();
dir
}
#[test]
fn depth_zero_returns_empty() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 0,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
assert!(result.entries.is_empty());
}
#[test]
fn depth_one_lists_immediate_children() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 1,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"src"));
assert!(paths.contains(&"tests"));
assert!(paths.contains(&"README.md"));
assert!(!paths.contains(&".hidden_file"));
assert!(!paths.contains(&".hidden_dir"));
assert!(!paths.iter().any(|p| p.contains('/')));
}
#[test]
fn depth_two_includes_nested() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 2,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"src/main.rs"));
assert!(paths.contains(&"src/lib.rs"));
assert!(paths.contains(&"tests/test.rs"));
}
#[test]
fn show_files_excludes_dirs() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 1,
show: Show::Files,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
for entry in &result.entries {
assert!(
matches!(entry.kind, EntryKind::File | EntryKind::Symlink),
"Expected file, got dir: {}",
entry.path
);
}
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"README.md"));
assert!(!paths.contains(&"src"));
}
#[test]
fn show_dirs_excludes_files() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 1,
show: Show::Dirs,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
for entry in &result.entries {
assert!(
matches!(entry.kind, EntryKind::Dir),
"Expected dir, got file: {}",
entry.path
);
}
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"src"));
assert!(paths.contains(&"tests"));
assert!(!paths.contains(&"README.md"));
}
#[test]
fn hidden_flag_includes_hidden_items() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 1,
show: Show::All,
user_ignores: &[],
include_hidden: true,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&".hidden_file"));
assert!(paths.contains(&".hidden_dir"));
}
#[test]
fn custom_ignore_patterns_work() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 2,
show: Show::All,
user_ignores: &["*.rs".into()],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(!paths.iter().any(|p| p.ends_with(".rs")));
assert!(paths.contains(&"README.md"));
}
#[test]
fn sorting_dirs_first_for_show_all() {
let dir = create_test_tree();
let cfg = WalkConfig {
root: dir.path(),
depth: 1,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let mut seen_file = false;
for entry in &result.entries {
if matches!(entry.kind, EntryKind::File) {
seen_file = true;
} else if matches!(entry.kind, EntryKind::Dir) && seen_file {
panic!("Found directory after file - sorting is wrong");
}
}
}
#[test]
fn builtin_ignores_exist() {
assert!(BUILTIN_IGNORES.contains(&"**/node_modules/**"));
assert!(BUILTIN_IGNORES.contains(&"**/target/**"));
assert!(BUILTIN_IGNORES.contains(&"**/__pycache__/**"));
}
#[test]
fn builtin_ignores_applied() {
let dir = TempDir::new().unwrap();
let root = dir.path();
fs::create_dir(root.join("node_modules")).unwrap();
fs::write(root.join("node_modules/package.json"), "{}").unwrap();
fs::write(root.join("index.js"), "// main").unwrap();
let cfg = WalkConfig {
root,
depth: 2,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(!paths.iter().any(|p| p.contains("node_modules")));
assert!(paths.contains(&"index.js"));
}
#[test]
fn gitignore_file_respected() {
let dir = TempDir::new().unwrap();
let root = dir.path();
fs::create_dir(root.join(".git")).unwrap();
fs::write(root.join(".gitignore"), "ignored_dir/\n*.log\n").unwrap();
fs::create_dir(root.join("ignored_dir")).unwrap();
fs::write(root.join("ignored_dir/secret.txt"), "secret").unwrap();
fs::write(root.join("app.log"), "logs").unwrap();
fs::write(root.join("main.rs"), "fn main() {}").unwrap();
let cfg = WalkConfig {
root,
depth: 2,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(!paths.iter().any(|p| p.contains("ignored_dir")));
assert!(!paths.contains(&"app.log"));
assert!(paths.contains(&"main.rs"));
}
#[test]
#[cfg(unix)]
fn symlink_included_as_entry() {
use std::os::unix::fs::symlink;
let dir = TempDir::new().unwrap();
let root = dir.path();
fs::write(root.join("target.txt"), "target content").unwrap();
symlink(root.join("target.txt"), root.join("link.txt")).unwrap();
let cfg = WalkConfig {
root,
depth: 1,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"link.txt"));
assert!(paths.contains(&"target.txt"));
}
#[test]
#[cfg(unix)]
fn broken_symlink_generates_warning() {
use std::os::unix::fs::symlink;
let dir = TempDir::new().unwrap();
let root = dir.path();
symlink("/nonexistent/path/target.txt", root.join("broken_link.txt")).unwrap();
let cfg = WalkConfig {
root,
depth: 1,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg).unwrap();
assert!(
result.warnings.iter().any(|w| w.contains("broken_link")),
"Expected broken symlink warning, got: {:?}",
result.warnings
);
}
#[test]
#[cfg(unix)]
fn permission_denied_yields_warning() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let root = dir.path();
let restricted = root.join("restricted");
fs::create_dir(&restricted).unwrap();
fs::write(restricted.join("secret.txt"), "secret").unwrap();
let mut perms = fs::metadata(&restricted).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&restricted, perms).unwrap();
let cfg = WalkConfig {
root,
depth: 2,
show: Show::All,
user_ignores: &[],
include_hidden: false,
};
let result = list(&cfg);
let mut perms = fs::metadata(&restricted).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&restricted, perms).unwrap();
let result = result.unwrap();
let paths: Vec<_> = result.entries.iter().map(|e| e.path.as_str()).collect();
assert!(!paths.contains(&"restricted/secret.txt"));
}
}
mod pagination_integration_tests {
use coding_agent_tools::pagination::PAGE_SIZE_ALL;
use coding_agent_tools::pagination::PAGE_SIZE_FILTERED;
use coding_agent_tools::pagination::paginate;
use coding_agent_tools::types::EntryKind;
use coding_agent_tools::types::LsEntry;
fn make_entries(count: usize) -> Vec<LsEntry> {
(0..count)
.map(|i| LsEntry {
path: format!("file_{i:04}.txt"),
kind: EntryKind::File,
})
.collect()
}
#[test]
fn pagination_boundary_100_entries() {
let entries = make_entries(100);
let (page1, has_more) = paginate(entries, 0, PAGE_SIZE_ALL);
assert_eq!(page1.len(), 100);
assert!(!has_more);
}
#[test]
fn pagination_boundary_101_entries() {
let entries = make_entries(101);
let (page1, has_more) = paginate(entries.clone(), 0, PAGE_SIZE_ALL);
assert_eq!(page1.len(), 100);
assert!(has_more);
let (page2, has_more) = paginate(entries, 100, PAGE_SIZE_ALL);
assert_eq!(page2.len(), 1);
assert!(!has_more);
}
#[test]
fn pagination_boundary_1000_entries_filtered() {
let entries = make_entries(1000);
let (page1, has_more) = paginate(entries, 0, PAGE_SIZE_FILTERED);
assert_eq!(page1.len(), 1000);
assert!(!has_more);
}
#[test]
fn pagination_boundary_1001_entries_filtered() {
let entries = make_entries(1001);
let (page1, has_more) = paginate(entries.clone(), 0, PAGE_SIZE_FILTERED);
assert_eq!(page1.len(), 1000);
assert!(has_more);
let (page2, has_more) = paginate(entries, 1000, PAGE_SIZE_FILTERED);
assert_eq!(page2.len(), 1);
assert!(!has_more);
}
#[test]
fn pagination_multiple_pages() {
let entries = make_entries(250);
let (page1, has_more1) = paginate(entries.clone(), 0, PAGE_SIZE_ALL);
assert_eq!(page1.len(), 100);
assert!(has_more1);
assert_eq!(page1[0].path, "file_0000.txt");
let (page2, has_more2) = paginate(entries.clone(), 100, PAGE_SIZE_ALL);
assert_eq!(page2.len(), 100);
assert!(has_more2);
assert_eq!(page2[0].path, "file_0100.txt");
let (page3, has_more3) = paginate(entries, 200, PAGE_SIZE_ALL);
assert_eq!(page3.len(), 50);
assert!(!has_more3);
assert_eq!(page3[0].path, "file_0200.txt");
}
}
mod ls_stateful_pagination_tests {
use coding_agent_tools::CodingAgentTools;
use coding_agent_tools::types::Show;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_files(root: &Path, count: usize) {
for i in 0..count {
let name = format!("file_{i:04}.txt");
fs::write(root.join(name), "x").unwrap();
}
}
#[tokio::test]
async fn ls_auto_paginates_across_calls() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 250);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let out1 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert_eq!(out1.entries.len(), 100);
assert!(out1.has_more, "should have more after first page");
assert_eq!(out1.entries.first().unwrap().path, "file_0000.txt");
assert_eq!(out1.entries.last().unwrap().path, "file_0099.txt");
let out2 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert_eq!(out2.entries.len(), 100);
assert!(out2.has_more, "should have more after second page");
assert_eq!(out2.entries.first().unwrap().path, "file_0100.txt");
assert_eq!(out2.entries.last().unwrap().path, "file_0199.txt");
let out3 = tools.ls(Some(path), None, None, None, None).await.unwrap();
assert_eq!(out3.entries.len(), 50);
assert!(!out3.has_more, "no more pages after last page");
assert_eq!(out3.entries.first().unwrap().path, "file_0200.txt");
assert_eq!(out3.entries.last().unwrap().path, "file_0249.txt");
}
#[tokio::test]
async fn ls_new_params_reset_pagination() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 150);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let _out1 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
let out2 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert_eq!(out2.entries.first().unwrap().path, "file_0100.txt");
let out_reset = tools
.ls(Some(path), None, Some(Show::Files), None, None)
.await
.unwrap();
assert_eq!(out_reset.entries.first().unwrap().path, "file_0000.txt");
assert!(
!out_reset.has_more,
"all files should fit in one page at page_size=1000"
);
assert_eq!(out_reset.entries.len(), 150);
}
}
mod ls_parallel_and_cache_tests {
use coding_agent_tools::CodingAgentTools;
use coding_agent_tools::types::Show;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_files(root: &Path, count: usize) {
for i in 0..count {
let name = format!("file_{i:04}.txt");
fs::write(root.join(name), "x").unwrap();
}
}
#[tokio::test]
async fn parallel_identical_calls_serialize_and_paginate() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 250);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let (a, b) = tokio::join!(
tools.ls(Some(path.clone()), None, None, None, None),
tools.ls(Some(path.clone()), None, None, None, None)
);
let out_a = a.unwrap();
let out_b = b.unwrap();
assert_eq!(out_a.entries.len(), 100);
assert_eq!(out_b.entries.len(), 100);
let set_a: HashSet<_> = out_a.entries.iter().map(|e| e.path.clone()).collect();
let set_b: HashSet<_> = out_b.entries.iter().map(|e| e.path.clone()).collect();
assert!(
set_a.is_disjoint(&set_b),
"Parallel identical calls should return disjoint pages"
);
}
#[tokio::test]
async fn parallel_different_params_do_not_contend() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 150);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let (a, b) = tokio::join!(
tools.ls(Some(path.clone()), None, None, None, None),
tools.ls(Some(path.clone()), None, Some(Show::Files), None, None)
);
let out_a = a.unwrap();
let out_b = b.unwrap();
assert_eq!(out_a.entries.first().unwrap().path, "file_0000.txt");
assert_eq!(out_b.entries.first().unwrap().path, "file_0000.txt");
}
#[tokio::test]
async fn cache_prevents_rescan_between_pages() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 101);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let out1 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert_eq!(out1.entries.len(), 100);
assert!(out1.has_more);
fs::write(root.join("zzz_new.txt"), "x").unwrap();
let out2 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert_eq!(out2.entries.len(), 1);
assert_eq!(out2.entries[0].path, "file_0100.txt");
assert!(!out2.has_more);
}
#[tokio::test]
async fn removal_after_final_page_resets_session() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 150);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let _ = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
let out2 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert!(!out2.has_more, "page 2 should be last page");
let out3 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
assert_eq!(out3.entries.first().unwrap().path, "file_0000.txt");
}
}
mod truncation_sentinel_tests {
use coding_agent_tools::types::TRUNCATION_SENTINEL;
use coding_agent_tools::types::encode_truncation_info;
#[test]
fn encode_format_contains_numbers() {
let s = encode_truncation_info(100, 250, 100);
assert!(s.starts_with(TRUNCATION_SENTINEL));
assert!(s.contains("shown=100"));
assert!(s.contains("total=250"));
assert!(s.contains("page_size=100"));
}
#[test]
fn encode_different_values() {
let s = encode_truncation_info(50, 1000, 50);
assert!(s.contains("shown=50"));
assert!(s.contains("total=1000"));
assert!(s.contains("page_size=50"));
}
}
mod enhanced_truncation_message_tests {
use agentic_tools_core::fmt::TextFormat;
use agentic_tools_core::fmt::TextOptions;
use coding_agent_tools::CodingAgentTools;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_files(root: &Path, count: usize) {
for i in 0..count {
let name = format!("file_{i:04}.txt");
fs::write(root.join(name), "x").unwrap();
}
}
#[tokio::test]
async fn truncation_message_shows_counts_and_pages_remaining() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 250);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let out1 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
let text1 = out1.fmt_text(&TextOptions::default());
assert!(
text1.contains("showing 100 of 250 entries"),
"Expected 'showing 100 of 250 entries' in: {text1}"
);
assert!(
text1.contains("2 pages remaining"),
"Expected '2 pages remaining' in: {text1}"
);
assert!(
text1.contains("REMINDER"),
"Expected REMINDER when >1 page remains in: {text1}"
);
let out2 = tools
.ls(Some(path.clone()), None, None, None, None)
.await
.unwrap();
let text2 = out2.fmt_text(&TextOptions::default());
assert!(
text2.contains("showing 200 of 250 entries"),
"Expected 'showing 200 of 250 entries' in: {text2}"
);
assert!(
text2.contains("1 page remaining"),
"Expected '1 page remaining' (singular) in: {text2}"
);
assert!(
!text2.contains("REMINDER"),
"Should not have REMINDER when only 1 page remains in: {text2}"
);
}
#[tokio::test]
async fn sentinel_not_visible_in_normal_warnings() {
let dir = TempDir::new().unwrap();
let root = dir.path();
create_files(root, 150);
let tools = CodingAgentTools::new();
let path = root.to_string_lossy().to_string();
let out = tools.ls(Some(path), None, None, None, None).await.unwrap();
let text = out.fmt_text(&TextOptions::default());
assert!(
!text.contains("<<<mcp:ls:page_info>>>"),
"Sentinel should not be visible in output: {text}"
);
}
}