Files
g3/crates/g3-core/tests/tool_execution_test.rs
Dhanji R. Prasanna dea0e6b1ca Compact tool output improvements
- 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
2026-01-14 08:12:50 +05:30

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"));
}
}