use mcp_servers::CodingMcp;
use mcp_servers::coding::error::BashError;
use mcp_servers::coding::tools::bash::{BashEnvironment, BashInput, execute_command};
use mcp_utils::testing::args;
use rmcp::model::{CallToolRequestParams, CallToolResponse, CancelTaskParams, TaskStatus};
use serde_json::json;
use std::fs::canonicalize;
use std::process::{Command, Stdio};
use super::common::{TestResult, await_terminal_task, call_task, cancels_prompts, connect, taskless};
#[tokio::test]
async fn bash_commands_receive_eof_instead_of_inherited_stdin() {
const OPEN_STDIN: &str = "AETHER_TEST_BASH_OPEN_STDIN";
if std::env::var_os(OPEN_STDIN).is_some() {
let output = execute_command(
BashInput { command: "read input || printf '%s' eof".into(), ..Default::default() },
None,
&BashEnvironment::default(),
)
.await
.unwrap();
assert_eq!(output.output, "eof");
return;
}
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args(["--exact", "bash_commands_receive_eof_instead_of_inherited_stdin", "--nocapture"])
.env(OPEN_STDIN, "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().unwrap();
let _open_stdin = child.stdin.take().unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr));
}
#[tokio::test]
async fn test_basic_command() {
let output = execute_command(
BashInput { command: "echo 'hello world'".into(), ..Default::default() },
None,
&BashEnvironment::default(),
)
.await
.unwrap();
assert_eq!(output.output.trim(), "hello world");
assert_eq!(output.exit_code, 0);
assert!(!output.killed);
}
#[tokio::test]
async fn test_command_with_exit_code_and_stderr() {
let output = execute_command(
BashInput { command: "echo error >&2; exit 42".into(), ..Default::default() },
None,
&BashEnvironment::default(),
)
.await
.unwrap();
assert!(output.output.contains("error"));
assert_eq!(output.exit_code, 42);
assert!(!output.killed);
}
#[tokio::test]
async fn test_command_timeout() {
let output = execute_command(
BashInput { command: "sleep 10".into(), timeout: Some(100), ..Default::default() },
None,
&BashEnvironment::default(),
)
.await
.unwrap();
assert!(output.output.contains("timed out"));
assert_eq!(output.exit_code, -1);
assert!(output.killed);
}
#[tokio::test]
async fn test_timeout_validation() {
let result = execute_command(
BashInput { command: "echo test".into(), timeout: Some(700_000), ..Default::default() },
None,
&BashEnvironment::default(),
)
.await;
assert!(matches!(result.unwrap_err(), BashError::TimeoutTooLarge));
}
#[tokio::test]
async fn test_rm_command_blocked() {
let result =
execute_command(BashInput { command: "rm".into(), ..Default::default() }, None, &BashEnvironment::default())
.await;
assert!(matches!(result.unwrap_err(), BashError::Forbidden(_)));
}
#[tokio::test]
async fn command_runs_in_working_directory() -> Result<(), Box<dyn std::error::Error>> {
let temp = tempfile::tempdir()?;
let output = execute_command(
BashInput { command: "pwd".into(), ..Default::default() },
Some(temp.path()),
&BashEnvironment::default(),
)
.await?;
assert_eq!(canonicalize(output.output.trim())?, canonicalize(temp.path())?);
Ok(())
}
#[tokio::test]
async fn background_bash_uses_tasks_and_returns_structured_output() -> TestResult {
let client = connect(CodingMcp::new(std::env::temp_dir()), cancels_prompts()).await;
let result = call_task(
&client,
"bash",
json!({ "command": "echo stdout; echo stderr >&2; exit 7", "runInBackground": true }),
)
.await?;
assert_eq!(result.is_error, Some(false));
let structured = result.structured_content.as_ref().unwrap();
let output = structured["output"].as_str().unwrap();
assert!(output.contains("stdout"));
assert!(output.contains("stderr"));
assert_eq!(structured["exitCode"], 7);
Ok(())
}
#[tokio::test]
async fn cancelling_a_background_bash_task_stops_it() -> TestResult {
let client = connect(CodingMcp::new(std::env::temp_dir()), cancels_prompts()).await;
let request = CallToolRequestParams::new("bash")
.with_arguments(args(json!({ "command": "sleep 60", "runInBackground": true })));
let CallToolResponse::Task(created) = client.peer().call_tool_once(request).await? else {
panic!("background bash creates a task");
};
client.peer().cancel_task(CancelTaskParams::new(created.task.task_id.clone())).await?;
let task = await_terminal_task(&client, &created.task.task_id).await?;
assert_eq!(task.status(), TaskStatus::Cancelled);
Ok(())
}
#[tokio::test]
async fn background_bash_requires_tasks_capability() -> TestResult {
let client = connect(CodingMcp::new(std::env::temp_dir()), taskless()).await;
let error = client
.peer()
.call_tool_once(
CallToolRequestParams::new("bash")
.with_arguments(args(json!({ "command": "echo never", "runInBackground": true }))),
)
.await
.unwrap_err();
assert!(
error.to_string().to_lowercase().contains("missing required client capability"),
"unexpected error: {error}"
);
Ok(())
}