200 lines
5.6 KiB
Python
200 lines
5.6 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""Tests for skill validator module."""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import tempfile
|
|||
|
|
import unittest
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|||
|
|
from tools.skill_validator import SkillValidator, SkillValidationError
|
|||
|
|
|
|||
|
|
|
|||
|
|
VALID_SKILL = """---
|
|||
|
|
name: test-skill
|
|||
|
|
description: A valid test skill for validation
|
|||
|
|
version: "1.0.0"
|
|||
|
|
author: ezra
|
|||
|
|
tags: [testing, validation]
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Test Skill
|
|||
|
|
|
|||
|
|
## Trigger
|
|||
|
|
Use when testing skill validation.
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
1. First step: do something
|
|||
|
|
2. Second step: verify
|
|||
|
|
3. Third step: done
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
echo "hello world"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Pitfalls
|
|||
|
|
- Don't forget to test edge cases
|
|||
|
|
|
|||
|
|
## Verification
|
|||
|
|
- Check the output matches expected
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
MINIMAL_SKILL = """---
|
|||
|
|
name: minimal
|
|||
|
|
description: Minimal skill
|
|||
|
|
version: "1.0"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Trigger
|
|||
|
|
When needed.
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
1. Do it.
|
|||
|
|
2. Done.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
BROKEN_SKILL_NO_FM = """# No Frontmatter Skill
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
1. This will fail validation
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
BROKEN_SKILL_BAD_YAML = """---
|
|||
|
|
name: [invalid yaml
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
1. test
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
BROKEN_SKILL_MISSING_FIELDS = """---
|
|||
|
|
description: Missing name and version
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
1. test
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestSkillValidationError(unittest.TestCase):
|
|||
|
|
def test_repr_error(self):
|
|||
|
|
e = SkillValidationError("ERROR", "bad thing", "frontmatter")
|
|||
|
|
self.assertIn("❌", repr(e))
|
|||
|
|
self.assertIn("bad thing", repr(e))
|
|||
|
|
|
|||
|
|
def test_repr_warning(self):
|
|||
|
|
e = SkillValidationError("WARNING", "maybe bad")
|
|||
|
|
self.assertIn("⚠️", repr(e))
|
|||
|
|
|
|||
|
|
def test_repr_info(self):
|
|||
|
|
e = SkillValidationError("INFO", "just fyi")
|
|||
|
|
self.assertIn("ℹ️", repr(e))
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestSkillValidator(unittest.TestCase):
|
|||
|
|
def setUp(self):
|
|||
|
|
self.validator = SkillValidator()
|
|||
|
|
self.tmp_dir = tempfile.mkdtemp()
|
|||
|
|
|
|||
|
|
def tearDown(self):
|
|||
|
|
import shutil
|
|||
|
|
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
|||
|
|
|
|||
|
|
def _write_skill(self, content: str, name: str = "test-skill") -> Path:
|
|||
|
|
skill_dir = Path(self.tmp_dir) / name
|
|||
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|||
|
|
path = skill_dir / "SKILL.md"
|
|||
|
|
path.write_text(content)
|
|||
|
|
return path
|
|||
|
|
|
|||
|
|
def test_valid_skill_no_errors(self):
|
|||
|
|
path = self._write_skill(VALID_SKILL)
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
error_count = len([e for e in errors if e.level == "ERROR"])
|
|||
|
|
self.assertEqual(error_count, 0, f"Unexpected errors: {errors}")
|
|||
|
|
|
|||
|
|
def test_minimal_skill_warnings_only(self):
|
|||
|
|
path = self._write_skill(MINIMAL_SKILL, "minimal")
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
error_count = len([e for e in errors if e.level == "ERROR"])
|
|||
|
|
self.assertEqual(error_count, 0)
|
|||
|
|
# Should have warnings for missing recommended sections
|
|||
|
|
warning_count = len([e for e in errors if e.level == "WARNING"])
|
|||
|
|
self.assertGreater(warning_count, 0)
|
|||
|
|
|
|||
|
|
def test_no_frontmatter_error(self):
|
|||
|
|
path = self._write_skill(BROKEN_SKILL_NO_FM, "broken1")
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
fm_errors = [e for e in errors if "frontmatter" in e.field and e.level == "ERROR"]
|
|||
|
|
self.assertGreater(len(fm_errors), 0)
|
|||
|
|
|
|||
|
|
def test_bad_yaml_error(self):
|
|||
|
|
path = self._write_skill(BROKEN_SKILL_BAD_YAML, "broken2")
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
yaml_errors = [e for e in errors if "YAML" in e.message or "frontmatter" in e.field]
|
|||
|
|
self.assertGreater(len(yaml_errors), 0)
|
|||
|
|
|
|||
|
|
def test_missing_required_fields(self):
|
|||
|
|
path = self._write_skill(BROKEN_SKILL_MISSING_FIELDS, "broken3")
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
missing = [e for e in errors if "Missing required" in e.message]
|
|||
|
|
self.assertGreater(len(missing), 0)
|
|||
|
|
|
|||
|
|
def test_file_not_found(self):
|
|||
|
|
errors = self.validator.validate_file(Path("/nonexistent/SKILL.md"))
|
|||
|
|
self.assertEqual(len(errors), 1)
|
|||
|
|
self.assertEqual(errors[0].level, "ERROR")
|
|||
|
|
|
|||
|
|
def test_empty_file(self):
|
|||
|
|
path = self._write_skill("", "empty")
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
self.assertTrue(any(e.message == "File is empty" for e in errors))
|
|||
|
|
|
|||
|
|
def test_invalid_name_format(self):
|
|||
|
|
skill = """---
|
|||
|
|
name: BAD NAME!
|
|||
|
|
description: test
|
|||
|
|
version: "1.0"
|
|||
|
|
---
|
|||
|
|
## Trigger
|
|||
|
|
test
|
|||
|
|
## Steps
|
|||
|
|
1. test
|
|||
|
|
2. done
|
|||
|
|
"""
|
|||
|
|
path = self._write_skill(skill, "badname")
|
|||
|
|
errors = self.validator.validate_file(path)
|
|||
|
|
name_errors = [e for e in errors if "Invalid name" in e.message]
|
|||
|
|
self.assertGreater(len(name_errors), 0)
|
|||
|
|
|
|||
|
|
def test_validate_all(self):
|
|||
|
|
self._write_skill(VALID_SKILL, "skill-a")
|
|||
|
|
self._write_skill(MINIMAL_SKILL, "skill-b")
|
|||
|
|
results = self.validator.validate_all(Path(self.tmp_dir))
|
|||
|
|
self.assertEqual(len(results), 2)
|
|||
|
|
self.assertIn("skill-a", results)
|
|||
|
|
self.assertIn("skill-b", results)
|
|||
|
|
|
|||
|
|
def test_format_report(self):
|
|||
|
|
self._write_skill(VALID_SKILL, "good")
|
|||
|
|
self._write_skill(BROKEN_SKILL_NO_FM, "bad")
|
|||
|
|
results = self.validator.validate_all(Path(self.tmp_dir))
|
|||
|
|
report = self.validator.format_report(results)
|
|||
|
|
self.assertIn("Skill Validation Report", report)
|
|||
|
|
self.assertIn("good", report)
|
|||
|
|
self.assertIn("bad", report)
|
|||
|
|
|
|||
|
|
def test_nonstandard_subdir_warning(self):
|
|||
|
|
skill_dir = Path(self.tmp_dir) / "weirdskill"
|
|||
|
|
skill_dir.mkdir()
|
|||
|
|
(skill_dir / "SKILL.md").write_text(VALID_SKILL)
|
|||
|
|
(skill_dir / "random_dir").mkdir()
|
|||
|
|
errors = self.validator.validate_file(skill_dir / "SKILL.md")
|
|||
|
|
dir_warnings = [e for e in errors if "Non-standard" in e.message]
|
|||
|
|
self.assertGreater(len(dir_warnings), 0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
unittest.main()
|