Files
BoosAPI/desktop/src-tauri/crates/open-agent-sdk/tests/tools_test.rs
T
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

687 lines
20 KiB
Rust

use open_agent_sdk::tools::ToolRegistry;
use open_agent_sdk::types::{Tool, ToolUseContext};
use serde_json::json;
use std::sync::Arc;
use tokio::sync::RwLock;
fn create_test_context(dir: &str) -> ToolUseContext {
ToolUseContext {
working_dir: dir.to_string(),
abort_signal: tokio_util::sync::CancellationToken::new(),
read_file_state: Arc::new(RwLock::new(std::collections::HashMap::new())),
}
}
// --- Registry Tests ---
#[test]
fn test_default_registry() {
let registry = ToolRegistry::default_registry();
assert!(registry.len() > 0);
// Check core tools exist
assert!(registry.get("Bash").is_some());
assert!(registry.get("Read").is_some());
assert!(registry.get("Write").is_some());
assert!(registry.get("Edit").is_some());
assert!(registry.get("Glob").is_some());
assert!(registry.get("Grep").is_some());
assert!(registry.get("WebFetch").is_some());
assert!(registry.get("WebSearch").is_some());
assert!(registry.get("AskUserQuestion").is_some());
assert!(registry.get("TaskCreate").is_some());
assert!(registry.get("TaskGet").is_some());
assert!(registry.get("TaskList").is_some());
assert!(registry.get("TaskUpdate").is_some());
assert!(registry.get("ToolSearch").is_some());
}
#[test]
fn test_registry_register_custom() {
use async_trait::async_trait;
use open_agent_sdk::types::{ToolError, ToolInputSchema, ToolResult};
use serde_json::Value;
struct CustomTool;
#[async_trait]
impl Tool for CustomTool {
fn name(&self) -> &str {
"CustomTool"
}
fn description(&self) -> &str {
"A test tool"
}
fn input_schema(&self) -> ToolInputSchema {
ToolInputSchema::default()
}
async fn call(
&self,
_input: Value,
_ctx: &ToolUseContext,
) -> Result<ToolResult, ToolError> {
Ok(ToolResult::text("custom result"))
}
}
let mut registry = ToolRegistry::new();
assert!(registry.is_empty());
registry.register(Arc::new(CustomTool));
assert_eq!(registry.len(), 1);
assert!(registry.get("CustomTool").is_some());
}
#[test]
fn test_registry_filter() {
let registry = ToolRegistry::default_registry();
let read_only = registry.filter(|t| t.is_read_only(&json!({})));
assert!(read_only.len() > 0);
// Read, Glob, Grep should be read-only
let names: Vec<&str> = read_only.iter().map(|t| t.name()).collect();
assert!(names.contains(&"Read"));
assert!(names.contains(&"Glob"));
assert!(names.contains(&"Grep"));
}
#[test]
fn test_registry_retain() {
let mut registry = ToolRegistry::default_registry();
let initial_count = registry.len();
registry.retain(&["Read", "Glob", "Grep"]);
assert_eq!(registry.len(), 3);
assert!(registry.len() < initial_count);
assert!(registry.get("Read").is_some());
assert!(registry.get("Bash").is_none());
}
#[test]
fn test_registry_remove() {
let mut registry = ToolRegistry::default_registry();
assert!(registry.get("Bash").is_some());
registry.remove(&["Bash"]);
assert!(registry.get("Bash").is_none());
}
// --- Bash Tool Tests ---
#[tokio::test]
async fn test_bash_echo() {
let registry = ToolRegistry::default_registry();
let bash = registry.get("Bash").unwrap();
let ctx = create_test_context("/tmp");
let result = bash
.call(json!({"command": "echo 'hello world'"}), &ctx)
.await
.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("hello world"));
}
#[tokio::test]
async fn test_bash_exit_code() {
let registry = ToolRegistry::default_registry();
let bash = registry.get("Bash").unwrap();
let ctx = create_test_context("/tmp");
let result = bash.call(json!({"command": "false"}), &ctx).await.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("Exit code:"));
}
#[tokio::test]
async fn test_bash_destructive_detection() {
let registry = ToolRegistry::default_registry();
let bash = registry.get("Bash").unwrap();
let ctx = create_test_context("/tmp");
let result = bash
.call(json!({"command": "rm -rf /"}), &ctx)
.await
.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("destructive"));
}
#[tokio::test]
async fn test_bash_is_read_only() {
let registry = ToolRegistry::default_registry();
let bash = registry.get("Bash").unwrap();
assert!(bash.is_read_only(&json!({"command": "ls -la"})));
assert!(bash.is_read_only(&json!({"command": "git status"})));
assert!(!bash.is_read_only(&json!({"command": "rm file.txt"})));
assert!(!bash.is_read_only(&json!({"command": "cargo build"})));
}
// --- File Read Tool Tests ---
#[tokio::test]
async fn test_read_file() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test.txt");
std::fs::write(&file_path, "line 1\nline 2\nline 3\n").unwrap();
let registry = ToolRegistry::default_registry();
let read = registry.get("Read").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = read
.call(json!({"file_path": file_path.to_str().unwrap()}), &ctx)
.await
.unwrap();
assert!(!result.is_error);
let text = result.get_text();
assert!(text.contains("line 1"));
assert!(text.contains("line 2"));
assert!(text.contains("line 3"));
// Should have line numbers
assert!(text.contains("1\t"));
}
#[tokio::test]
async fn test_read_file_not_found() {
let registry = ToolRegistry::default_registry();
let read = registry.get("Read").unwrap();
let ctx = create_test_context("/tmp");
let result = read
.call(
json!({"file_path": "/tmp/nonexistent_file_12345.txt"}),
&ctx,
)
.await
.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("not found"));
}
#[tokio::test]
async fn test_read_file_with_offset_and_limit() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let content: String = (1..=100).map(|i| format!("line {}\n", i)).collect();
std::fs::write(&file_path, &content).unwrap();
let registry = ToolRegistry::default_registry();
let read = registry.get("Read").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = read
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"offset": 10,
"limit": 5
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
let text = result.get_text();
assert!(text.contains("line 11"));
assert!(text.contains("line 15"));
assert!(!text.contains("line 16"));
}
#[tokio::test]
async fn test_read_directory_error() {
let registry = ToolRegistry::default_registry();
let read = registry.get("Read").unwrap();
let ctx = create_test_context("/tmp");
let result = read.call(json!({"file_path": "/tmp"}), &ctx).await.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("directory"));
}
#[tokio::test]
async fn test_read_is_read_only() {
let registry = ToolRegistry::default_registry();
let read = registry.get("Read").unwrap();
assert!(read.is_read_only(&json!({})));
assert!(read.is_concurrency_safe(&json!({})));
}
// --- File Write Tool Tests ---
#[tokio::test]
async fn test_write_new_file() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("new_file.txt");
let registry = ToolRegistry::default_registry();
let write = registry.get("Write").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = write
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"content": "hello world"
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("Created"));
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "hello world");
}
#[tokio::test]
async fn test_write_creates_directories() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("a/b/c/file.txt");
let registry = ToolRegistry::default_registry();
let write = registry.get("Write").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = write
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"content": "nested"
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
assert!(file_path.exists());
}
// --- File Edit Tool Tests ---
#[tokio::test]
async fn test_edit_file() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("edit_test.txt");
std::fs::write(&file_path, "hello world").unwrap();
let registry = ToolRegistry::default_registry();
let edit = registry.get("Edit").unwrap();
let read = registry.get("Read").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
// Must read first for staleness tracking
read.call(json!({"file_path": file_path.to_str().unwrap()}), &ctx)
.await
.unwrap();
let result = edit
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"old_string": "hello",
"new_string": "goodbye"
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
assert_eq!(
std::fs::read_to_string(&file_path).unwrap(),
"goodbye world"
);
}
#[tokio::test]
async fn test_edit_string_not_found() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("edit_test2.txt");
std::fs::write(&file_path, "hello world").unwrap();
let registry = ToolRegistry::default_registry();
let edit = registry.get("Edit").unwrap();
let read = registry.get("Read").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
read.call(json!({"file_path": file_path.to_str().unwrap()}), &ctx)
.await
.unwrap();
let result = edit
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"old_string": "xyz",
"new_string": "abc"
}),
&ctx,
)
.await
.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("not found"));
}
#[tokio::test]
async fn test_edit_multiple_occurrences() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("edit_multi.txt");
std::fs::write(&file_path, "foo bar foo baz foo").unwrap();
let registry = ToolRegistry::default_registry();
let edit = registry.get("Edit").unwrap();
let read = registry.get("Read").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
read.call(json!({"file_path": file_path.to_str().unwrap()}), &ctx)
.await
.unwrap();
// Without replace_all, should fail for multiple occurrences
let result = edit
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"old_string": "foo",
"new_string": "qux"
}),
&ctx,
)
.await
.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("3 times"));
}
#[tokio::test]
async fn test_edit_replace_all() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("edit_all.txt");
std::fs::write(&file_path, "foo bar foo baz foo").unwrap();
let registry = ToolRegistry::default_registry();
let edit = registry.get("Edit").unwrap();
let read = registry.get("Read").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
read.call(json!({"file_path": file_path.to_str().unwrap()}), &ctx)
.await
.unwrap();
let result = edit
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"old_string": "foo",
"new_string": "qux",
"replace_all": true
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
assert_eq!(
std::fs::read_to_string(&file_path).unwrap(),
"qux bar qux baz qux"
);
}
#[tokio::test]
async fn test_edit_same_string_error() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("edit_same.txt");
std::fs::write(&file_path, "hello world").unwrap();
let registry = ToolRegistry::default_registry();
let edit = registry.get("Edit").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = edit
.call(
json!({
"file_path": file_path.to_str().unwrap(),
"old_string": "hello",
"new_string": "hello"
}),
&ctx,
)
.await
.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("different"));
}
// --- Glob Tool Tests ---
#[tokio::test]
async fn test_glob_find_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.rs"), "").unwrap();
std::fs::write(dir.path().join("b.rs"), "").unwrap();
std::fs::write(dir.path().join("c.txt"), "").unwrap();
let registry = ToolRegistry::default_registry();
let glob = registry.get("Glob").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = glob.call(json!({"pattern": "*.rs"}), &ctx).await.unwrap();
assert!(!result.is_error);
let text = result.get_text();
assert!(text.contains("a.rs"));
assert!(text.contains("b.rs"));
assert!(!text.contains("c.txt"));
}
#[tokio::test]
async fn test_glob_no_matches() {
let dir = tempfile::tempdir().unwrap();
let registry = ToolRegistry::default_registry();
let glob = registry.get("Glob").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = glob.call(json!({"pattern": "*.xyz"}), &ctx).await.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("No files found"));
}
#[tokio::test]
async fn test_glob_dot_lists_current_directory_without_recursing() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("root.rs"), "").unwrap();
std::fs::create_dir(dir.path().join("nested")).unwrap();
std::fs::write(dir.path().join("nested").join("child.rs"), "").unwrap();
let registry = ToolRegistry::default_registry();
let glob = registry.get("Glob").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = glob.call(json!({"pattern": "."}), &ctx).await.unwrap();
assert!(!result.is_error);
let text = result.get_text();
assert!(text.contains("root.rs"));
assert!(!text.contains("child.rs"));
}
#[tokio::test]
async fn test_glob_skips_heavy_directories_before_descending() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("node_modules")).unwrap();
std::fs::write(dir.path().join("node_modules").join("dep.rs"), "").unwrap();
std::fs::create_dir(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src").join("lib.rs"), "").unwrap();
let registry = ToolRegistry::default_registry();
let glob = registry.get("Glob").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = glob
.call(json!({"pattern": "**/*.rs"}), &ctx)
.await
.unwrap();
assert!(!result.is_error);
let text = result.get_text();
assert!(text.contains("lib.rs"));
assert!(!text.contains("dep.rs"));
}
#[tokio::test]
async fn test_glob_returns_when_cancelled() {
let dir = tempfile::tempdir().unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
ctx.abort_signal.cancel();
let registry = ToolRegistry::default_registry();
let glob = registry.get("Glob").unwrap();
let result = glob.call(json!({"pattern": "**/*"}), &ctx).await.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("No files found"));
}
// --- Grep Tool Tests ---
#[tokio::test]
async fn test_grep_search() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("test.txt"),
"hello world\nfoo bar\nhello again\n",
)
.unwrap();
let registry = ToolRegistry::default_registry();
let grep = registry.get("Grep").unwrap();
let ctx = create_test_context(dir.path().to_str().unwrap());
let result = grep
.call(
json!({
"pattern": "hello",
"path": dir.path().to_str().unwrap(),
"output_mode": "content"
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
let text = result.get_text();
assert!(text.contains("hello world") || text.contains("hello"));
}
// --- Task Tool Tests ---
#[tokio::test]
async fn test_task_lifecycle() {
let registry = ToolRegistry::default_registry();
let ctx = create_test_context("/tmp");
// Create a task
let create = registry.get("TaskCreate").unwrap();
let result = create
.call(
json!({
"subject": "Test task",
"description": "A test task"
}),
&ctx,
)
.await
.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("Created task"));
// List tasks
let list = registry.get("TaskList").unwrap();
let result = list.call(json!({}), &ctx).await.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("Test task"));
// Get task
let get = registry.get("TaskGet").unwrap();
let result = get.call(json!({"id": "task_1"}), &ctx).await.unwrap();
assert!(!result.is_error);
assert!(result.get_text().contains("Test task"));
// Update task
let update = registry.get("TaskUpdate").unwrap();
let result = update
.call(json!({"id": "task_1", "status": "completed"}), &ctx)
.await
.unwrap();
assert!(!result.is_error);
// Verify update
let result = get.call(json!({"id": "task_1"}), &ctx).await.unwrap();
assert!(result.get_text().to_lowercase().contains("completed"));
}
#[tokio::test]
async fn test_task_not_found() {
let registry = ToolRegistry::default_registry();
let ctx = create_test_context("/tmp");
let get = registry.get("TaskGet").unwrap();
let result = get.call(json!({"id": "nonexistent"}), &ctx).await.unwrap();
assert!(result.is_error);
assert!(result.get_text().contains("not found"));
}
// --- WebFetch Tool Tests ---
#[tokio::test]
async fn test_webfetch_missing_url() {
let registry = ToolRegistry::default_registry();
let webfetch = registry.get("WebFetch").unwrap();
let ctx = create_test_context("/tmp");
let result = webfetch.call(json!({}), &ctx).await;
assert!(result.is_err());
}
// --- Diff Tests ---
#[test]
fn test_unified_diff() {
let old = "line 1\nline 2\nline 3\n";
let new = "line 1\nline 2 modified\nline 3\n";
let diff = open_agent_sdk::tools::diff::unified_diff(old, new, "test.txt");
assert!(diff.contains("--- a/test.txt"));
assert!(diff.contains("+++ b/test.txt"));
assert!(diff.contains("-line 2"));
assert!(diff.contains("+line 2 modified"));
}
#[test]
fn test_count_changes() {
let old = "a\nb\nc\n";
let new = "a\nx\nc\nd\n";
let (added, removed) = open_agent_sdk::tools::diff::count_changes(old, new);
assert!(added >= 1);
assert!(removed >= 1);
}