test(protocols,skills): add 90 tests for MCP types + skill loader/runner
zclaw-protocols: +43 tests covering mcp_types serde, ContentBlock variants, transport config builders, and domain type roundtrips. zclaw-skills: +47 tests covering SKILL.md/TOML parsing, auto-classify, PromptOnlySkill execution, and SkillManifest/SkillResult roundtrips. Batch 8 of audit plan (plans/stateless-petting-rossum.md).
This commit is contained in:
55
crates/zclaw-protocols/tests/mcp_transport_tests.rs
Normal file
55
crates/zclaw-protocols/tests/mcp_transport_tests.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Tests for MCP Transport configuration (McpServerConfig)
|
||||
//!
|
||||
//! These tests cover McpServerConfig builder methods without spawning processes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use zclaw_protocols::McpServerConfig;
|
||||
|
||||
#[test]
|
||||
fn npx_config_creates_correct_command() {
|
||||
let config = McpServerConfig::npx("@modelcontextprotocol/server-memory");
|
||||
assert_eq!(config.command, "npx");
|
||||
assert_eq!(config.args, vec!["-y", "@modelcontextprotocol/server-memory"]);
|
||||
assert!(config.env.is_empty());
|
||||
assert!(config.cwd.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_config_creates_correct_command() {
|
||||
let config = McpServerConfig::node("/path/to/server.js");
|
||||
assert_eq!(config.command, "node");
|
||||
assert_eq!(config.args, vec!["/path/to/server.js"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_config_creates_correct_command() {
|
||||
let config = McpServerConfig::python("mcp_server.py");
|
||||
assert_eq!(config.command, "python");
|
||||
assert_eq!(config.args, vec!["mcp_server.py"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_adds_variables() {
|
||||
let config = McpServerConfig::node("server.js")
|
||||
.env("API_KEY", "secret123")
|
||||
.env("DEBUG", "true");
|
||||
assert_eq!(config.env.get("API_KEY").unwrap(), "secret123");
|
||||
assert_eq!(config.env.get("DEBUG").unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cwd_sets_working_directory() {
|
||||
let config = McpServerConfig::node("server.js").cwd("/tmp/work");
|
||||
assert_eq!(config.cwd.unwrap(), "/tmp/work");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_builder_pattern() {
|
||||
let config = McpServerConfig::npx("@scope/server")
|
||||
.env("PORT", "3000")
|
||||
.cwd("/app");
|
||||
assert_eq!(config.command, "npx");
|
||||
assert_eq!(config.args.len(), 2);
|
||||
assert_eq!(config.env.len(), 1);
|
||||
assert_eq!(config.cwd.unwrap(), "/app");
|
||||
}
|
||||
186
crates/zclaw-protocols/tests/mcp_types_domain_tests.rs
Normal file
186
crates/zclaw-protocols/tests/mcp_types_domain_tests.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
//! Tests for MCP domain types (mcp.rs) — McpTool, McpContent, McpResource, etc.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use zclaw_protocols::*;
|
||||
|
||||
// === McpTool ===
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_roundtrip() {
|
||||
let tool = McpTool {
|
||||
name: "search".to_string(),
|
||||
description: "Search documents".to_string(),
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {"query": {"type": "string"}}}),
|
||||
};
|
||||
let json = serde_json::to_string(&tool).unwrap();
|
||||
let parsed: McpTool = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.name, "search");
|
||||
assert_eq!(parsed.description, "Search documents");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_empty_description() {
|
||||
let tool = McpTool {
|
||||
name: "ping".to_string(),
|
||||
description: String::new(),
|
||||
input_schema: serde_json::json!({}),
|
||||
};
|
||||
let parsed: McpTool = serde_json::from_str(&serde_json::to_string(&tool).unwrap()).unwrap();
|
||||
assert!(parsed.description.is_empty());
|
||||
}
|
||||
|
||||
// === McpContent ===
|
||||
|
||||
#[test]
|
||||
fn mcp_content_text_roundtrip() {
|
||||
let content = McpContent::Text { text: "hello".to_string() };
|
||||
let json = serde_json::to_string(&content).unwrap();
|
||||
let parsed: McpContent = serde_json::from_str(&json).unwrap();
|
||||
match parsed {
|
||||
McpContent::Text { text } => assert_eq!(text, "hello"),
|
||||
_ => panic!("Expected Text"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_content_image_roundtrip() {
|
||||
let content = McpContent::Image {
|
||||
data: "base64==".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&content).unwrap();
|
||||
let parsed: McpContent = serde_json::from_str(&json).unwrap();
|
||||
match parsed {
|
||||
McpContent::Image { data, mime_type } => {
|
||||
assert_eq!(data, "base64==");
|
||||
assert_eq!(mime_type, "image/png");
|
||||
}
|
||||
_ => panic!("Expected Image"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_content_resource_roundtrip() {
|
||||
let content = McpContent::Resource {
|
||||
resource: McpResourceContent {
|
||||
uri: "file:///test.txt".to_string(),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
text: Some("content".to_string()),
|
||||
blob: None,
|
||||
},
|
||||
};
|
||||
let json = serde_json::to_string(&content).unwrap();
|
||||
let parsed: McpContent = serde_json::from_str(&json).unwrap();
|
||||
match parsed {
|
||||
McpContent::Resource { resource } => {
|
||||
assert_eq!(resource.uri, "file:///test.txt");
|
||||
assert_eq!(resource.text.unwrap(), "content");
|
||||
}
|
||||
_ => panic!("Expected Resource"),
|
||||
}
|
||||
}
|
||||
|
||||
// === McpToolCallRequest ===
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_request_serialization() {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("query".to_string(), serde_json::json!("test"));
|
||||
let req = McpToolCallRequest {
|
||||
name: "search".to_string(),
|
||||
arguments: args,
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("\"name\":\"search\""));
|
||||
assert!(json.contains("\"query\":\"test\""));
|
||||
}
|
||||
|
||||
// === McpToolCallResponse ===
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_response_parse_success() {
|
||||
let json = r#"{"content":[{"type":"text","text":"found 3 results"}],"is_error":false}"#;
|
||||
let resp: McpToolCallResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(!resp.is_error);
|
||||
assert_eq!(resp.content.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_response_parse_error() {
|
||||
let json = r#"{"content":[{"type":"text","text":"tool not found"}],"is_error":true}"#;
|
||||
let resp: McpToolCallResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.is_error);
|
||||
}
|
||||
|
||||
// === McpResource ===
|
||||
|
||||
#[test]
|
||||
fn mcp_resource_roundtrip() {
|
||||
let res = McpResource {
|
||||
uri: "file:///doc.md".to_string(),
|
||||
name: "Documentation".to_string(),
|
||||
description: Some("Project docs".to_string()),
|
||||
mime_type: Some("text/markdown".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&res).unwrap();
|
||||
let parsed: McpResource = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.uri, "file:///doc.md");
|
||||
assert_eq!(parsed.description.unwrap(), "Project docs");
|
||||
}
|
||||
|
||||
// === McpPrompt ===
|
||||
|
||||
#[test]
|
||||
fn mcp_prompt_roundtrip() {
|
||||
let prompt = McpPrompt {
|
||||
name: "summarize".to_string(),
|
||||
description: "Summarize text".to_string(),
|
||||
arguments: vec![
|
||||
McpPromptArgument {
|
||||
name: "length".to_string(),
|
||||
description: "Target length".to_string(),
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
let json = serde_json::to_string(&prompt).unwrap();
|
||||
let parsed: McpPrompt = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.arguments.len(), 1);
|
||||
assert!(!parsed.arguments[0].required);
|
||||
}
|
||||
|
||||
// === McpServerInfo ===
|
||||
|
||||
#[test]
|
||||
fn mcp_server_info_roundtrip() {
|
||||
let info = McpServerInfo {
|
||||
name: "test-mcp".to_string(),
|
||||
version: "2.0.0".to_string(),
|
||||
protocol_version: "2024-11-05".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&info).unwrap();
|
||||
let parsed: McpServerInfo = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.name, "test-mcp");
|
||||
assert_eq!(parsed.protocol_version, "2024-11-05");
|
||||
}
|
||||
|
||||
// === McpCapabilities ===
|
||||
|
||||
#[test]
|
||||
fn mcp_capabilities_default_empty() {
|
||||
let caps = McpCapabilities::default();
|
||||
assert!(caps.tools.is_none());
|
||||
assert!(caps.resources.is_none());
|
||||
assert!(caps.prompts.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_capabilities_with_tools() {
|
||||
let caps = McpCapabilities {
|
||||
tools: Some(McpToolCapabilities { list_changed: true }),
|
||||
resources: None,
|
||||
prompts: None,
|
||||
};
|
||||
let json = serde_json::to_string(&caps).unwrap();
|
||||
assert!(json.contains("\"list_changed\":true"));
|
||||
}
|
||||
267
crates/zclaw-protocols/tests/mcp_types_tests.rs
Normal file
267
crates/zclaw-protocols/tests/mcp_types_tests.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! Tests for MCP JSON-RPC types (mcp_types.rs)
|
||||
//!
|
||||
//! Covers: serialization, deserialization, builder patterns, edge cases.
|
||||
|
||||
use serde_json;
|
||||
use zclaw_protocols::*;
|
||||
|
||||
// === JsonRpcRequest ===
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_request_new_has_correct_defaults() {
|
||||
let req = JsonRpcRequest::new(42, "tools/list");
|
||||
assert_eq!(req.jsonrpc, "2.0");
|
||||
assert_eq!(req.id, 42);
|
||||
assert_eq!(req.method, "tools/list");
|
||||
assert!(req.params.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_request_with_params() {
|
||||
let req = JsonRpcRequest::new(1, "tools/call")
|
||||
.with_params(serde_json::json!({"name": "search"}));
|
||||
let serialized = serde_json::to_string(&req).unwrap();
|
||||
assert!(serialized.contains("\"params\""));
|
||||
assert!(serialized.contains("\"name\":\"search\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_request_skip_null_params() {
|
||||
let req = JsonRpcRequest::new(1, "ping");
|
||||
let serialized = serde_json::to_string(&req).unwrap();
|
||||
// params is None, should be skipped
|
||||
assert!(!serialized.contains("\"params\""));
|
||||
}
|
||||
|
||||
// === JsonRpcResponse ===
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_response_parse_success() {
|
||||
let json = r#"{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}"#;
|
||||
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(resp.id, 1);
|
||||
assert!(resp.result.is_some());
|
||||
assert!(resp.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_response_parse_error() {
|
||||
let json = r#"{"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"Invalid Request"}}"#;
|
||||
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(resp.id, 2);
|
||||
assert!(resp.result.is_none());
|
||||
let err = resp.error.unwrap();
|
||||
assert_eq!(err.code, -32600);
|
||||
assert_eq!(err.message, "Invalid Request");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_response_parse_error_with_data() {
|
||||
let json = r#"{"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"Bad params","data":{"field":"uri"}}}"#;
|
||||
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
|
||||
let err = resp.error.unwrap();
|
||||
assert!(err.data.is_some());
|
||||
assert_eq!(err.data.unwrap()["field"], "uri");
|
||||
}
|
||||
|
||||
// === InitializeRequest ===
|
||||
|
||||
#[test]
|
||||
fn initialize_request_default() {
|
||||
let req = InitializeRequest::default();
|
||||
assert_eq!(req.protocol_version, "2024-11-05");
|
||||
assert_eq!(req.client_info.name, "zclaw");
|
||||
assert!(!req.client_info.version.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_request_serializes() {
|
||||
let req = InitializeRequest::default();
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("\"protocol_version\":\"2024-11-05\""));
|
||||
assert!(json.contains("\"client_info\""));
|
||||
}
|
||||
|
||||
// === ServerCapabilities ===
|
||||
|
||||
#[test]
|
||||
fn server_capabilities_empty() {
|
||||
let json = r#"{"protocol_version":"2024-11-05","capabilities":{},"server_info":{"name":"test","version":"1.0"}}"#;
|
||||
let result: InitializeResult = serde_json::from_str(json).unwrap();
|
||||
assert!(result.capabilities.tools.is_none());
|
||||
assert!(result.capabilities.resources.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_capabilities_with_tools() {
|
||||
let json = r#"{"protocol_version":"2024-11-05","capabilities":{"tools":{"list_changed":true}},"server_info":{"name":"test","version":"1.0"}}"#;
|
||||
let result: InitializeResult = serde_json::from_str(json).unwrap();
|
||||
let tools = result.capabilities.tools.unwrap();
|
||||
assert!(tools.list_changed);
|
||||
}
|
||||
|
||||
// === ContentBlock ===
|
||||
|
||||
#[test]
|
||||
fn content_block_text() {
|
||||
let json = r#"{"type":"text","text":"hello world"}"#;
|
||||
let block: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
match block {
|
||||
ContentBlock::Text { text } => assert_eq!(text, "hello world"),
|
||||
_ => panic!("Expected Text variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_block_image() {
|
||||
let json = r#"{"type":"image","data":"base64data","mime_type":"image/png"}"#;
|
||||
let block: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
match block {
|
||||
ContentBlock::Image { data, mime_type } => {
|
||||
assert_eq!(data, "base64data");
|
||||
assert_eq!(mime_type, "image/png");
|
||||
}
|
||||
_ => panic!("Expected Image variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_block_resource() {
|
||||
let json = r#"{"type":"resource","resource":{"uri":"file:///test.txt","text":"content"}}"#;
|
||||
let block: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
match block {
|
||||
ContentBlock::Resource { resource } => {
|
||||
assert_eq!(resource.uri, "file:///test.txt");
|
||||
assert_eq!(resource.text.unwrap(), "content");
|
||||
}
|
||||
_ => panic!("Expected Resource variant"),
|
||||
}
|
||||
}
|
||||
|
||||
// === CallToolResult ===
|
||||
|
||||
#[test]
|
||||
fn call_tool_result_parse() {
|
||||
let json = r#"{"content":[{"type":"text","text":"result"}],"is_error":false}"#;
|
||||
let result: CallToolResult = serde_json::from_str(json).unwrap();
|
||||
assert!(!result.is_error);
|
||||
assert_eq!(result.content.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_tool_result_error() {
|
||||
let json = r#"{"content":[{"type":"text","text":"something went wrong"}],"is_error":true}"#;
|
||||
let result: CallToolResult = serde_json::from_str(json).unwrap();
|
||||
assert!(result.is_error);
|
||||
}
|
||||
|
||||
// === ListToolsResult ===
|
||||
|
||||
#[test]
|
||||
fn list_tools_result_with_cursor() {
|
||||
let json = r#"{"tools":[{"name":"search","input_schema":{"type":"object"}}],"next_cursor":"abc123"}"#;
|
||||
let result: ListToolsResult = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(result.tools.len(), 1);
|
||||
assert_eq!(result.tools[0].name, "search");
|
||||
assert_eq!(result.next_cursor.unwrap(), "abc123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_tools_result_without_cursor() {
|
||||
let json = r#"{"tools":[]}"#;
|
||||
let result: ListToolsResult = serde_json::from_str(json).unwrap();
|
||||
assert!(result.tools.is_empty());
|
||||
assert!(result.next_cursor.is_none());
|
||||
}
|
||||
|
||||
// === Resource types ===
|
||||
|
||||
#[test]
|
||||
fn resource_parse_with_optional_fields() {
|
||||
let json = r#"{"uri":"file:///doc.txt","name":"doc","description":"A doc","mime_type":"text/plain"}"#;
|
||||
let res: Resource = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(res.uri, "file:///doc.txt");
|
||||
assert_eq!(res.name, "doc");
|
||||
assert_eq!(res.description.unwrap(), "A doc");
|
||||
assert_eq!(res.mime_type.unwrap(), "text/plain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_parse_minimal() {
|
||||
let json = r#"{"uri":"file:///x","name":"x"}"#;
|
||||
let res: Resource = serde_json::from_str(json).unwrap();
|
||||
assert!(res.description.is_none());
|
||||
assert!(res.mime_type.is_none());
|
||||
}
|
||||
|
||||
// === LoggingLevel ===
|
||||
|
||||
#[test]
|
||||
fn logging_level_serialize_roundtrip() {
|
||||
let levels = vec![
|
||||
LoggingLevel::Debug,
|
||||
LoggingLevel::Info,
|
||||
LoggingLevel::Warning,
|
||||
LoggingLevel::Error,
|
||||
LoggingLevel::Critical,
|
||||
LoggingLevel::Emergency,
|
||||
];
|
||||
for level in levels {
|
||||
let json = serde_json::to_string(&level).unwrap();
|
||||
let parsed: LoggingLevel = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(std::mem::discriminant(&level), std::mem::discriminant(&parsed));
|
||||
}
|
||||
}
|
||||
|
||||
// === InitializedNotification ===
|
||||
|
||||
#[test]
|
||||
fn initialized_notification_fields() {
|
||||
let n = InitializedNotification::new();
|
||||
assert_eq!(n.jsonrpc, "2.0");
|
||||
assert_eq!(n.method, "notifications/initialized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialized_notification_serializes() {
|
||||
let n = InitializedNotification::default();
|
||||
let json = serde_json::to_string(&n).unwrap();
|
||||
assert!(json.contains("\"notifications/initialized\""));
|
||||
}
|
||||
|
||||
// === Prompt types ===
|
||||
|
||||
#[test]
|
||||
fn prompt_parse_with_arguments() {
|
||||
let json = r#"{"name":"greet","description":"Greeting","arguments":[{"name":"lang","description":"Language","required":true}]}"#;
|
||||
let prompt: Prompt = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(prompt.name, "greet");
|
||||
assert_eq!(prompt.arguments.len(), 1);
|
||||
assert!(prompt.arguments[0].required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_message_parse() {
|
||||
let json = r#"{"role":"user","content":{"type":"text","text":"hello"}}"#;
|
||||
let msg: PromptMessage = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(msg.role, "user");
|
||||
}
|
||||
|
||||
// === McpClientConfig ===
|
||||
|
||||
#[test]
|
||||
fn mcp_client_config_roundtrip() {
|
||||
let config = McpClientConfig {
|
||||
server_url: "http://localhost:3000".to_string(),
|
||||
server_info: McpServerInfo {
|
||||
name: "test-server".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
protocol_version: "2024-11-05".to_string(),
|
||||
},
|
||||
capabilities: McpCapabilities::default(),
|
||||
};
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let parsed: McpClientConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.server_url, config.server_url);
|
||||
assert_eq!(parsed.server_info.name, "test-server");
|
||||
}
|
||||
Reference in New Issue
Block a user