selfware 0.6.0

Your personal AI workshop — software you own, software that lasts
Documentation
use super::*;
use crate::api::types::{FunctionDefinition, ToolDefinition, ToolFunction};

fn make_definition(name: &str, required: Vec<&str>) -> ToolDefinition {
    let required: Vec<Value> = required
        .into_iter()
        .map(|s| Value::String(s.to_string()))
        .collect();
    ToolDefinition {
        def_type: "function".to_string(),
        function: FunctionDefinition {
            name: name.to_string(),
            description: "test tool".to_string(),
            parameters: serde_json::json!({
                "type": "object",
                "properties": {
                    "path": { "type": "string" },
                    "count": { "type": "integer" }
                },
                "required": required
            }),
        },
    }
}

#[test]
fn test_validate_tool_call_ok() {
    let defs = vec![make_definition("file_read", vec!["path"])];
    let call = ToolCall {
        id: "call_1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "file_read".to_string(),
            arguments: r#"{"path": "/tmp/test.txt"}"#.to_string(),
        },
    };
    assert!(validate_tool_call(&call, &defs).is_ok());
}

#[test]
fn test_validate_unknown_tool() {
    let defs = vec![make_definition("file_read", vec!["path"])];
    let call = ToolCall {
        id: "call_1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "unknown_tool".to_string(),
            arguments: r#"{}"#.to_string(),
        },
    };
    assert!(validate_tool_call(&call, &defs).is_err());
}

#[test]
fn test_validate_missing_required_arg() {
    let defs = vec![make_definition("file_read", vec!["path"])];
    let call = ToolCall {
        id: "call_1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "file_read".to_string(),
            arguments: r#"{"count": 5}"#.to_string(),
        },
    };
    let err = validate_tool_call(&call, &defs).unwrap_err().to_string();
    assert!(err.contains("Missing required argument 'path'"));
}

#[test]
fn test_validate_wrong_type() {
    let defs = vec![make_definition("file_read", vec![])];
    let call = ToolCall {
        id: "call_1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "file_read".to_string(),
            arguments: r#"{"path": 123}"#.to_string(),
        },
    };
    let err = validate_tool_call(&call, &defs).unwrap_err().to_string();
    assert!(err.contains("expected type"));
    assert!(err.contains("string"));
}

#[test]
fn test_validate_union_type_accepts_any_member() {
    let defs = vec![ToolDefinition {
        def_type: "function".to_string(),
        function: FunctionDefinition {
            name: "example".to_string(),
            description: "test".to_string(),
            parameters: serde_json::json!({
                "type": "object",
                "properties": {
                    "value": { "type": ["string", "null"] }
                }
            }),
        },
    }];

    let string_call = ToolCall {
        id: "1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "example".to_string(),
            arguments: r#"{"value": "hello"}"#.to_string(),
        },
    };
    assert!(validate_tool_call(&string_call, &defs).is_ok());

    let null_call = ToolCall {
        id: "2".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "example".to_string(),
            arguments: r#"{"value": null}"#.to_string(),
        },
    };
    assert!(validate_tool_call(&null_call, &defs).is_ok());

    let bad_call = ToolCall {
        id: "3".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "example".to_string(),
            arguments: r#"{"value": 42}"#.to_string(),
        },
    };
    assert!(validate_tool_call(&bad_call, &defs).is_err());
}

#[test]
fn test_validate_enum_rejects_invalid_value() {
    let defs = vec![ToolDefinition {
        def_type: "function".to_string(),
        function: FunctionDefinition {
            name: "example".to_string(),
            description: "test".to_string(),
            parameters: serde_json::json!({
                "type": "object",
                "properties": {
                    "mode": { "type": "string", "enum": ["fast", "slow"] }
                }
            }),
        },
    }];

    let ok_call = ToolCall {
        id: "1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "example".to_string(),
            arguments: r#"{"mode": "fast"}"#.to_string(),
        },
    };
    assert!(validate_tool_call(&ok_call, &defs).is_ok());

    let bad_call = ToolCall {
        id: "2".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "example".to_string(),
            arguments: r#"{"mode": "turbo"}"#.to_string(),
        },
    };
    let err = validate_tool_call(&bad_call, &defs)
        .unwrap_err()
        .to_string();
    assert!(err.contains("must be one of"));
}

#[test]
fn test_validate_structure_empty_id() {
    let call = ToolCall {
        id: "".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "file_read".to_string(),
            arguments: r#"{}"#.to_string(),
        },
    };
    let err = call.validate_structure().unwrap_err().to_string();
    assert!(err.contains("missing a valid 'id'"));
}

#[test]
fn test_validate_structure_invalid_json() {
    let call = ToolCall {
        id: "call_1".to_string(),
        call_type: "function".to_string(),
        function: ToolFunction {
            name: "file_read".to_string(),
            arguments: r#"{not json}"#.to_string(),
        },
    };
    let err = call.validate_structure().unwrap_err().to_string();
    assert!(err.contains("not valid JSON"));
}