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()
|