Files
agentskills/skills-ref/tests/test_validator.py
T
Keith Lazuka 547831f3a2 Add skills-ref library (#2)
Reference library for Agent Skills with CLI and Python API:
- validate: Check skill directories for valid SKILL.md with proper frontmatter
- read-properties: Parse and output skill properties as JSON
- to-prompt: Generate suggested <available_skills> XML for agent prompts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 09:39:21 -05:00

291 lines
7.1 KiB
Python

"""Tests for validator module."""
from skills_ref.validator import validate
def test_valid_skill(tmp_path):
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my-skill
description: A test skill
---
# My Skill
""")
errors = validate(skill_dir)
assert errors == []
def test_nonexistent_path(tmp_path):
errors = validate(tmp_path / "nonexistent")
assert len(errors) == 1
assert "does not exist" in errors[0]
def test_not_a_directory(tmp_path):
file_path = tmp_path / "file.txt"
file_path.write_text("test")
errors = validate(file_path)
assert len(errors) == 1
assert "Not a directory" in errors[0]
def test_missing_skill_md(tmp_path):
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
errors = validate(skill_dir)
assert len(errors) == 1
assert "Missing required file: SKILL.md" in errors[0]
def test_invalid_name_uppercase(tmp_path):
skill_dir = tmp_path / "MySkill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: MySkill
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert any("lowercase" in e for e in errors)
def test_name_too_long(tmp_path):
long_name = "a" * 70 # Exceeds 64 char limit
skill_dir = tmp_path / long_name
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(f"""---
name: {long_name}
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert any("exceeds" in e and "character limit" in e for e in errors)
def test_name_leading_hyphen(tmp_path):
skill_dir = tmp_path / "-my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: -my-skill
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert any("cannot start or end with a hyphen" in e for e in errors)
def test_name_consecutive_hyphens(tmp_path):
skill_dir = tmp_path / "my--skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my--skill
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert any("consecutive hyphens" in e for e in errors)
def test_name_invalid_characters(tmp_path):
skill_dir = tmp_path / "my_skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my_skill
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert any("invalid characters" in e for e in errors)
def test_name_directory_mismatch(tmp_path):
skill_dir = tmp_path / "wrong-name"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: correct-name
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert any("must match skill name" in e for e in errors)
def test_unexpected_fields(tmp_path):
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my-skill
description: A test skill
unknown_field: should not be here
---
Body
""")
errors = validate(skill_dir)
assert any("Unexpected fields" in e for e in errors)
def test_valid_with_all_fields(tmp_path):
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my-skill
description: A test skill
license: MIT
metadata:
author: Test
---
Body
""")
errors = validate(skill_dir)
assert errors == []
def test_allowed_tools_accepted(tmp_path):
"""allowed-tools is accepted (experimental feature)."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my-skill
description: A test skill
allowed-tools: Bash(jq:*) Bash(git:*)
---
Body
""")
errors = validate(skill_dir)
assert errors == []
def test_i18n_chinese_name(tmp_path):
"""Chinese characters are allowed in skill names."""
skill_dir = tmp_path / "技能"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: 技能
description: A skill with Chinese name
---
Body
""")
errors = validate(skill_dir)
assert errors == []
def test_i18n_russian_name_with_hyphens(tmp_path):
"""Russian names with hyphens are allowed."""
skill_dir = tmp_path / "мой-навык"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: мой-навык
description: A skill with Russian name
---
Body
""")
errors = validate(skill_dir)
assert errors == []
def test_i18n_russian_lowercase_valid(tmp_path):
"""Russian lowercase names should be accepted."""
skill_dir = tmp_path / "навык"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: навык
description: A skill with Russian lowercase name
---
Body
""")
errors = validate(skill_dir)
assert errors == []
def test_i18n_russian_uppercase_rejected(tmp_path):
"""Russian uppercase names should be rejected."""
skill_dir = tmp_path / "НАВЫК"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: НАВЫК
description: A skill with Russian uppercase name
---
Body
""")
errors = validate(skill_dir)
assert any("lowercase" in e for e in errors)
def test_description_too_long(tmp_path):
"""Description exceeding 1024 chars should fail."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
long_desc = "x" * 1100
(skill_dir / "SKILL.md").write_text(f"""---
name: my-skill
description: {long_desc}
---
Body
""")
errors = validate(skill_dir)
assert any("exceeds" in e and "1024" in e for e in errors)
def test_valid_compatibility(tmp_path):
"""Valid compatibility field should be accepted."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""---
name: my-skill
description: A test skill
compatibility: Requires Python 3.11+
---
Body
""")
errors = validate(skill_dir)
assert errors == []
def test_compatibility_too_long(tmp_path):
"""Compatibility exceeding 500 chars should fail."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
long_compat = "x" * 550
(skill_dir / "SKILL.md").write_text(f"""---
name: my-skill
description: A test skill
compatibility: {long_compat}
---
Body
""")
errors = validate(skill_dir)
assert any("exceeds" in e and "500" in e for e in errors)
def test_nfkc_normalization(tmp_path):
"""Skill names are NFKC normalized before validation.
The name 'café' can be represented two ways:
- Precomposed: 'café' (4 chars, 'é' is U+00E9)
- Decomposed: 'café' (5 chars, 'e' + combining acute U+0301)
NFKC normalizes both to the precomposed form.
"""
# Use decomposed form: 'cafe' + combining acute accent (U+0301)
decomposed_name = "cafe\u0301" # 'café' with combining accent
composed_name = "café" # precomposed form
# Directory uses composed form, SKILL.md uses decomposed - should match after normalization
skill_dir = tmp_path / composed_name
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(f"""---
name: {decomposed_name}
description: A test skill
---
Body
""")
errors = validate(skill_dir)
assert errors == [], f"Expected no errors, got: {errors}"