opencrabs 0.3.58

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Install with: cargo install opencrabs
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() {
    // No single quotes — values pass through unchanged
    let params = serde_json::json!({"msg": "hello world"});
    let escaped = DynamicToolDef::shell_escape_params(&params);
    assert_eq!(escaped["msg"], "hello world");
}

#[test]
fn test_shell_escape_params_single_quote() {
    // Single quote in value gets escaped
    let params = serde_json::json!({"msg": "it's nice"});
    let escaped = DynamicToolDef::shell_escape_params(&params);
    assert_eq!(escaped["msg"], "it'\\''s nice");
}

#[test]
fn test_shell_escape_params_multiple_quotes() {
    // Multiple single quotes
    let params = serde_json::json!({"msg": "'a' 'b'"});
    let escaped = DynamicToolDef::shell_escape_params(&params);
    assert_eq!(escaped["msg"], "'\\''a'\\'' '\\''b'\\''");
}

#[test]
fn test_shell_escape_params_nested() {
    // Nested object values are also escaped
    let params = serde_json::json!({"outer": {"inner": "it's nested"}});
    let escaped = DynamicToolDef::shell_escape_params(&params);
    assert_eq!(escaped["outer"]["inner"], "it'\\''s nested");
}

#[test]
fn test_shell_escape_params_non_string() {
    // Numbers, booleans, null pass through unchanged
    let params = serde_json::json!({"n": 42, "b": true, "x": null});
    let escaped = DynamicToolDef::shell_escape_params(&params);
    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() {
    // Message with single quote — escaped params prevent shell breakage
    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() {
    // Multi-line message — newlines survive single-quoted shell arg
    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"));
}