use std::fs;
use tempfile::tempdir;
use collet::tools::registry;
#[tokio::test]
async fn bash_echo_hello() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let result = registry::dispatch("bash", r#"{"command": "echo hello"}"#, wd)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["exit_code"], 0);
assert_eq!(parsed["stdout"].as_str().unwrap().trim(), "hello");
}
#[tokio::test]
async fn bash_nonzero_exit_code() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let result = registry::dispatch("bash", r#"{"command": "exit 42"}"#, wd)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["exit_code"], 42);
}
#[tokio::test]
async fn bash_captures_stderr() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let result = registry::dispatch("bash", r#"{"command": "echo oops >&2"}"#, wd)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(parsed["stderr"].as_str().unwrap().contains("oops"));
}
#[tokio::test]
async fn bash_runs_in_working_dir() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let result = registry::dispatch("bash", r#"{"command": "pwd"}"#, wd)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let stdout = parsed["stdout"].as_str().unwrap().trim();
let expected = fs::canonicalize(dir.path()).unwrap();
let actual = fs::canonicalize(stdout).unwrap();
assert_eq!(actual, expected);
}
#[tokio::test]
async fn file_write_then_read_round_trip() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let write_args = serde_json::json!({
"path": "test_file.txt",
"content": "line one\nline two\nline three\n"
});
let write_result = registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
assert!(write_result.contains("Successfully wrote"));
let read_args = serde_json::json!({ "path": "test_file.txt" });
let read_result = registry::dispatch("file_read", &read_args.to_string(), wd)
.await
.unwrap();
assert!(read_result.contains("line one"));
assert!(read_result.contains("line two"));
assert!(read_result.contains("line three"));
}
#[tokio::test]
async fn file_write_creates_subdirectories() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let write_args = serde_json::json!({
"path": "deep/nested/dir/file.txt",
"content": "nested content"
});
let result = registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
assert!(result.contains("Successfully wrote"));
let read_args = serde_json::json!({ "path": "deep/nested/dir/file.txt" });
let content = registry::dispatch("file_read", &read_args.to_string(), wd)
.await
.unwrap();
assert!(content.contains("nested content"));
}
#[tokio::test]
async fn file_read_with_offset_and_limit() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let write_args = serde_json::json!({
"path": "lines.txt",
"content": "alpha\nbeta\ngamma\ndelta\nepsilon\n"
});
registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
let read_args = serde_json::json!({
"path": "lines.txt",
"offset": 2,
"limit": 2
});
let result = registry::dispatch("file_read", &read_args.to_string(), wd)
.await
.unwrap();
assert!(result.contains("beta"));
assert!(result.contains("gamma"));
assert!(!result.contains("alpha"), "offset should skip first line");
}
#[tokio::test]
async fn file_read_nonexistent_returns_error() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let read_args = serde_json::json!({ "path": "nonexistent.txt" });
let result = registry::dispatch("file_read", &read_args.to_string(), wd).await;
assert!(result.is_err());
}
#[tokio::test]
async fn file_edit_replaces_exact_string() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let write_args = serde_json::json!({
"path": "editable.txt",
"content": "Hello, World!\nThis is a test.\nGoodbye.\n"
});
registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
let edit_args = serde_json::json!({
"path": "editable.txt",
"old_string": "This is a test.",
"new_string": "This has been edited."
});
let result = registry::dispatch("file_edit", &edit_args.to_string(), wd)
.await
.unwrap();
assert!(result.contains("Successfully edited"));
let read_args = serde_json::json!({ "path": "editable.txt" });
let content = registry::dispatch("file_read", &read_args.to_string(), wd)
.await
.unwrap();
assert!(content.contains("This has been edited."));
assert!(!content.contains("This is a test."));
assert!(content.contains("Hello, World!"));
assert!(content.contains("Goodbye."));
}
#[tokio::test]
async fn file_edit_fails_when_old_string_not_found() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let write_args = serde_json::json!({
"path": "stable.txt",
"content": "original content\n"
});
registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
let edit_args = serde_json::json!({
"path": "stable.txt",
"old_string": "this does not exist",
"new_string": "replacement"
});
let result = registry::dispatch("file_edit", &edit_args.to_string(), wd).await;
assert!(
result.is_err(),
"edit should fail when old_string is not found"
);
}
#[tokio::test]
async fn file_edit_fails_when_old_string_is_ambiguous() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let write_args = serde_json::json!({
"path": "ambiguous.txt",
"content": "foo bar foo\n"
});
registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
let edit_args = serde_json::json!({
"path": "ambiguous.txt",
"old_string": "foo",
"new_string": "baz"
});
let result = registry::dispatch("file_edit", &edit_args.to_string(), wd).await;
assert!(
result.is_err(),
"edit should fail when old_string matches multiple times"
);
}
#[tokio::test]
async fn search_finds_pattern_in_files() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
fs::write(
dir.path().join("a.txt"),
"the quick brown fox\njumps over\n",
)
.unwrap();
fs::write(dir.path().join("b.txt"), "the lazy dog\nfox trot\n").unwrap();
let args = serde_json::json!({
"pattern": "fox",
"path": wd
});
let result = registry::dispatch("search", &args.to_string(), wd)
.await
.unwrap();
assert!(result.contains("fox"), "search should find 'fox'");
assert!(result.contains("a.txt"));
assert!(result.contains("b.txt"));
}
#[tokio::test]
async fn search_with_glob_filter() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
fs::write(dir.path().join("code.rs"), "fn main() { todo!() }\n").unwrap();
fs::write(dir.path().join("notes.txt"), "todo: review code\n").unwrap();
let args = serde_json::json!({
"pattern": "todo",
"path": wd,
"glob": "*.rs"
});
let result = registry::dispatch("search", &args.to_string(), wd)
.await
.unwrap();
assert!(result.contains("code.rs"), "should find match in .rs file");
assert!(
!result.contains("notes.txt"),
"glob filter should exclude .txt files"
);
}
#[tokio::test]
async fn search_no_matches_returns_no_matches_message() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
fs::write(dir.path().join("file.txt"), "hello world\n").unwrap();
let args = serde_json::json!({
"pattern": "zzz_nonexistent_pattern_zzz",
"path": wd
});
let result = registry::dispatch("search", &args.to_string(), wd)
.await
.unwrap();
assert!(
result.contains("No matches found"),
"should indicate no matches"
);
}
#[tokio::test]
async fn dispatch_unknown_tool_returns_error() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let result = registry::dispatch("nonexistent_tool", "{}", wd).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Unknown tool"),
"error should mention unknown tool"
);
}
#[test]
fn all_tool_definitions_is_nonempty() {
let defs = registry::all_tool_definitions(false, false);
assert!(defs.len() >= 5, "should have at least 5 tool definitions");
for def in &defs {
let name = def["function"]["name"].as_str();
assert!(
name.is_some(),
"each tool definition should have a function name"
);
}
}
#[test]
fn tool_definitions_contain_expected_tools() {
let defs = registry::all_tool_definitions(false, false);
let names: Vec<String> = defs
.iter()
.filter_map(|d| d["function"]["name"].as_str().map(String::from))
.collect();
assert!(names.contains(&"bash".to_string()));
assert!(names.contains(&"file_read".to_string()));
assert!(names.contains(&"file_write".to_string()));
assert!(names.contains(&"file_edit".to_string()));
assert!(names.contains(&"search".to_string()));
}
#[tokio::test]
async fn file_write_and_read_with_absolute_path() {
let dir = tempdir().unwrap();
let wd = dir.path().to_str().unwrap();
let abs_path = dir.path().join("abs_test.txt");
let write_args = serde_json::json!({
"path": abs_path.to_str().unwrap(),
"content": "absolute path content"
});
registry::dispatch("file_write", &write_args.to_string(), wd)
.await
.unwrap();
let read_args = serde_json::json!({
"path": abs_path.to_str().unwrap()
});
let content = registry::dispatch("file_read", &read_args.to_string(), wd)
.await
.unwrap();
assert!(content.contains("absolute path content"));
}