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:
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