#[cfg(test)]
mod tests {
use crate::toolbox::ToolBox;
use serde_json::json;
use std::path::PathBuf;
use tokio::runtime::Runtime;
fn get_test_paths() -> (PathBuf, PathBuf) {
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let linux_path = root.clone();
let prompts_path = root.join("third_party/prompts/kernel");
(linux_path, prompts_path)
}
#[test]
fn test_virtualize_ref() {
let mut toolbox = ToolBox::new(PathBuf::from("."), None);
assert_eq!(toolbox.virtualize_ref("HEAD"), "HEAD");
assert_eq!(toolbox.virtualize_ref("HEAD~1"), "HEAD~1");
assert_eq!(toolbox.virtualize_ref("origin/HEAD"), "origin/HEAD");
toolbox.set_virtual_head("abc123e".to_string());
assert_eq!(toolbox.virtualize_ref("HEAD"), "abc123e");
assert_eq!(toolbox.virtualize_ref("HEAD~1"), "abc123e~1");
assert_eq!(toolbox.virtualize_ref("HEAD^"), "abc123e^");
assert_eq!(
toolbox.virtualize_ref("baseline..HEAD"),
"baseline..abc123e"
);
assert_eq!(
toolbox.virtualize_ref("HEAD..baseline"),
"abc123e..baseline"
);
assert_eq!(toolbox.virtualize_ref("HEAD:file.c"), "abc123e:file.c");
assert_eq!(toolbox.virtualize_ref("origin/HEAD"), "origin/HEAD");
assert_eq!(toolbox.virtualize_ref("origin/HEAD~1"), "origin/HEAD~1");
assert_eq!(
toolbox.virtualize_ref("refs/remotes/origin/HEAD"),
"refs/remotes/origin/HEAD"
);
assert_eq!(toolbox.virtualize_ref("FOREHEAD"), "FOREHEAD");
assert_eq!(toolbox.virtualize_ref("my-HEAD-branch"), "my-HEAD-branch");
assert_eq!(toolbox.virtualize_ref("HEAD-fixes"), "HEAD-fixes");
}
#[test]
fn test_git_ls_linux() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({ "revision": "HEAD", "path": "." });
let result = rt.block_on(toolbox.call("git_ls", args)).unwrap();
let entries = result["entries"].as_array().unwrap();
assert!(entries.iter().any(|e| e["name"] == "README.md"));
assert!(entries.iter().any(|e| e["name"] == "Cargo.toml"));
}
#[test]
fn test_read_files_linux_readme() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"revision": "HEAD",
"files": [
{ "path": "README.md", "start_line": 1, "end_line": 5 }
]
});
let result = rt.block_on(toolbox.call("git_read_files", args)).unwrap();
let results = result["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
let content = results[0]["content"].as_str().unwrap();
assert!(!content.is_empty());
assert!(content.contains("Sashiko"));
}
#[test]
fn test_git_log() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({ "range": "HEAD", "limit": 1 });
let result = rt.block_on(toolbox.call("git_log", args)).unwrap();
let output = result["output"].as_str().unwrap();
assert!(output.contains("commit"));
assert!(output.contains("Author:"));
}
#[test]
fn test_git_show_head() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({ "object": "HEAD" });
let result = rt.block_on(toolbox.call("git_show", args)).unwrap();
let content = result["content"].as_str().unwrap();
assert!(content.contains("commit"));
assert!(content.contains("Author:"));
}
fn setup_test_repo() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().to_path_buf();
let run_git = |args: &[&str]| {
let status = std::process::Command::new("git")
.current_dir(&repo_path)
.args(args)
.status()
.unwrap();
assert!(status.success());
};
run_git(&["init"]);
run_git(&["config", "user.name", "Test User"]);
run_git(&["config", "user.email", "test@example.com"]);
run_git(&["commit", "--allow-empty", "-m", "Initial commit"]);
run_git(&["commit", "--allow-empty", "-m", "Second commit"]);
(temp_dir, repo_path)
}
#[test]
fn test_git_show_virtual_head() {
let (_temp_dir, repo_path) = setup_test_repo();
let mut toolbox = ToolBox::new(repo_path.clone(), None);
let rt = Runtime::new().unwrap();
let output = std::process::Command::new("git")
.current_dir(&repo_path)
.args(["rev-parse", "HEAD~1"])
.output()
.unwrap();
let head_minus_1 = String::from_utf8(output.stdout).unwrap().trim().to_string();
toolbox.set_virtual_head(head_minus_1.clone());
let args = json!({ "object": "HEAD" });
let result = rt.block_on(toolbox.call("git_show", args)).unwrap();
let content = result["content"].as_str().unwrap();
assert!(content.contains(&head_minus_1));
let output_current = std::process::Command::new("git")
.current_dir(&repo_path)
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
let current_head = String::from_utf8(output_current.stdout)
.unwrap()
.trim()
.to_string();
assert!(!content.contains(¤t_head));
}
#[test]
fn test_git_show_file_full() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({ "object": "HEAD:README.md" });
let result = rt.block_on(toolbox.call("git_show", args)).unwrap();
let content = result["content"].as_str().unwrap();
assert!(content.contains("Sashiko"));
}
#[test]
fn test_git_show_file_range() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"object": "HEAD:README.md",
"start_line": 1,
"end_line": 5
});
let result = rt.block_on(toolbox.call("git_show", args)).unwrap();
let content = result["content"].as_str().unwrap();
let end_line = result["end_line"].as_u64().unwrap();
let start_line = result["start_line"].as_u64().unwrap();
assert_eq!(start_line, 1);
assert_eq!(end_line, 5);
let lines_count = content.lines().count();
assert_eq!(lines_count, 5);
}
#[test]
fn test_git_show_file_default_limit() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"object": "HEAD:README.md",
"start_line": 10
});
let result = rt.block_on(toolbox.call("git_show", args)).unwrap();
let content = result["content"].as_str().unwrap();
let end_line = result["end_line"].as_u64().unwrap();
let start_line = result["start_line"].as_u64().unwrap();
assert_eq!(start_line, 10);
assert_eq!(end_line, 110);
let lines_count = content.lines().count();
assert_eq!(lines_count, 101); }
#[test]
fn test_git_show_raw_caching() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
assert!(toolbox.cache.read().unwrap().is_empty());
let args1 = json!({
"object": "HEAD:README.md",
"start_line": 1,
"end_line": 5
});
let result1 = rt.block_on(toolbox.call("git_show", args1)).unwrap();
assert_eq!(result1["start_line"].as_u64().unwrap(), 1);
assert_eq!(result1["end_line"].as_u64().unwrap(), 5);
let raw_key = "git_show_raw:HEAD:README.md:false:None";
{
let cache = toolbox.cache.read().unwrap();
assert!(cache.contains_key(raw_key), "Raw key should be cached");
assert!(
cache
.get(raw_key)
.unwrap()
.as_str()
.unwrap()
.contains("Sashiko")
);
}
let args2 = json!({
"object": "HEAD:README.md",
"start_line": 10,
"end_line": 15
});
let result2 = rt.block_on(toolbox.call("git_show", args2)).unwrap();
assert_eq!(result2["start_line"].as_u64().unwrap(), 10);
assert_eq!(result2["end_line"].as_u64().unwrap(), 15);
{
let cache = toolbox.cache.read().unwrap();
assert_eq!(cache.len(), 3);
}
}
#[test]
fn test_git_blame_readme() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args =
json!({ "revision": "HEAD", "path": "README.md", "start_line": 1, "end_line": 3 });
let result = rt.block_on(toolbox.call("git_blame", args)).unwrap();
let content = result["content"].as_str().unwrap();
assert!(!content.is_empty());
}
#[test]
fn test_git_blame_truncation() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"revision": "HEAD",
"path": "src/worker/prompts.rs",
"start_line": 1,
"end_line": 3000
});
let result = rt.block_on(toolbox.call("git_blame", args)).unwrap();
assert_eq!(result["truncated"].as_bool(), Some(true));
let content = result["content"].as_str().unwrap();
let returned_items = result["metadata"]["returned_items"].as_u64().unwrap() as usize;
let actual_lines = content.lines().count();
println!("git_blame returned_items metadata: {}", returned_items);
println!("git_blame actual returned content lines: {}", actual_lines);
assert!(actual_lines < 2400, "Blame was not truncated!");
assert_eq!(
returned_items + 1,
actual_lines,
"returned_items metadata does not match actual lines returned (accounting for warning line)!"
);
let start_index = result["metadata"]["start_index"].as_u64().unwrap();
let end_index = result["metadata"]["end_index"].as_u64().unwrap();
assert_eq!(end_index, start_index + returned_items as u64 - 1);
let hint = result["next_page_hint"].as_str().unwrap();
let expected_next_start = end_index + 1;
assert!(hint.contains(&format!("start_line={}", expected_next_start)));
}
#[test]
fn test_git_grep_relative_path() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"revision": "HEAD",
"pattern": "Sashiko",
"path": "README.md"
});
let result = rt.block_on(toolbox.call("git_grep", args)).unwrap();
let content = result["content"].as_str().unwrap();
assert!(!content.is_empty());
for line in content.lines() {
assert!(
!line.starts_with("/"),
"Line starts with absolute path: {}",
line
);
}
assert!(content.contains("README.md") || content.contains("./README.md"));
}
#[test]
fn test_read_prompt() {
let (linux_path, prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path.clone(), Some(prompts_path.clone()));
let rt = Runtime::new().unwrap();
let args = json!({ "name": "technical-patterns.md" });
if prompts_path.join("technical-patterns.md").exists() {
let result = rt
.block_on(toolbox.call("read_prompt", args.clone()))
.expect("Failed to call read_prompt");
assert!(result.get("content").is_some());
} else {
println!("Skipping read_prompt content check: technical-patterns.md not found");
}
let toolbox_disabled = ToolBox::new(linux_path, None);
let result = rt.block_on(toolbox_disabled.call("read_prompt", args));
assert!(result.is_err());
}
#[test]
fn test_git_read_files_truncation() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"revision": "HEAD",
"files": [
{ "path": "src/worker/prompts.rs" }
]
});
let result = rt.block_on(toolbox.call("git_read_files", args)).unwrap();
let results = result["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
let res = &results[0];
assert_eq!(res["truncated"].as_bool(), Some(true));
let content = res["content"].as_str().unwrap();
let returned_items = res["metadata"]["returned_items"].as_u64().unwrap() as usize;
let actual_lines = content.lines().count();
println!("returned_items metadata: {}", returned_items);
println!("actual returned content lines: {}", actual_lines);
assert!(
actual_lines < 2400,
"Content was not truncated! (should be around 800 lines)"
);
assert_eq!(
returned_items + 1,
actual_lines,
"returned_items metadata does not match actual lines returned (accounting for warning line)!"
);
}
#[test]
fn test_git_show_truncation() {
let (linux_path, _prompts_path) = get_test_paths();
let toolbox = ToolBox::new(linux_path, None);
let rt = Runtime::new().unwrap();
let args = json!({
"object": "HEAD:src/worker/prompts.rs",
"start_line": 1,
"end_line": 3000
});
let result = rt.block_on(toolbox.call("git_show", args)).unwrap();
assert_eq!(result["truncated"].as_bool(), Some(true));
let content = result["content"].as_str().unwrap();
let returned_items = result["metadata"]["returned_items"].as_u64().unwrap() as usize;
let actual_lines = content.lines().count();
println!("git_show returned_items metadata: {}", returned_items);
println!("git_show actual returned content lines: {}", actual_lines);
assert!(
actual_lines < 2400,
"Content was not truncated! (should be around 800 lines)"
);
assert_eq!(
returned_items + 1,
actual_lines,
"git_show returned_items metadata does not match actual lines returned (accounting for warning line)!"
);
}
}