cc_plugin_validator/
lib.rs1use serde_json::Value;
2
3mod component;
4mod manifest;
5
6pub use component::validate_component_markdown;
7pub use manifest::validate_plugin_manifest;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ValidationIssue {
11 pub path: String,
12 pub code: String,
13 pub message: String,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum ValidationResult {
18 Success { data: Value },
19 Failure { issues: Vec<ValidationIssue> },
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ComponentValidation {
24 pub path: String,
25 pub errors: Vec<ValidationIssue>,
26 pub warnings: Vec<ValidationIssue>,
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32
33 #[test]
34 fn manifest_valid_minimal() {
35 let input = serde_json::json!({
36 "name": "demo-plugin",
37 "commands": {
38 "about": {
39 "source": "./commands/about.md"
40 }
41 }
42 });
43
44 let result = validate_plugin_manifest(input);
45 match result {
46 ValidationResult::Success { .. } => {}
47 ValidationResult::Failure { issues } => {
48 panic!("expected success, got: {issues:#?}")
49 }
50 }
51 }
52
53 #[test]
54 fn manifest_invalid_should_fail() {
55 let input = serde_json::json!({
56 "name": "bad plugin",
57 "commands": {
58 "x": {
59 "source": "commands/no-prefix.md",
60 "content": "bad"
61 }
62 },
63 "hooks": {
64 "NotARealHookEvent": []
65 }
66 });
67
68 let result = validate_plugin_manifest(input);
69 match result {
70 ValidationResult::Failure { issues } => {
71 assert!(!issues.is_empty());
72 }
73 ValidationResult::Success { .. } => panic!("expected failure"),
74 }
75 }
76
77 #[test]
78 fn component_frontmatter_validation() {
79 let markdown = "---\nname: reviewer\ndescription: checks code\nallowed-tools:\n - Bash\nshell: bash\n---\n\n# Reviewer\n";
80 let result = validate_component_markdown("agents/reviewer.md", markdown, "agent");
81 assert!(result.errors.is_empty());
82 }
83
84 #[test]
85 fn component_invalid_frontmatter_validation() {
86 let markdown =
87 "---\nname: 1\ndescription:\n nested: yes\nshell: fish\n---\n\n# Reviewer\n";
88 let result = validate_component_markdown("agents/reviewer.md", markdown, "agent");
89 assert!(!result.errors.is_empty());
90 }
91
92 #[test]
93 fn fixture_manifest_valid() {
94 let s =
95 std::fs::read_to_string("fixtures/manifest/valid/plugin.json").expect("read fixture");
96 let v: Value = serde_json::from_str(&s).expect("json");
97 match validate_plugin_manifest(v) {
98 ValidationResult::Success { .. } => {}
99 ValidationResult::Failure { issues } => {
100 panic!("expected fixture valid to pass, got {issues:#?}")
101 }
102 }
103 }
104
105 #[test]
106 fn fixture_manifest_invalid() {
107 let s =
108 std::fs::read_to_string("fixtures/manifest/invalid/plugin.json").expect("read fixture");
109 let v: Value = serde_json::from_str(&s).expect("json");
110 match validate_plugin_manifest(v) {
111 ValidationResult::Failure { issues } => assert!(!issues.is_empty()),
112 ValidationResult::Success { .. } => panic!("expected fixture invalid to fail"),
113 }
114 }
115
116 #[test]
117 fn fixture_component_valid() {
118 let s = std::fs::read_to_string("fixtures/components/valid/agent.md").expect("read md");
119 let r = validate_component_markdown("fixtures/components/valid/agent.md", &s, "agent");
120 assert!(r.errors.is_empty());
121 }
122
123 #[test]
124 fn fixture_component_invalid() {
125 let s = std::fs::read_to_string("fixtures/components/invalid/agent.md").expect("read md");
126 let r = validate_component_markdown("fixtures/components/invalid/agent.md", &s, "agent");
127 assert!(!r.errors.is_empty());
128 }
129}