use super::*;
#[test]
fn tool_settings_absolute_paths_default_omitted_and_false_values() {
let defaults = ToolSettings::default();
assert!(defaults.read.absolute_paths);
assert!(defaults.view_image.absolute_paths);
assert_eq!(defaults.view_image.max_image_bytes, 5 * 1024 * 1024);
assert!(defaults.view_image.vision_model.is_none());
assert!(defaults.hash_edit.absolute_paths);
assert!(defaults.write.absolute_paths);
assert!(defaults.grep.absolute_paths);
assert!(defaults.find.absolute_paths);
assert!(defaults.list_files.absolute_paths);
assert!(defaults.bash.absolute_paths);
assert!(defaults.bash.shell_expansion);
assert!(defaults.subagents.absolute_paths);
assert_eq!(defaults.subagents.max_depth, DEFAULT_SUBAGENT_MAX_DEPTH);
assert!(!defaults.output_compression.enabled);
let omitted: ToolSettings = serde_json::from_value(json!({
"read": {},
"view_image": {},
"hash_edit": {},
"write": {},
"grep": {},
"find": {},
"list_files": {},
"bash": {},
"subagents": {},
"output_compression": {}
}))
.unwrap();
assert_eq!(omitted, defaults);
let false_values: ToolSettings = serde_json::from_value(json!({
"read": {"absolute_paths": false},
"view_image": {"absolute_paths": false, "max_image_bytes": 1234, "vision_model": {"provider":"vision-provider","model":"vision-model"}},
"hash_edit": {"absolute_paths": false},
"write": {"absolute_paths": false},
"grep": {"absolute_paths": false},
"find": {"absolute_paths": false},
"list_files": {"absolute_paths": false},
"bash": {"absolute_paths": false, "shell_expansion": false},
"subagents": {"absolute_paths": false, "max_depth": 1},
"output_compression": {"enabled": true}
}))
.unwrap();
assert!(!false_values.read.absolute_paths);
assert!(!false_values.view_image.absolute_paths);
assert_eq!(false_values.view_image.max_image_bytes, 1234);
assert_eq!(
false_values
.view_image
.vision_model
.as_ref()
.unwrap()
.provider,
"vision-provider"
);
assert!(!false_values.hash_edit.absolute_paths);
assert!(!false_values.write.absolute_paths);
assert!(!false_values.grep.absolute_paths);
assert!(!false_values.find.absolute_paths);
assert!(!false_values.list_files.absolute_paths);
assert!(!false_values.bash.absolute_paths);
assert!(!false_values.bash.shell_expansion);
assert!(!false_values.subagents.absolute_paths);
assert_eq!(false_values.subagents.max_depth, 1);
assert!(false_values.output_compression.enabled);
let temp = TempDir::new().unwrap();
let runtime = ToolRuntime::new_with_settings(temp.path(), false_values)
.unwrap()
.clone_for_cwd_with_subagent_depth(temp.path(), 1)
.unwrap();
assert!(!runtime.read_absolute_paths);
assert!(!runtime.view_image_absolute_paths);
assert_eq!(runtime.view_image_max_image_bytes, 1234);
assert_eq!(
runtime.view_image_vision_model.as_ref().unwrap().model,
"vision-model"
);
assert!(!runtime.hashline_absolute_paths);
assert!(!runtime.write_absolute_paths);
assert!(!runtime.grep_absolute_paths);
assert!(!runtime.find_absolute_paths);
assert!(!runtime.list_files_absolute_paths);
assert!(!runtime.bash_absolute_paths);
assert!(!runtime.bash_shell_expansion);
assert!(!runtime.subagents_absolute_paths);
assert_eq!(runtime.subagents_max_depth, 1);
assert!(runtime.output_compression_settings().enabled);
assert!(!runtime.subagents_schema_enabled());
}
#[test]
fn read_argument_validation_rejects_invalid_path_shapes() {
let (_temp, runtime) = runtime();
for args in [
json!({}),
json!({"path":"a.txt", "paths":["b.txt"]}),
json!({"path":" "}),
json!({"paths":[]}),
json!({"paths":["a.txt", ""]}),
json!({"paths": vec!["a.txt"; FILE_READ_MAX_FILES + 1]}),
json!({"paths":["a.txt"], "offset":0}),
json!({"paths":["a.txt"], "limit":0}),
json!({"paths":["a.txt"], "limit":FILE_READ_MAX_LINES + 1}),
] {
let result = runtime.dispatch("read", args);
assert!(!result.success, "accepted invalid args: {}", result.content);
}
}
#[test]
fn absolute_paths_default_for_hash_edit_write_grep_and_false_rejects_outside() {
let (temp, runtime) = runtime();
let outside = TempDir::new().unwrap();
let outside_edit = outside.path().join("edit.txt");
let outside_write = outside.path().join("write.txt");
let outside_grep = outside.path().join("grep.txt");
fs::write(
&outside_edit,
"alpha
beta
",
)
.unwrap();
fs::write(
&outside_grep,
"find me
",
)
.unwrap();
let tag = runtime.hashline_snapshots.lock().unwrap().record(
outside_edit.canonicalize().unwrap(),
"alpha
beta
",
[1, 2],
);
let edited = runtime.dispatch(
"hash_edit",
json!({"input": format!("[{}#{tag}]
SWAP 2.=2:
+gamma", outside_edit.display())}),
);
assert!(edited.success, "{}", edited.content);
assert_eq!(
fs::read_to_string(&outside_edit).unwrap(),
"alpha
gamma
"
);
let written = runtime.dispatch(
"write",
json!({"path": outside_write, "content":"outside write"}),
);
assert!(written.success, "{}", written.content);
let grep = runtime.dispatch("grep", json!({"pattern":"find", "path": outside.path()}));
assert!(grep.success, "{}", grep.content);
let restricted = ToolRuntime::new_with_settings(
temp.path(),
ToolSettings {
hash_edit: HashEditToolSettings {
absolute_paths: false,
},
write: WriteToolSettings {
absolute_paths: false,
},
grep: GrepToolSettings {
absolute_paths: false,
},
..ToolSettings::default()
},
)
.unwrap();
assert!(
!restricted
.dispatch(
"hash_edit",
json!({"input": format!("[{}#{tag}]
SWAP 2.=2:
+delta", outside_edit.display())})
)
.success
);
assert!(
!restricted
.dispatch(
"write",
json!({"path": outside.path().join("blocked.txt"), "content":"blocked"})
)
.success
);
assert!(
!restricted
.dispatch("grep", json!({"pattern":"find", "path": outside.path()}))
.success
);
assert!(
restricted
.dispatch("write", json!({"path":"inside.txt", "content":"ok"}))
.success
);
}
#[test]
fn existing_path_policy_covers_relative_absolute_and_out_of_root_paths() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("inside.txt"), "inside").unwrap();
let outside = TempDir::new().unwrap();
let outside_file = outside.path().join("outside.txt");
fs::write(&outside_file, "outside").unwrap();
let inside = runtime
.resolve_existing_path(
"inside.txt",
crate::tools::fs::ExistingPathPolicy::read(false),
)
.unwrap();
assert_eq!(
inside,
temp.path().join("inside.txt").canonicalize().unwrap()
);
let allowed = runtime
.resolve_existing_path(
outside_file.to_str().unwrap(),
crate::tools::fs::ExistingPathPolicy::grep(true),
)
.unwrap();
assert_eq!(allowed, outside_file.canonicalize().unwrap());
let blocked = runtime
.resolve_existing_path(
outside_file.to_str().unwrap(),
crate::tools::fs::ExistingPathPolicy::grep(false),
)
.unwrap_err()
.to_string();
assert!(blocked.contains("tools.grep.absolute_paths"), "{blocked}");
assert!(blocked.contains("escapes cwd"), "{blocked}");
}
#[test]
fn search_tools_preserve_find_cwd_policy_and_grep_absolute_policy() {
let (temp, runtime) = runtime();
fs::create_dir(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/inside.txt"), "needle inside\n").unwrap();
let outside = TempDir::new().unwrap();
fs::write(outside.path().join("outside.txt"), "needle outside\n").unwrap();
let find_relative = runtime.dispatch("find", json!({"query":"inside", "path":"src"}));
assert!(find_relative.success, "{}", find_relative.content);
let find_outside = runtime.dispatch("find", json!({"query":"outside", "path": outside.path()}));
assert!(find_outside.success, "{}", find_outside.content);
assert!(find_outside.content.contains("outside.txt"));
let grep_outside = runtime.dispatch(
"grep",
json!({"pattern":"needle", "path": outside.path(), "limit": 1}),
);
assert!(grep_outside.success, "{}", grep_outside.content);
let restricted = ToolRuntime::new_with_settings(
temp.path(),
ToolSettings {
grep: GrepToolSettings {
absolute_paths: false,
},
find: FindToolSettings {
absolute_paths: false,
},
..ToolSettings::default()
},
)
.unwrap();
let blocked = restricted.dispatch(
"grep",
json!({"pattern":"needle", "path": outside.path(), "limit": 1}),
);
assert!(!blocked.success);
assert!(blocked.content.contains("tools.grep.absolute_paths"));
let blocked_find =
restricted.dispatch("find", json!({"query":"outside", "path": outside.path()}));
assert!(!blocked_find.success);
assert!(blocked_find.content.contains("tools.find.absolute_paths"));
}
#[test]
fn bash_argument_validation_accepts_command_and_cmd_alias() {
let (_temp, runtime) = runtime();
let command = runtime.dispatch("bash", json!({"command":"printf command"}));
assert!(command.success, "{}", command.content);
assert!(
command.metadata["stdout"]
.as_str()
.unwrap()
.contains("command")
);
let cmd = runtime.dispatch("bash", json!({"cmd":"printf cmd"}));
assert!(cmd.success, "{}", cmd.content);
assert!(cmd.metadata["stdout"].as_str().unwrap().contains("cmd"));
let precedence = runtime.dispatch(
"bash",
json!({"command":"printf command", "cmd":"printf cmd"}),
);
assert!(precedence.success, "{}", precedence.content);
assert_eq!(precedence.metadata["stdout"], "command");
}
#[test]
fn bash_argument_validation_rejects_empty_command_and_invalid_timeout() {
let (_temp, runtime) = runtime();
let empty = runtime.dispatch("bash", json!({"command":" "}));
assert!(!empty.success);
assert!(empty.content.contains("command must not be empty"));
let empty_cmd = runtime.dispatch("bash", json!({"cmd":""}));
assert!(!empty_cmd.success);
assert!(empty_cmd.content.contains("command must not be empty"));
for timeout in [0, 301] {
let result = runtime.dispatch(
"bash",
json!({"command":"printf no-run", "timeout": timeout}),
);
assert!(!result.success, "accepted timeout {timeout}");
assert!(result.content.contains("timeout must be between 1 and 300"));
}
}
#[test]
fn runtime_argument_validation_rejects_empty_tasks() {
let (_temp, runtime) = runtime();
let result = runtime.dispatch("subagents", json!({"tasks":[]}));
assert!(!result.success);
assert!(
result.content.contains("at least one task"),
"{}",
result.content
);
}
#[test]
fn runtime_argument_validation_rejects_unknown_fields() {
let (temp, runtime) = runtime();
fs::write(temp.path().join("a.txt"), "alpha").unwrap();
let read = runtime.dispatch("read", json!({"path":"a.txt", "unexpected": true}));
assert!(!read.success);
assert!(read.content.contains("unknown field"), "{}", read.content);
let bash = runtime.dispatch(
"bash",
json!({"command":"printf no-run", "unexpected": true}),
);
assert!(!bash.success);
assert!(bash.content.contains("unknown field"), "{}", bash.content);
let parallel = runtime.dispatch(
"subagents",
json!({"tasks":[{"intent":"inspect","unexpected":true}],"concurrency":1}),
);
assert!(!parallel.success);
assert!(
parallel.content.contains("unknown field"),
"{}",
parallel.content
);
let web = runtime.dispatch(
"web_search",
json!({"queries":["rust"], "unexpected": true}),
);
assert!(!web.success);
assert!(web.content.contains("unknown field"), "{}", web.content);
let code = runtime.dispatch("code_search", json!({"query":"rust", "unexpected": true}));
assert!(!code.success);
assert!(code.content.contains("unknown field"), "{}", code.content);
}