Skip to main content

cc_plugin_validator/
lib.rs

1use 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}