use crate::brain::tools::Tool;
use crate::brain::tools::ToolCapability;
use crate::brain::tools::ToolExecutionContext;
use crate::brain::tools::dynamic::tool::*;
use std::collections::HashMap;
use tokio;
use uuid::Uuid;
fn make_shell(name: &str, cmd: &str, params: Vec<ParamDef>) -> DynamicTool {
DynamicTool::new(DynamicToolDef {
name: name.into(),
description: format!("Test: {name}"),
executor: ExecutorType::Shell,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 10,
command: Some(cmd.into()),
params,
})
}
fn ctx() -> ToolExecutionContext {
ToolExecutionContext::new(Uuid::new_v4())
}
#[test]
fn test_name() {
assert_eq!(make_shell("t", "echo", vec![]).name(), "t");
}
#[test]
fn test_capabilities() {
assert_eq!(
make_shell("s", "echo", vec![]).capabilities(),
vec![ToolCapability::ExecuteShell]
);
}
#[test]
fn test_input_schema() {
let tool = make_shell(
"echo",
"echo {{msg}}",
vec![ParamDef {
name: "msg".into(),
param_type: "string".into(),
description: "Msg".into(),
required: true,
default: None,
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
}],
);
let schema = tool.input_schema();
assert_eq!(schema["type"], "object");
assert_eq!(schema["required"][0], "msg");
}
#[test]
fn test_extract_params_with_defaults() {
let tool = make_shell(
"echo",
"echo {{msg}} {{count}}",
vec![
ParamDef {
name: "msg".into(),
param_type: "string".into(),
description: "".into(),
required: true,
default: None,
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
},
ParamDef {
name: "count".into(),
param_type: "integer".into(),
description: "".into(),
required: false,
default: Some(serde_json::json!(3)),
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
},
],
);
let params = tool.extract_params(&serde_json::json!({"msg": "hello"}));
assert_eq!(params["msg"], "hello");
assert_eq!(params["count"], 3);
}
#[test]
fn test_template_rendering() {
let result = DynamicToolDef::render_template(
"deploy {{branch}} x{{count}}",
&serde_json::json!({"branch": "main", "count": 3}),
);
assert_eq!(result, "deploy main x3");
}
#[test]
fn test_parse_toml() {
let config: DynamicToolsConfig = toml::from_str(
r#"
[[tools]]
name = "check"
description = "Check health"
executor = "http"
method = "GET"
url = "https://example.com/health"
"#,
)
.unwrap();
assert_eq!(config.tools.len(), 1);
assert_eq!(config.tools[0].executor, ExecutorType::Http);
}
#[test]
fn test_roundtrip_toml() {
let config = DynamicToolsConfig {
tools: vec![DynamicToolDef {
name: "ping".into(),
description: "Ping".into(),
executor: ExecutorType::Shell,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 30,
command: Some("ping -c 1 {{host}}".into()),
params: vec![ParamDef {
name: "host".into(),
param_type: "string".into(),
description: "".into(),
required: true,
default: None,
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
}],
}],
};
let content = toml::to_string_pretty(&config).unwrap();
let loaded: DynamicToolsConfig = toml::from_str(&content).unwrap();
assert_eq!(loaded.tools[0].name, "ping");
}
#[tokio::test]
async fn test_execute_shell_echo() {
let tool = make_shell("echo_test", "echo hello", vec![]);
let result = tool.execute(serde_json::json!({}), &ctx()).await.unwrap();
assert!(result.success);
assert!(result.output.contains("hello"));
}
#[tokio::test]
async fn test_execute_shell_failure() {
let result = make_shell("fail", "exit 42", vec![])
.execute(serde_json::json!({}), &ctx())
.await
.unwrap();
assert!(!result.success);
}
#[tokio::test]
async fn test_missing_command() {
let t = DynamicTool::new(DynamicToolDef {
name: "b".into(),
description: "".into(),
executor: ExecutorType::Shell,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 5,
command: None,
params: vec![],
});
let result = t.execute(serde_json::json!({}), &ctx()).await.unwrap();
assert!(!result.success);
}
#[tokio::test]
async fn test_missing_url() {
let t = DynamicTool::new(DynamicToolDef {
name: "h".into(),
description: "".into(),
executor: ExecutorType::Http,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 5,
command: None,
params: vec![],
});
let result = t.execute(serde_json::json!({}), &ctx()).await.unwrap();
assert!(!result.success);
}
#[test]
fn test_shell_escape_params_noop() {
let params = serde_json::json!({"msg": "hello world"});
let escaped = DynamicToolDef::shell_escape_params(¶ms);
assert_eq!(escaped["msg"], "hello world");
}
#[test]
fn test_shell_escape_params_single_quote() {
let params = serde_json::json!({"msg": "it's nice"});
let escaped = DynamicToolDef::shell_escape_params(¶ms);
assert_eq!(escaped["msg"], "it'\\''s nice");
}
#[test]
fn test_shell_escape_params_multiple_quotes() {
let params = serde_json::json!({"msg": "'a' 'b'"});
let escaped = DynamicToolDef::shell_escape_params(¶ms);
assert_eq!(escaped["msg"], "'\\''a'\\'' '\\''b'\\''");
}
#[test]
fn test_shell_escape_params_nested() {
let params = serde_json::json!({"outer": {"inner": "it's nested"}});
let escaped = DynamicToolDef::shell_escape_params(¶ms);
assert_eq!(escaped["outer"]["inner"], "it'\\''s nested");
}
#[test]
fn test_shell_escape_params_non_string() {
let params = serde_json::json!({"n": 42, "b": true, "x": null});
let escaped = DynamicToolDef::shell_escape_params(¶ms);
assert_eq!(escaped["n"], 42);
assert_eq!(escaped["b"], true);
assert_eq!(escaped["x"], serde_json::Value::Null);
}
#[tokio::test]
async fn test_execute_shell_with_single_quote() {
let result = make_shell(
"echo_test",
"echo 'msg={{msg}}'",
vec![ParamDef {
name: "msg".into(),
param_type: "string".into(),
description: "".into(),
required: true,
default: None,
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
}],
)
.execute(serde_json::json!({"msg": "it's nice"}), &ctx())
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("msg=it's nice"));
}
#[tokio::test]
async fn test_execute_shell_newlines() {
let result = make_shell(
"echo_test",
"echo 'msg={{msg}}'",
vec![ParamDef {
name: "msg".into(),
param_type: "string".into(),
description: "".into(),
required: true,
default: None,
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
}],
)
.execute(serde_json::json!({"msg": "line1\nline2"}), &ctx())
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("line1\nline2") || result.output.contains("line1\nline2"));
}