Files
owlen/crates/core/agent/tests/tool_context.rs
vikingowl 84fa08ab45 feat(plan): Add plan execution system with external tool support
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>
2025-12-26 22:47:54 +01:00

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