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");
|
||||||
|
}
|
||||||
247
crates/zclaw-skills/tests/loader_tests.rs
Normal file
247
crates/zclaw-skills/tests/loader_tests.rs
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
//! Tests for skill loader — SKILL.md and TOML parsing
|
||||||
|
|
||||||
|
use zclaw_skills::*;
|
||||||
|
|
||||||
|
// === parse_skill_md ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_basic_frontmatter() {
|
||||||
|
let content = r#"---
|
||||||
|
name: "Code Reviewer"
|
||||||
|
description: "Reviews code"
|
||||||
|
version: "1.0.0"
|
||||||
|
mode: prompt-only
|
||||||
|
tags: coding, review
|
||||||
|
---
|
||||||
|
# Code Reviewer
|
||||||
|
Reviews code for quality.
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.name, "Code Reviewer");
|
||||||
|
assert_eq!(manifest.description, "Reviews code");
|
||||||
|
assert_eq!(manifest.version, "1.0.0");
|
||||||
|
assert_eq!(manifest.mode, zclaw_skills::SkillMode::PromptOnly);
|
||||||
|
assert_eq!(manifest.tags, vec!["coding", "review"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_with_triggers_list() {
|
||||||
|
let content = r#"---
|
||||||
|
name: "Translator"
|
||||||
|
description: "Translates text"
|
||||||
|
version: "1.0.0"
|
||||||
|
mode: prompt-only
|
||||||
|
triggers:
|
||||||
|
- "翻译"
|
||||||
|
- "translate"
|
||||||
|
- "中译英"
|
||||||
|
---
|
||||||
|
# Translator
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.triggers, vec!["翻译", "translate", "中译英"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_with_tools_list() {
|
||||||
|
let content = r#"---
|
||||||
|
name: "Builder"
|
||||||
|
description: "Builds projects"
|
||||||
|
version: "1.0.0"
|
||||||
|
mode: shell
|
||||||
|
tools:
|
||||||
|
- "bash"
|
||||||
|
- "cargo"
|
||||||
|
---
|
||||||
|
# Builder
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.tools, vec!["bash", "cargo"]);
|
||||||
|
assert_eq!(manifest.mode, zclaw_skills::SkillMode::Shell);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_with_category() {
|
||||||
|
let content = r#"---
|
||||||
|
name: "Math Solver"
|
||||||
|
description: "Solves math problems"
|
||||||
|
version: "1.0.0"
|
||||||
|
mode: prompt-only
|
||||||
|
category: "math"
|
||||||
|
---
|
||||||
|
# Math Solver
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.category.unwrap(), "math");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_auto_classify_coding() {
|
||||||
|
let content = r#"---
|
||||||
|
name: "Code Helper"
|
||||||
|
description: "Helps with programming and debugging"
|
||||||
|
version: "1.0.0"
|
||||||
|
mode: prompt-only
|
||||||
|
---
|
||||||
|
# Code Helper
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
// Should auto-classify as "coding" based on description
|
||||||
|
assert_eq!(manifest.category.unwrap(), "coding");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_auto_classify_translation() {
|
||||||
|
let content = r#"---
|
||||||
|
name: "Translator"
|
||||||
|
description: "Helps with translation between languages"
|
||||||
|
version: "1.0.0"
|
||||||
|
mode: prompt-only
|
||||||
|
---
|
||||||
|
# Translator
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
// Should auto-classify based on "translat" keyword
|
||||||
|
assert!(manifest.category.is_some(), "Should auto-classify translation skill");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_no_frontmatter_extracts_name() {
|
||||||
|
let content = "# My Skill\n\nThis is a cool skill.";
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.name, "My Skill");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_fallback_name() {
|
||||||
|
let content = "Just some text without structure.";
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.name, "unnamed-skill");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_id_generation() {
|
||||||
|
let content = "---\nname: \"Hello World\"\n---\n";
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.id.as_str(), "hello-world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_all_modes() {
|
||||||
|
for (mode_str, expected) in &[
|
||||||
|
("prompt-only", zclaw_skills::SkillMode::PromptOnly),
|
||||||
|
("python", zclaw_skills::SkillMode::Python),
|
||||||
|
("shell", zclaw_skills::SkillMode::Shell),
|
||||||
|
("wasm", zclaw_skills::SkillMode::Wasm),
|
||||||
|
("native", zclaw_skills::SkillMode::Native),
|
||||||
|
] {
|
||||||
|
let content = format!("---\nname: \"Test\"\nmode: {}\n---\n", mode_str);
|
||||||
|
let manifest = parse_skill_md(&content).unwrap();
|
||||||
|
assert_eq!(&manifest.mode, expected, "Failed for mode: {}", mode_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_md_capabilities_csv() {
|
||||||
|
let content = "---\nname: \"Multi\"\ncapabilities: llm, web, file\n---\n";
|
||||||
|
let manifest = parse_skill_md(content).unwrap();
|
||||||
|
assert_eq!(manifest.capabilities, vec!["llm", "web", "file"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === parse_skill_toml ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_basic() {
|
||||||
|
let content = r#"
|
||||||
|
name = "Calculator"
|
||||||
|
description = "Performs calculations"
|
||||||
|
version = "2.0.0"
|
||||||
|
mode = "prompt_only"
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.name, "Calculator");
|
||||||
|
assert_eq!(manifest.description, "Performs calculations");
|
||||||
|
assert_eq!(manifest.version, "2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_with_id() {
|
||||||
|
let content = r#"
|
||||||
|
id = "my-calc"
|
||||||
|
name = "Calculator"
|
||||||
|
description = "Calc"
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.id.as_str(), "my-calc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_generates_id_from_name() {
|
||||||
|
let content = "name = \"Hello World\"\ndescription = \"x\"";
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.id.as_str(), "hello-world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_requires_name() {
|
||||||
|
let content = r#"description = "no name""#;
|
||||||
|
let result = parse_skill_toml(content);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_arrays() {
|
||||||
|
let content = r#"
|
||||||
|
name = "X"
|
||||||
|
description = "x"
|
||||||
|
tags = ["a", "b", "c"]
|
||||||
|
capabilities = ["llm"]
|
||||||
|
triggers = ["go", "run"]
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.tags, vec!["a", "b", "c"]);
|
||||||
|
assert_eq!(manifest.capabilities, vec!["llm"]);
|
||||||
|
assert_eq!(manifest.triggers, vec!["go", "run"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_category() {
|
||||||
|
let content = r#"
|
||||||
|
name = "X"
|
||||||
|
description = "x"
|
||||||
|
category = "data"
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.category.unwrap(), "data");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_tools() {
|
||||||
|
let content = r#"
|
||||||
|
name = "X"
|
||||||
|
description = "x"
|
||||||
|
tools = ["bash", "cargo"]
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.tools, vec!["bash", "cargo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_toml_ignores_comments_and_sections() {
|
||||||
|
let content = r#"
|
||||||
|
# This is a comment
|
||||||
|
[section]
|
||||||
|
name = "X"
|
||||||
|
description = "x"
|
||||||
|
"#;
|
||||||
|
let manifest = parse_skill_toml(content).unwrap();
|
||||||
|
assert_eq!(manifest.name, "X");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === discover_skills ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_skills_nonexistent_dir() {
|
||||||
|
let result = discover_skills(std::path::Path::new("/nonexistent/path")).unwrap();
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
78
crates/zclaw-skills/tests/runner_tests.rs
Normal file
78
crates/zclaw-skills/tests/runner_tests.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
//! Tests for PromptOnlySkill runner
|
||||||
|
|
||||||
|
use zclaw_skills::*;
|
||||||
|
use zclaw_types::SkillId;
|
||||||
|
|
||||||
|
/// Helper to create a minimal manifest
|
||||||
|
fn test_manifest(mode: SkillMode) -> SkillManifest {
|
||||||
|
SkillManifest {
|
||||||
|
id: SkillId::new("test-prompt-skill"),
|
||||||
|
name: "Test Prompt Skill".to_string(),
|
||||||
|
description: "A test prompt skill".to_string(),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
author: None,
|
||||||
|
mode,
|
||||||
|
capabilities: vec![],
|
||||||
|
input_schema: None,
|
||||||
|
output_schema: None,
|
||||||
|
tags: vec![],
|
||||||
|
category: None,
|
||||||
|
triggers: vec![],
|
||||||
|
tools: vec![],
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prompt_only_skill_returns_formatted_prompt() {
|
||||||
|
let manifest = test_manifest(SkillMode::PromptOnly);
|
||||||
|
let template = "Hello {{input}}, welcome!".to_string();
|
||||||
|
let skill = PromptOnlySkill::new(manifest, template);
|
||||||
|
|
||||||
|
let ctx = SkillContext::default();
|
||||||
|
let skill_ref: &dyn Skill = &skill;
|
||||||
|
let result = skill_ref.execute(&ctx, serde_json::json!("World")).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
let output = result.output.as_str().unwrap();
|
||||||
|
assert_eq!(output, "Hello World, welcome!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prompt_only_skill_json_input() {
|
||||||
|
let manifest = test_manifest(SkillMode::PromptOnly);
|
||||||
|
let template = "Input: {{input}}".to_string();
|
||||||
|
let skill = PromptOnlySkill::new(manifest, template);
|
||||||
|
|
||||||
|
let ctx = SkillContext::default();
|
||||||
|
let input = serde_json::json!({"key": "value"});
|
||||||
|
let skill_ref: &dyn Skill = &skill;
|
||||||
|
let result = skill_ref.execute(&ctx, input).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
let output = result.output.as_str().unwrap();
|
||||||
|
assert!(output.contains("key"));
|
||||||
|
assert!(output.contains("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prompt_only_skill_no_placeholder() {
|
||||||
|
let manifest = test_manifest(SkillMode::PromptOnly);
|
||||||
|
let template = "Static prompt content".to_string();
|
||||||
|
let skill = PromptOnlySkill::new(manifest, template);
|
||||||
|
|
||||||
|
let ctx = SkillContext::default();
|
||||||
|
let skill_ref: &dyn Skill = &skill;
|
||||||
|
let result = skill_ref.execute(&ctx, serde_json::json!("ignored")).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
assert_eq!(result.output.as_str().unwrap(), "Static prompt content");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prompt_only_skill_manifest() {
|
||||||
|
let manifest = test_manifest(SkillMode::PromptOnly);
|
||||||
|
let skill = PromptOnlySkill::new(manifest.clone(), "prompt".to_string());
|
||||||
|
assert_eq!(skill.manifest().id.as_str(), "test-prompt-skill");
|
||||||
|
assert_eq!(skill.manifest().name, "Test Prompt Skill");
|
||||||
|
}
|
||||||
148
crates/zclaw-skills/tests/skill_types_tests.rs
Normal file
148
crates/zclaw-skills/tests/skill_types_tests.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
//! Tests for zclaw-skills types: SkillManifest, SkillMode, SkillResult, SkillContext
|
||||||
|
|
||||||
|
use serde_json;
|
||||||
|
use zclaw_skills::*;
|
||||||
|
use zclaw_types::SkillId;
|
||||||
|
|
||||||
|
// === SkillMode ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_mode_serialization_roundtrip() {
|
||||||
|
let modes = vec![
|
||||||
|
SkillMode::PromptOnly,
|
||||||
|
SkillMode::Python,
|
||||||
|
SkillMode::Shell,
|
||||||
|
SkillMode::Wasm,
|
||||||
|
SkillMode::Native,
|
||||||
|
];
|
||||||
|
for mode in modes {
|
||||||
|
let json = serde_json::to_string(&mode).unwrap();
|
||||||
|
let parsed: SkillMode = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(mode, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_mode_snake_case_serialization() {
|
||||||
|
let json = serde_json::to_string(&SkillMode::PromptOnly).unwrap();
|
||||||
|
assert!(json.contains("prompt_only"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SkillResult ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_result_success() {
|
||||||
|
let result = SkillResult::success(serde_json::json!({"answer": 42}));
|
||||||
|
assert!(result.success);
|
||||||
|
assert!(result.error.is_none());
|
||||||
|
assert_eq!(result.output["answer"], 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_result_error() {
|
||||||
|
let result = SkillResult::error("execution failed");
|
||||||
|
assert!(!result.success);
|
||||||
|
assert_eq!(result.error.unwrap(), "execution failed");
|
||||||
|
assert!(result.output.is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_result_roundtrip() {
|
||||||
|
let result = SkillResult {
|
||||||
|
success: true,
|
||||||
|
output: serde_json::json!("hello"),
|
||||||
|
error: None,
|
||||||
|
duration_ms: Some(150),
|
||||||
|
tokens_used: Some(42),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&result).unwrap();
|
||||||
|
let parsed: SkillResult = serde_json::from_str(&json).unwrap();
|
||||||
|
assert!(parsed.success);
|
||||||
|
assert_eq!(parsed.duration_ms.unwrap(), 150);
|
||||||
|
assert_eq!(parsed.tokens_used.unwrap(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SkillManifest ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_manifest_full_roundtrip() {
|
||||||
|
let manifest = SkillManifest {
|
||||||
|
id: SkillId::new("test-skill"),
|
||||||
|
name: "Test Skill".to_string(),
|
||||||
|
description: "A test skill".to_string(),
|
||||||
|
version: "2.0.0".to_string(),
|
||||||
|
author: Some("tester".to_string()),
|
||||||
|
mode: SkillMode::PromptOnly,
|
||||||
|
capabilities: vec!["llm".to_string()],
|
||||||
|
input_schema: Some(serde_json::json!({"type": "object"})),
|
||||||
|
output_schema: None,
|
||||||
|
tags: vec!["test".to_string()],
|
||||||
|
category: Some("coding".to_string()),
|
||||||
|
triggers: vec!["test trigger".to_string()],
|
||||||
|
tools: vec!["bash".to_string()],
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&manifest).unwrap();
|
||||||
|
let parsed: SkillManifest = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(parsed.id.as_str(), "test-skill");
|
||||||
|
assert_eq!(parsed.name, "Test Skill");
|
||||||
|
assert_eq!(parsed.mode, SkillMode::PromptOnly);
|
||||||
|
assert_eq!(parsed.capabilities.len(), 1);
|
||||||
|
assert_eq!(parsed.triggers.len(), 1);
|
||||||
|
assert_eq!(parsed.tools.len(), 1);
|
||||||
|
assert_eq!(parsed.category.unwrap(), "coding");
|
||||||
|
assert!(parsed.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_manifest_default_enabled() {
|
||||||
|
let json = r#"{"id":"x","name":"X","description":"x","version":"1.0","mode":"prompt_only"}"#;
|
||||||
|
let manifest: SkillManifest = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(manifest.enabled, "enabled should default to true");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_manifest_disabled() {
|
||||||
|
let json = r#"{"id":"x","name":"X","description":"x","version":"1.0","mode":"prompt_only","enabled":false}"#;
|
||||||
|
let manifest: SkillManifest = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(!manifest.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_manifest_all_modes_roundtrip() {
|
||||||
|
for mode in &[SkillMode::PromptOnly, SkillMode::Python, SkillMode::Shell, SkillMode::Wasm] {
|
||||||
|
let manifest = SkillManifest {
|
||||||
|
id: SkillId::new("m"),
|
||||||
|
name: "M".into(),
|
||||||
|
description: "d".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
author: None,
|
||||||
|
mode: mode.clone(),
|
||||||
|
capabilities: vec![],
|
||||||
|
input_schema: None,
|
||||||
|
output_schema: None,
|
||||||
|
tags: vec![],
|
||||||
|
category: None,
|
||||||
|
triggers: vec![],
|
||||||
|
tools: vec![],
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&manifest).unwrap();
|
||||||
|
let parsed: SkillManifest = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(*mode, parsed.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SkillContext ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_context_default() {
|
||||||
|
let ctx = SkillContext::default();
|
||||||
|
assert!(ctx.agent_id.is_empty());
|
||||||
|
assert!(ctx.session_id.is_empty());
|
||||||
|
assert!(ctx.working_dir.is_none());
|
||||||
|
assert_eq!(ctx.timeout_secs, 60);
|
||||||
|
assert!(!ctx.network_allowed);
|
||||||
|
assert!(!ctx.file_access_allowed);
|
||||||
|
assert!(ctx.llm.is_none());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user