Plan Execution System: - Add PlanStep, AccumulatedPlan types for multi-turn tool call accumulation - Implement AccumulatedPlanStatus for tracking plan lifecycle - Support selective approval of proposed tool calls before execution External Tools Integration: - Add ExternalToolDefinition and ExternalToolTransport to plugins crate - Extend ToolContext with external_tools registry - Add external_tool_to_llm_tool conversion for LLM compatibility JSON-RPC Communication: - Add jsonrpc crate for JSON-RPC 2.0 protocol support - Enable stdio-based communication with external tool servers UI & Engine Updates: - Add plan_panel.rs component for displaying accumulated plans - Wire plan mode into engine loop - Add plan mode integration tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
225 lines
7.7 KiB
Rust
225 lines
7.7 KiB
Rust
// Test that ToolContext properly wires up the placeholder tools
|
|
use agent_core::{ToolContext, execute_tool, get_tool_definitions_with_external};
|
|
use permissions::{Mode, PermissionManager};
|
|
use plugins::{ExternalToolDefinition, ExternalToolTransport, ExternalToolSchema};
|
|
use tools_todo::{TodoList, TodoStatus};
|
|
use tools_bash::ShellManager;
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
#[tokio::test]
|
|
async fn test_todo_write_with_context() {
|
|
let todo_list = TodoList::new();
|
|
let ctx = ToolContext::new().with_todo_list(todo_list.clone());
|
|
let perms = PermissionManager::new(Mode::Code); // Allow all tools
|
|
|
|
let arguments = json!({
|
|
"todos": [
|
|
{
|
|
"content": "First task",
|
|
"status": "pending",
|
|
"active_form": "Working on first task"
|
|
},
|
|
{
|
|
"content": "Second task",
|
|
"status": "in_progress",
|
|
"active_form": "Working on second task"
|
|
}
|
|
]
|
|
});
|
|
|
|
let result = execute_tool("todo_write", &arguments, &perms, &ctx).await;
|
|
assert!(result.is_ok(), "TodoWrite should succeed: {:?}", result);
|
|
|
|
// Verify the todos were written
|
|
let todos = todo_list.read();
|
|
assert_eq!(todos.len(), 2);
|
|
assert_eq!(todos[0].content, "First task");
|
|
assert_eq!(todos[1].status, TodoStatus::InProgress);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_todo_write_without_context() {
|
|
let ctx = ToolContext::new(); // No todo_list
|
|
let perms = PermissionManager::new(Mode::Code);
|
|
|
|
let arguments = json!({
|
|
"todos": []
|
|
});
|
|
|
|
let result = execute_tool("todo_write", &arguments, &perms, &ctx).await;
|
|
assert!(result.is_err(), "TodoWrite should fail without TodoList");
|
|
assert!(result.unwrap_err().to_string().contains("not available"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bash_output_with_context() {
|
|
let manager = ShellManager::new();
|
|
let ctx = ToolContext::new().with_shell_manager(manager.clone());
|
|
let perms = PermissionManager::new(Mode::Code);
|
|
|
|
// Start a shell and run a command
|
|
let shell_id = manager.start_shell().await.unwrap();
|
|
let _ = manager.execute(&shell_id, "echo test", None).await.unwrap();
|
|
|
|
let arguments = json!({
|
|
"shell_id": shell_id
|
|
});
|
|
|
|
let result = execute_tool("bash_output", &arguments, &perms, &ctx).await;
|
|
assert!(result.is_ok(), "BashOutput should succeed: {:?}", result);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bash_output_without_context() {
|
|
let ctx = ToolContext::new(); // No shell_manager
|
|
let perms = PermissionManager::new(Mode::Code);
|
|
|
|
let arguments = json!({
|
|
"shell_id": "fake-id"
|
|
});
|
|
|
|
let result = execute_tool("bash_output", &arguments, &perms, &ctx).await;
|
|
assert!(result.is_err(), "BashOutput should fail without ShellManager");
|
|
assert!(result.unwrap_err().to_string().contains("not available"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_kill_shell_with_context() {
|
|
let manager = ShellManager::new();
|
|
let ctx = ToolContext::new().with_shell_manager(manager.clone());
|
|
let perms = PermissionManager::new(Mode::Code);
|
|
|
|
// Start a shell
|
|
let shell_id = manager.start_shell().await.unwrap();
|
|
|
|
let arguments = json!({
|
|
"shell_id": shell_id
|
|
});
|
|
|
|
let result = execute_tool("kill_shell", &arguments, &perms, &ctx).await;
|
|
assert!(result.is_ok(), "KillShell should succeed: {:?}", result);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_ask_user_without_context() {
|
|
let ctx = ToolContext::new(); // No ask_sender
|
|
let perms = PermissionManager::new(Mode::Code);
|
|
|
|
let arguments = json!({
|
|
"questions": []
|
|
});
|
|
|
|
let result = execute_tool("ask_user", &arguments, &perms, &ctx).await;
|
|
assert!(result.is_err(), "AskUser should fail without AskSender");
|
|
assert!(result.unwrap_err().to_string().contains("not available"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// External Tools Tests
|
|
// ============================================================================
|
|
|
|
fn create_test_external_tool(name: &str, description: &str) -> ExternalToolDefinition {
|
|
ExternalToolDefinition {
|
|
name: name.to_string(),
|
|
description: description.to_string(),
|
|
transport: ExternalToolTransport::Stdio,
|
|
command: Some("echo".to_string()),
|
|
args: vec![],
|
|
url: None,
|
|
timeout_ms: 5000,
|
|
input_schema: ExternalToolSchema {
|
|
schema_type: "object".to_string(),
|
|
properties: {
|
|
let mut props = HashMap::new();
|
|
props.insert(
|
|
"input".to_string(),
|
|
json!({"type": "string", "description": "Test input"}),
|
|
);
|
|
props
|
|
},
|
|
required: vec!["input".to_string()],
|
|
},
|
|
source_path: PathBuf::from("/test/plugin"),
|
|
plugin_name: "test-plugin".to_string(),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_with_external_tools() {
|
|
let mut ext_tools = HashMap::new();
|
|
ext_tools.insert(
|
|
"my_custom_tool".to_string(),
|
|
create_test_external_tool("my_custom_tool", "A custom tool for testing"),
|
|
);
|
|
|
|
let ctx = ToolContext::new().with_external_tools(ext_tools);
|
|
|
|
assert!(ctx.has_external_tool("my_custom_tool"));
|
|
assert!(!ctx.has_external_tool("nonexistent"));
|
|
|
|
let tool = ctx.get_external_tool("my_custom_tool").unwrap();
|
|
assert_eq!(tool.name, "my_custom_tool");
|
|
assert_eq!(tool.description, "A custom tool for testing");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_tool_definitions_with_external() {
|
|
let mut ext_tools = HashMap::new();
|
|
ext_tools.insert(
|
|
"external_analyzer".to_string(),
|
|
create_test_external_tool("external_analyzer", "Analyze stuff externally"),
|
|
);
|
|
ext_tools.insert(
|
|
"external_formatter".to_string(),
|
|
create_test_external_tool("external_formatter", "Format things externally"),
|
|
);
|
|
|
|
let ctx = ToolContext::new().with_external_tools(ext_tools);
|
|
let all_tools = get_tool_definitions_with_external(&ctx);
|
|
|
|
// Should have built-in tools plus our 2 external tools
|
|
let external_tool_names: Vec<_> = all_tools
|
|
.iter()
|
|
.filter(|t| t.function.name == "external_analyzer" || t.function.name == "external_formatter")
|
|
.collect();
|
|
assert_eq!(external_tool_names.len(), 2);
|
|
|
|
// Verify external tool schema is correct
|
|
let analyzer = all_tools.iter().find(|t| t.function.name == "external_analyzer").unwrap();
|
|
assert!(analyzer.function.description.contains("Analyze stuff"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_unknown_tool_without_external() {
|
|
let ctx = ToolContext::new();
|
|
let perms = PermissionManager::new(Mode::Code);
|
|
|
|
let arguments = json!({"input": "test"});
|
|
let result = execute_tool("completely_unknown_tool", &arguments, &perms, &ctx).await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().to_string().contains("Unknown tool"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_external_tool_permission_denied() {
|
|
let mut ext_tools = HashMap::new();
|
|
ext_tools.insert(
|
|
"dangerous_tool".to_string(),
|
|
create_test_external_tool("dangerous_tool", "A dangerous tool"),
|
|
);
|
|
|
|
let ctx = ToolContext::new().with_external_tools(ext_tools);
|
|
let perms = PermissionManager::new(Mode::Plan); // Plan mode denies bash-like tools
|
|
|
|
let arguments = json!({"input": "test"});
|
|
let result = execute_tool("dangerous_tool", &arguments, &perms, &ctx).await;
|
|
|
|
// External tools are treated like Bash, so should require permission in Plan mode
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(err.contains("Permission required") || err.contains("Permission denied"));
|
|
}
|