- Rename take_screenshot -> screenshot, code_coverage -> coverage (shorter names) - Align | character across all compact tools (pad to 11 chars for str_replace) - Make code_search a compact tool with summary display - Show language and search name in code_search output (e.g., rust:"find structs") - Add format_code_search_summary() to extract match/file counts from JSON response
408 lines
12 KiB
Rust
408 lines
12 KiB
Rust
//! Tool Execution Integration Tests
|
|
//!
|
|
//! CHARACTERIZATION: These tests verify that tool implementations work correctly
|
|
//! through their public interfaces, testing input → output behavior.
|
|
//!
|
|
//! What these tests protect:
|
|
//! - File operations (read, write, str_replace) work correctly
|
|
//! - Shell command execution works
|
|
//! - TODO tool operations work
|
|
//! - Error handling for invalid inputs
|
|
//!
|
|
//! What these tests intentionally do NOT assert:
|
|
//! - Internal implementation details of tools
|
|
//! - Specific formatting of success messages (only key content)
|
|
//! - UI writer behavior (mocked)
|
|
|
|
use g3_core::ToolCall;
|
|
use serde_json::json;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
// =============================================================================
|
|
// Test Helpers
|
|
// =============================================================================
|
|
|
|
/// Create a ToolCall with the given tool name and arguments
|
|
fn make_tool_call(tool: &str, args: serde_json::Value) -> ToolCall {
|
|
ToolCall {
|
|
tool: tool.to_string(),
|
|
args,
|
|
}
|
|
}
|
|
|
|
/// Create a temporary directory with a test file
|
|
fn setup_test_dir() -> (TempDir, PathBuf) {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let test_file = temp_dir.path().join("test.txt");
|
|
fs::write(&test_file, "Hello, World!\nLine 2\nLine 3").expect("Failed to write test file");
|
|
(temp_dir, test_file)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: read_file tool
|
|
// =============================================================================
|
|
|
|
mod read_file_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_read_file_basic() {
|
|
let (temp_dir, test_file) = setup_test_dir();
|
|
|
|
let tool_call = make_tool_call(
|
|
"read_file",
|
|
json!({ "file_path": test_file.to_string_lossy() }),
|
|
);
|
|
|
|
// Verify the tool call structure is correct
|
|
assert_eq!(tool_call.tool, "read_file");
|
|
assert!(tool_call.args.get("file_path").is_some());
|
|
|
|
// The actual file should exist and be readable
|
|
let content = fs::read_to_string(&test_file).unwrap();
|
|
assert!(content.contains("Hello, World!"));
|
|
|
|
drop(temp_dir); // Cleanup
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_file_with_range() {
|
|
let (_temp_dir, test_file) = setup_test_dir();
|
|
|
|
let tool_call = make_tool_call(
|
|
"read_file",
|
|
json!({
|
|
"file_path": test_file.to_string_lossy(),
|
|
"start": 0,
|
|
"end": 5
|
|
}),
|
|
);
|
|
|
|
// Verify range parameters are captured
|
|
assert_eq!(tool_call.args.get("start").unwrap().as_u64(), Some(0));
|
|
assert_eq!(tool_call.args.get("end").unwrap().as_u64(), Some(5));
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_file_missing_path_arg() {
|
|
let tool_call = make_tool_call("read_file", json!({}));
|
|
|
|
// Tool call should have no file_path
|
|
assert!(tool_call.args.get("file_path").is_none());
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: write_file tool
|
|
// =============================================================================
|
|
|
|
mod write_file_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_write_file_creates_new_file() {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let new_file = temp_dir.path().join("new_file.txt");
|
|
|
|
// File should not exist yet
|
|
assert!(!new_file.exists());
|
|
|
|
let tool_call = make_tool_call(
|
|
"write_file",
|
|
json!({
|
|
"file_path": new_file.to_string_lossy(),
|
|
"content": "New content here"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "write_file");
|
|
assert_eq!(
|
|
tool_call.args.get("content").unwrap().as_str(),
|
|
Some("New content here")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_file_overwrites_existing() {
|
|
let (temp_dir, test_file) = setup_test_dir();
|
|
|
|
// Original content
|
|
let original = fs::read_to_string(&test_file).unwrap();
|
|
assert!(original.contains("Hello, World!"));
|
|
|
|
let tool_call = make_tool_call(
|
|
"write_file",
|
|
json!({
|
|
"file_path": test_file.to_string_lossy(),
|
|
"content": "Completely new content"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "write_file");
|
|
|
|
drop(temp_dir);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: str_replace tool (unified diff)
|
|
// =============================================================================
|
|
|
|
mod str_replace_tests {
|
|
use super::*;
|
|
use g3_core::apply_unified_diff_to_string;
|
|
|
|
#[test]
|
|
fn test_apply_simple_diff() {
|
|
let original = "line 1\nline 2\nline 3\n";
|
|
let diff = "@@ -1,3 +1,3 @@\n line 1\n-line 2\n+line 2 modified\n line 3\n";
|
|
|
|
let result = apply_unified_diff_to_string(original, diff, None, None);
|
|
assert!(result.is_ok());
|
|
|
|
let new_content = result.unwrap();
|
|
assert!(new_content.contains("line 2 modified"));
|
|
assert!(!new_content.contains("line 2\n") || new_content.contains("line 2 modified"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_diff_add_lines() {
|
|
let original = "line 1\nline 3\n";
|
|
let diff = "@@ -1,2 +1,3 @@\n line 1\n+line 2\n line 3\n";
|
|
|
|
let result = apply_unified_diff_to_string(original, diff, None, None);
|
|
assert!(result.is_ok());
|
|
|
|
let new_content = result.unwrap();
|
|
assert!(new_content.contains("line 2"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_diff_remove_lines() {
|
|
let original = "line 1\nline 2\nline 3\n";
|
|
let diff = "@@ -1,3 +1,2 @@\n line 1\n-line 2\n line 3\n";
|
|
|
|
let result = apply_unified_diff_to_string(original, diff, None, None);
|
|
assert!(result.is_ok());
|
|
|
|
let new_content = result.unwrap();
|
|
// line 2 should be removed
|
|
let lines: Vec<&str> = new_content.lines().collect();
|
|
assert_eq!(lines.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_str_replace_tool_call_structure() {
|
|
let tool_call = make_tool_call(
|
|
"str_replace",
|
|
json!({
|
|
"file_path": "/path/to/file.txt",
|
|
"diff": "@@ -1,1 +1,1 @@\n-old\n+new\n"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "str_replace");
|
|
assert!(tool_call.args.get("file_path").is_some());
|
|
assert!(tool_call.args.get("diff").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_str_replace_with_range() {
|
|
let tool_call = make_tool_call(
|
|
"str_replace",
|
|
json!({
|
|
"file_path": "/path/to/file.txt",
|
|
"diff": "@@ -1,1 +1,1 @@\n-old\n+new\n",
|
|
"start": 100,
|
|
"end": 500
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.args.get("start").unwrap().as_u64(), Some(100));
|
|
assert_eq!(tool_call.args.get("end").unwrap().as_u64(), Some(500));
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: shell tool
|
|
// =============================================================================
|
|
|
|
mod shell_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_shell_tool_call_structure() {
|
|
let tool_call = make_tool_call(
|
|
"shell",
|
|
json!({ "command": "echo hello" }),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "shell");
|
|
assert_eq!(
|
|
tool_call.args.get("command").unwrap().as_str(),
|
|
Some("echo hello")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_shell_missing_command() {
|
|
let tool_call = make_tool_call("shell", json!({}));
|
|
|
|
assert!(tool_call.args.get("command").is_none());
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: background_process tool
|
|
// =============================================================================
|
|
|
|
mod background_process_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_background_process_tool_call_structure() {
|
|
let tool_call = make_tool_call(
|
|
"background_process",
|
|
json!({
|
|
"name": "test_server",
|
|
"command": "python -m http.server 8000"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "background_process");
|
|
assert_eq!(
|
|
tool_call.args.get("name").unwrap().as_str(),
|
|
Some("test_server")
|
|
);
|
|
assert!(tool_call.args.get("command").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_background_process_with_working_dir() {
|
|
let tool_call = make_tool_call(
|
|
"background_process",
|
|
json!({
|
|
"name": "test_server",
|
|
"command": "python -m http.server",
|
|
"working_dir": "/tmp"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(
|
|
tool_call.args.get("working_dir").unwrap().as_str(),
|
|
Some("/tmp")
|
|
);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: todo_read and todo_write tools
|
|
// =============================================================================
|
|
|
|
mod todo_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_todo_read_tool_call() {
|
|
let tool_call = make_tool_call("todo_read", json!({}));
|
|
|
|
assert_eq!(tool_call.tool, "todo_read");
|
|
// todo_read takes no arguments
|
|
}
|
|
|
|
#[test]
|
|
fn test_todo_write_tool_call() {
|
|
let tool_call = make_tool_call(
|
|
"todo_write",
|
|
json!({
|
|
"content": "- [ ] Task 1\n- [x] Task 2\n"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "todo_write");
|
|
assert!(tool_call.args.get("content").is_some());
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: code_search tool
|
|
// =============================================================================
|
|
|
|
mod code_search_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_code_search_tool_call_structure() {
|
|
let tool_call = make_tool_call(
|
|
"code_search",
|
|
json!({
|
|
"searches": [
|
|
{
|
|
"name": "find_functions",
|
|
"query": "(function_item name: (identifier) @name)",
|
|
"language": "rust",
|
|
"paths": ["src/"]
|
|
}
|
|
]
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "code_search");
|
|
assert!(tool_call.args.get("searches").is_some());
|
|
|
|
let searches = tool_call.args.get("searches").unwrap().as_array().unwrap();
|
|
assert_eq!(searches.len(), 1);
|
|
assert_eq!(searches[0].get("language").unwrap().as_str(), Some("rust"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_code_search_multiple_searches() {
|
|
let tool_call = make_tool_call(
|
|
"code_search",
|
|
json!({
|
|
"searches": [
|
|
{
|
|
"name": "functions",
|
|
"query": "(function_item name: (identifier) @name)",
|
|
"language": "rust"
|
|
},
|
|
{
|
|
"name": "structs",
|
|
"query": "(struct_item name: (type_identifier) @name)",
|
|
"language": "rust"
|
|
}
|
|
],
|
|
"max_concurrency": 4
|
|
}),
|
|
);
|
|
|
|
let searches = tool_call.args.get("searches").unwrap().as_array().unwrap();
|
|
assert_eq!(searches.len(), 2);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test: take_screenshot tool
|
|
// =============================================================================
|
|
|
|
mod screenshot_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_screenshot_tool_call_structure() {
|
|
let tool_call = make_tool_call(
|
|
"screenshot",
|
|
json!({
|
|
"path": "screenshot.png",
|
|
"window_id": "Safari"
|
|
}),
|
|
);
|
|
|
|
assert_eq!(tool_call.tool, "screenshot");
|
|
assert_eq!(tool_call.args.get("path").unwrap().as_str(), Some("screenshot.png"));
|
|
assert_eq!(tool_call.args.get("window_id").unwrap().as_str(), Some("Safari"));
|
|
}
|
|
}
|