use super::*;
#[test]
fn bash_output_is_bounded() {
let (_temp, runtime) = runtime();
let stdout = runtime.dispatch(
"bash",
json!({"command": format!("yes x | head -c {}", BASH_STDOUT_MAX_BYTES + 4096)}),
);
assert!(!stdout.success);
assert_eq!(
stdout.metadata["stdout"].as_str().unwrap().len(),
BASH_STDOUT_MAX_BYTES
);
assert_eq!(stdout.metadata["stdout_truncated"], true);
assert_eq!(stdout.metadata["stdout_limit_bytes"], BASH_STDOUT_MAX_BYTES);
assert!(stdout.content.contains("output truncated"));
let stderr = runtime.dispatch(
"bash",
json!({"command": format!("yes err >&2 | head -c {}", BASH_STDERR_MAX_BYTES + 4096)}),
);
assert!(!stderr.success);
assert_eq!(
stderr.metadata["stderr"].as_str().unwrap().len(),
BASH_STDERR_MAX_BYTES
);
assert_eq!(stderr.metadata["stderr_truncated"], true);
assert_eq!(stderr.metadata["stderr_limit_bytes"], BASH_STDERR_MAX_BYTES);
}
#[test]
fn bash_captures_stdout_stderr_and_exit_code() {
let (_temp, runtime) = runtime();
let result = runtime.dispatch("bash", json!({"command":"echo out; echo err >&2; exit 7"}));
assert!(!result.success);
assert_eq!(result.metadata["exit_code"], 7);
assert!(result.metadata["stdout"].as_str().unwrap().contains("out"));
assert!(result.metadata["stderr"].as_str().unwrap().contains("err"));
}
#[cfg(windows)]
#[test]
fn bash_runs_powershell_stdout_stderr_and_exit_code() {
let (_temp, runtime) = runtime();
let result = runtime.dispatch(
"bash",
json!({"command":"Write-Output ok; Write-Error err; exit 7"}),
);
assert!(!result.success);
assert_eq!(result.tool_name, "bash");
assert_eq!(result.metadata["exit_code"], 7);
assert!(result.metadata["stdout"].as_str().unwrap().contains("ok"));
assert!(result.metadata["stderr"].as_str().unwrap().contains("err"));
}
#[test]
fn shell_alias_dispatches_to_bash_and_unknown_tool_reports_original_name() {
let (_temp, runtime) = runtime();
let result = runtime.dispatch("shell", json!({"command":"echo alias"}));
assert!(result.success, "{}", result.content);
assert_eq!(result.tool_name, "bash");
assert!(
result.metadata["stdout"]
.as_str()
.unwrap()
.contains("alias")
);
let unknown = runtime.dispatch("nope", json!({}));
assert!(!unknown.success);
assert_eq!(unknown.tool_name, "nope");
assert!(unknown.content.contains("unknown tool 'nope'"));
}
#[test]
fn mvp_tool_specs_define_provider_order_and_bash_alias() {
assert_eq!(
capability::MVP_TOOL_CAPABILITIES
.iter()
.map(|tool| tool.canonical_name())
.collect::<Vec<_>>(),
vec![
"read",
"view_image",
"bash",
"browser",
"hash_edit",
"write",
"grep",
"find",
"list_files",
"repo_map",
"subagents",
"web_search",
"code_search",
"ast_grep"
]
);
assert_eq!(
ToolCapability::from_dispatch_name("shell"),
Some(ToolCapability::Bash)
);
assert_eq!(ToolCapability::from_dispatch_name("missing"), None);
let definitions = mvp_tool_definitions_json();
let definitions = definitions.as_array().unwrap();
assert_eq!(definitions.len(), 14);
assert!(
!definitions
.iter()
.any(|definition| definition["name"] == "shell")
);
assert!(
!definitions
.iter()
.any(|definition| definition["name"] == "cmd")
);
let read = definitions
.iter()
.find(|definition| definition["name"] == "read")
.unwrap();
assert!(read["parameters"]["properties"].get("path").is_none());
assert!(read["parameters"]["properties"].get("paths").is_some());
assert_eq!(read["parameters"]["required"], json!(["paths"]));
assert_eq!(read["parameters"]["properties"]["paths"]["minItems"], 1);
assert_eq!(
read["parameters"]["properties"]["paths"]["maxItems"],
FILE_READ_MAX_FILES
);
assert_eq!(read["parameters"]["additionalProperties"], false);
assert!(read["parameters"].get("oneOf").is_none());
assert!(read["parameters"].get("anyOf").is_none());
assert!(read["parameters"].get("allOf").is_none());
assert!(read["parameters"].get("enum").is_none());
assert!(read["parameters"].get("not").is_none());
assert_eq!(read["parameters"]["x-maxBytes"], FILE_READ_MAX_BYTES);
assert_eq!(read["parameters"]["x-maxLines"], FILE_READ_MAX_LINES);
assert_eq!(read["parameters"]["x-maxFiles"], FILE_READ_MAX_FILES);
let read_description = read["description"].as_str().unwrap();
assert!(read_description.contains("resources"));
assert!(read_description.contains("paths"));
assert!(read_description.contains("**Tool Name**: `read`"));
assert!(read_description.contains("**Tool Arguments**"));
let find = definitions
.iter()
.find(|definition| definition["name"] == "find")
.unwrap();
assert_eq!(find["parameters"]["required"], json!(["query"]));
assert_eq!(find["parameters"]["additionalProperties"], false);
assert_eq!(find["parameters"]["properties"]["query"]["maxLength"], 512);
assert_eq!(
find["parameters"]["properties"]["kind"]["enum"],
json!(["files", "directories", "mixed"])
);
assert_eq!(
find["parameters"]["properties"]["limit"]["maximum"],
crate::tools::args::FIND_MAX_LIMIT
);
assert!(
find["description"]
.as_str()
.unwrap()
.contains("**Tool Name**: `find`")
);
let bash = definitions
.iter()
.find(|definition| definition["name"] == "bash")
.unwrap();
assert!(bash["parameters"]["properties"].get("command").is_some());
assert!(bash["parameters"]["properties"].get("timeout").is_some());
assert!(bash["parameters"]["properties"].get("cmd").is_none());
assert!(bash["parameters"].get("anyOf").is_none());
assert_eq!(bash["parameters"]["required"], json!(["command"]));
assert_eq!(bash["parameters"]["additionalProperties"], false);
assert_eq!(bash["parameters"]["x-notSandboxed"], true);
assert_eq!(
bash["parameters"]["x-stdoutMaxBytes"],
BASH_STDOUT_MAX_BYTES
);
let bash_description = bash["description"].as_str().unwrap();
assert!(bash_description.contains("**Tool Name**: `bash`"));
assert!(bash_description.contains("non-interactive shell command"));
assert!(bash_description.contains("cwd-scope preflight"));
}
#[test]
fn web_search_schema_and_validation_are_bounded() {
let definitions = mvp_tool_definitions_json();
let web = definitions
.as_array()
.unwrap()
.iter()
.find(|definition| definition["name"] == "web_search")
.unwrap();
assert_eq!(web["parameters"]["additionalProperties"], false);
assert_eq!(web["parameters"]["properties"]["queries"]["maxItems"], 5);
assert_eq!(web["parameters"]["properties"]["numResults"]["maximum"], 10);
assert_eq!(
web["parameters"]["properties"]["recencyFilter"]["enum"],
json!(["day", "week", "month", "year"])
);
let (_temp, runtime) = runtime();
let args = serde_json::from_value::<WebSearchArgs>(json!({
"queries":["Rust programming language official website"],
"numResults":2,
"includeContent":false,
"recencyFilter":"year",
"domainFilter":[]
}))
.unwrap()
.validate()
.unwrap();
assert_eq!(
args.normalized_queries(),
vec!["Rust programming language official website".to_string()]
);
for args in [
json!({}),
json!({"queries":[" "]}),
json!({"query":"one", "queries":["two"]}),
json!({"queries":["one", "two", "three", "four", "five", "six"]}),
json!({"queries":["one"], "numResults":0}),
json!({"queries":["one"], "numResults":11}),
json!({"queries":["one"], "recencyFilter":"hour"}),
json!({"queries":["one"], "domainFilter":["https://example.com"]}),
json!({"queries":["one"], "domainFilter":["example.com/path"]}),
json!({"queries":["one"], "domainFilter":["example.com:443"]}),
json!({"queries":["one"], "domainFilter":[".example.com"]}),
json!({"queries":["one"], "domainFilter":["example.com."]}),
json!({"queries":["one"], "domainFilter":["example..com"]}),
json!({"queries":["one"], "domainFilter":["bad_domain.com"]}),
json!({"queries":["one"], "domainFilter":["*.example.com"]}),
json!({"queries":["one"], "domainFilter":["good.-bad.com"]}),
json!({"queries":["one"], "domainFilter":["bad-.example.com"]}),
json!({"queries":["one"], "domainFilter":[format!("{}.com", "a".repeat(64))]}),
json!({"queries":["one"], "domainFilter":["--example.com"]}),
json!({"queries":["one"], "domainFilter":["-"]}),
] {
let result = runtime.dispatch("web_search", args);
assert!(!result.success, "accepted invalid args: {}", result.content);
}
let valid_domains = serde_json::from_value::<WebSearchArgs>(json!({
"queries":["one"],
"domainFilter":["example.com", "sub.example-123.com", "-old.example.com"]
}))
.unwrap()
.validate()
.unwrap();
assert_eq!(
valid_domains.domain_filter.unwrap(),
vec!["example.com", "sub.example-123.com", "-old.example.com"]
);
}
#[test]
fn code_search_schema_and_validation_are_bounded() {
let definitions = mvp_tool_definitions_json();
let code = definitions
.as_array()
.unwrap()
.iter()
.find(|definition| definition["name"] == "code_search")
.unwrap();
assert_eq!(code["parameters"]["required"], json!(["query"]));
assert_eq!(code["parameters"]["additionalProperties"], false);
assert_eq!(
code["parameters"]["properties"]["maxTokens"]["minimum"],
1000
);
assert_eq!(
code["parameters"]["properties"]["maxTokens"]["maximum"],
10000
);
let (_temp, runtime) = runtime();
for args in [
json!({}),
json!({"query":" "}),
json!({"query":"rust", "maxTokens":999}),
json!({"query":"rust", "maxTokens":10001}),
] {
let result = runtime.dispatch("code_search", args);
assert!(!result.success, "accepted invalid args: {}", result.content);
}
}
#[test]
fn bash_rejects_obvious_cwd_escape_constructs_before_execution() {
let (temp, runtime) = runtime();
let outside = temp
.path()
.parent()
.unwrap()
.join("magi-code-outside-marker");
let _ = fs::remove_file(&outside);
for command in [
"pwd > ../magi-code-outside-marker",
"cat ../magi-code-outside-marker",
"type ..\\magi-code-outside-marker",
"type foo\\..\\magi-code-outside-marker",
"cd .. && pwd",
"type '..\\magi-code-outside-marker'",
"type \"..\\magi-code-outside-marker\"",
] {
let result = runtime.dispatch("bash", json!({"command": command}));
assert!(!result.success, "accepted unsafe command: {command}");
assert!(
result.content.contains("cwd-scope preflight"),
"unexpected error for {command}: {}",
result.content
);
}
assert!(!outside.exists());
}
#[test]
fn bash_absolute_paths_are_allowed_by_default_and_rejected_when_disabled() {
let (_temp, runtime) = runtime();
assert!(
crate::shell::runtime::preflight_bash_cwd_scope(
"cat /tmp/magi-code-marker",
runtime.bash_absolute_paths,
runtime.bash_shell_expansion,
)
.is_ok()
);
let restricted = ToolRuntime::new_with_settings(
runtime.cwd.clone(),
ToolSettings {
bash: BashToolSettings {
absolute_paths: false,
..BashToolSettings::default()
},
..ToolSettings::default()
},
)
.unwrap();
let error = crate::shell::runtime::preflight_bash_cwd_scope(
"cat /tmp/magi-code-marker",
restricted.bash_absolute_paths,
restricted.bash_shell_expansion,
)
.unwrap_err()
.to_string();
assert!(error.contains("tools.bash.absolute_paths"), "{error}");
}
#[test]
fn bash_shell_expansion_is_allowed_by_default_and_rejected_when_disabled() {
let (_temp, runtime) = runtime();
assert!(
crate::shell::runtime::preflight_bash_cwd_scope(
"echo $HOME",
runtime.bash_absolute_paths,
runtime.bash_shell_expansion,
)
.is_ok()
);
let restricted = ToolRuntime::new_with_settings(
runtime.cwd.clone(),
ToolSettings {
bash: BashToolSettings {
shell_expansion: false,
..BashToolSettings::default()
},
..ToolSettings::default()
},
)
.unwrap();
let error = crate::shell::runtime::preflight_bash_cwd_scope(
"echo $HOME",
restricted.bash_absolute_paths,
restricted.bash_shell_expansion,
)
.unwrap_err()
.to_string();
assert!(error.contains("tools.bash.shell_expansion"), "{error}");
}
#[cfg(unix)]
#[test]
fn bash_timeout_terminates_process_tree() {
let (temp, runtime) = runtime();
let marker = temp.path().join("late-marker");
let result = runtime.dispatch(
"bash",
json!({"command":"(sleep 2; printf late > late-marker) & sleep 5", "timeout":1}),
);
assert!(!result.success);
assert_eq!(result.metadata["timed_out"], true);
thread::sleep(Duration::from_millis(1500));
assert!(
!marker.exists(),
"descendant survived timeout and wrote marker"
);
}
#[test]
fn bash_allows_safe_relative_commands() {
let (temp, runtime) = runtime();
let result = runtime.dispatch(
"bash",
json!({"command":"mkdir -p dir && printf ok > dir/file.txt && cat dir/file.txt"}),
);
assert!(result.success, "{}", result.content);
assert_eq!(
fs::read_to_string(temp.path().join("dir/file.txt")).unwrap(),
"ok"
);
assert!(result.metadata["stdout"].as_str().unwrap().contains("ok"));
let cd_current = runtime.dispatch("bash", json!({"command":"cd . && pwd"}));
assert!(cd_current.success, "{}", cd_current.content);
}