Files
mcp/rust/tests/test_shell_tools.rs
zeekayandHanzo Dev c36a1eb1c8 rust/tests: repair the two suites that stopped compiling at HIP-0300
test_shell_tools and test_think_tools have not built since the shell->exec
consolidation, so `cargo test` was red and 46 tests were silently not running.
Nothing here is a new assertion — each change follows the tool's live contract,
read off the published 1.1.22 binary rather than guessed:

  shell   ShellTool -> ExecTool, ProcToolArgs -> ExecToolArgs. Pure rename;
          all 16 pass untouched.
  think   ThinkTool::execute returns Value, not String, and HIP-0300 moved the
          payload under "data". Help reports the tool as "tool", not "name".

Tests whose subject no longer exists are deleted, not rewritten to pass:
critic/review with focus, code_snippets, file_paths and work_description (11);
the timestamp and examples keys (2); action-less auto-detect, which now falls
through to help (1).

browser: playwright_available -> driver_running, its successor since the tool
moved to a CDP driver. That file compiled, so this one was failing, not skipped.

351 pass, 0 fail: lib 223, browser 28, fs 27, search 28, memory 22, shell 16,
think 5, research 2. tests/ is excluded from the package, so 1.1.22's contents
are unchanged and no version bump is warranted.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 14:23:33 -07:00

390 lines
11 KiB
Rust

//! Shell Tools Tests - 1:1 parity with Python test_shell_tools.py
//!
//! Tests for proc tool covering:
//! - Tool properties and metadata
//! - Command execution (sync/async)
//! - Script flags and platform detection
//! - Process management (ps, kill, logs)
use hanzo_mcp::tools::{ExecTool, ExecToolArgs};
use serde_json::Value;
use std::collections::HashMap;
use tempfile::TempDir;
/// Test proc tool creation
#[test]
fn test_proc_tool_creation() {
let tool = ExecTool::new();
// Tool exists and is properly initialized
assert!(true);
}
/// Test proc tool with help action
#[tokio::test]
async fn test_proc_help_action() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "help".to_string(),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
// Help should list available actions
assert!(json.get("actions").is_some() || json.get("description").is_some());
}
/// Test simple echo command execution
#[tokio::test]
async fn test_proc_exec_simple() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo hello".to_string())),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
// Should contain stdout with "hello"
if let Some(stdout) = json.get("stdout") {
assert!(stdout.as_str().unwrap_or("").contains("hello"));
}
}
/// Test command execution with working directory
#[tokio::test]
async fn test_proc_exec_with_cwd() {
let temp_dir = TempDir::new().unwrap();
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("pwd".to_string())),
cwd: Some(temp_dir.path().to_string_lossy().to_string()),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
if let Some(stdout) = json.get("stdout") {
let stdout_str = stdout.as_str().unwrap_or("");
// On macOS, temp dirs may have /private prefix
let temp_path = temp_dir.path().to_string_lossy().to_string();
assert!(
stdout_str.contains(&temp_path) ||
stdout_str.contains(temp_dir.path().file_name().unwrap().to_str().unwrap())
);
}
}
/// Test command with environment variables
#[tokio::test]
async fn test_proc_exec_with_env() {
let tool = ExecTool::new();
let mut env_map = HashMap::new();
env_map.insert("TEST_VAR".to_string(), "test_value".to_string());
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo $TEST_VAR".to_string())),
env: Some(env_map),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
if let Some(stdout) = json.get("stdout") {
assert!(stdout.as_str().unwrap_or("").contains("test_value"));
}
}
/// Test command timeout (backgrounds process after timeout)
#[tokio::test]
async fn test_proc_exec_timeout() {
let tool = ExecTool::new();
// Command that would take too long - shell tool backgrounds on timeout
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("sleep 10".to_string())),
timeout: Some(1), // 1 second timeout
..Default::default()
};
let result = tool.execute(args).await;
// Should succeed and background the process
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
// Shell tool backgrounds processes on timeout and returns status: "running"
assert_eq!(
json.get("status").and_then(|v| v.as_str()),
Some("running"),
"Expected status='running' for timed out command"
);
assert!(
json.get("proc_id").is_some(),
"Expected proc_id for backgrounded process"
);
assert!(
output.contains("backgrounded"),
"Expected 'backgrounded' in message"
);
}
/// Test array command format (HIP-0300 compatibility)
#[tokio::test]
async fn test_proc_exec_array_command() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::Array(vec![
Value::String("echo".to_string()),
Value::String("hello".to_string()),
Value::String("world".to_string()),
])),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("hello") || output.contains("world"));
}
/// Test process listing (ps action)
#[tokio::test]
async fn test_proc_ps_action() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "ps".to_string(),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
// Should return processes array or empty list
assert!(json.get("processes").is_some() || json.is_array() || json.get("data").is_some());
}
/// Test command with exit code
#[tokio::test]
async fn test_proc_exec_exit_code() {
let tool = ExecTool::new();
// Command that exits with non-zero
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("exit 42".to_string())),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
if let Some(exit_code) = json.get("exit_code") {
assert_eq!(exit_code.as_i64().unwrap_or(0), 42);
}
}
/// Test stderr capture
#[tokio::test]
async fn test_proc_exec_stderr() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo error >&2".to_string())),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
if let Some(stderr) = json.get("stderr") {
assert!(stderr.as_str().unwrap_or("").contains("error"));
}
}
/// Test piped commands
#[tokio::test]
async fn test_proc_exec_pipe() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo hello world | wc -w".to_string())),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
// Should contain "2" (word count of "hello world")
assert!(output.contains("2") || output.contains("hello"));
}
/// Test chained commands with &&
#[tokio::test]
async fn test_proc_exec_chained() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo first && echo second".to_string())),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("first"));
assert!(output.contains("second"));
}
/// Test workdir alias for cwd
#[tokio::test]
async fn test_proc_workdir_alias() {
let temp_dir = TempDir::new().unwrap();
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("pwd".to_string())),
workdir: Some(temp_dir.path().to_string_lossy().to_string()),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
}
/// Test shell override
#[tokio::test]
async fn test_proc_shell_override() {
let tool = ExecTool::new();
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo 'test'".to_string())),
shell: Some("bash".to_string()),
..Default::default()
};
let _result = tool.execute(args).await;
// May fail if bash not available, that's OK
assert!(true);
}
/// Test default action (should be help or exec)
#[tokio::test]
async fn test_proc_default_action() {
let tool = ExecTool::new();
let args = ExecToolArgs {
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
}
/// Test multi-env variables
#[tokio::test]
async fn test_proc_multi_env() {
let tool = ExecTool::new();
let mut env_map = HashMap::new();
env_map.insert("VAR1".to_string(), "value1".to_string());
env_map.insert("VAR2".to_string(), "value2".to_string());
let args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo $VAR1 $VAR2".to_string())),
env: Some(env_map),
..Default::default()
};
let result = tool.execute(args).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("value1"));
assert!(output.contains("value2"));
}
#[cfg(test)]
mod integration_tests {
use super::*;
/// Test real shell execution (integration test)
#[tokio::test]
#[ignore] // Run with --ignored flag
async fn test_real_shell_execution() {
let tool = ExecTool::new();
// Test bash
let bash_args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo 'bash works'".to_string())),
shell: Some("bash".to_string()),
..Default::default()
};
let bash_result = tool.execute(bash_args).await;
if let Ok(output) = bash_result {
assert!(output.contains("bash works"));
}
// Test zsh if available
if which::which("zsh").is_ok() {
let zsh_args = ExecToolArgs {
action: "exec".to_string(),
command: Some(Value::String("echo 'zsh works'".to_string())),
shell: Some("zsh".to_string()),
..Default::default()
};
let zsh_result = tool.execute(zsh_args).await;
if let Ok(output) = zsh_result {
assert!(output.contains("zsh works"));
}
}
}
}