mirror of
https://github.com/agentskills/agentskills.git
synced 2026-06-18 15:54:06 +08:00
547831f3a2
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>
189 lines
4.7 KiB
Python
189 lines
4.7 KiB
Python
"""Tests for parser module."""
|
|
|
|
import pytest
|
|
|
|
from skills_ref.parser import (
|
|
ParseError,
|
|
ValidationError,
|
|
find_skill_md,
|
|
parse_frontmatter,
|
|
read_properties,
|
|
)
|
|
|
|
|
|
def test_valid_frontmatter():
|
|
content = """---
|
|
name: my-skill
|
|
description: A test skill
|
|
---
|
|
# My Skill
|
|
|
|
Instructions here.
|
|
"""
|
|
metadata, body = parse_frontmatter(content)
|
|
assert metadata["name"] == "my-skill"
|
|
assert metadata["description"] == "A test skill"
|
|
assert "# My Skill" in body
|
|
|
|
|
|
def test_missing_frontmatter():
|
|
content = "# No frontmatter here"
|
|
with pytest.raises(ParseError, match="must start with YAML frontmatter"):
|
|
parse_frontmatter(content)
|
|
|
|
|
|
def test_unclosed_frontmatter():
|
|
content = """---
|
|
name: my-skill
|
|
description: A test skill
|
|
"""
|
|
with pytest.raises(ParseError, match="not properly closed"):
|
|
parse_frontmatter(content)
|
|
|
|
|
|
def test_invalid_yaml():
|
|
content = """---
|
|
name: [invalid
|
|
description: broken
|
|
---
|
|
Body here
|
|
"""
|
|
with pytest.raises(ParseError, match="Invalid YAML"):
|
|
parse_frontmatter(content)
|
|
|
|
|
|
def test_non_dict_frontmatter():
|
|
content = """---
|
|
- just
|
|
- a
|
|
- list
|
|
---
|
|
Body
|
|
"""
|
|
with pytest.raises(ParseError, match="must be a YAML mapping"):
|
|
parse_frontmatter(content)
|
|
|
|
|
|
def test_read_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
|
|
license: MIT
|
|
---
|
|
# My Skill
|
|
""")
|
|
props = read_properties(skill_dir)
|
|
assert props.name == "my-skill"
|
|
assert props.description == "A test skill"
|
|
assert props.license == "MIT"
|
|
|
|
|
|
def test_read_with_metadata(tmp_path):
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text("""---
|
|
name: my-skill
|
|
description: A test skill
|
|
metadata:
|
|
author: Test Author
|
|
version: 1.0
|
|
---
|
|
Body
|
|
""")
|
|
props = read_properties(skill_dir)
|
|
assert props.metadata == {"author": "Test Author", "version": "1.0"}
|
|
|
|
|
|
def test_missing_skill_md(tmp_path):
|
|
with pytest.raises(ParseError, match="SKILL.md not found"):
|
|
read_properties(tmp_path)
|
|
|
|
|
|
def test_missing_name(tmp_path):
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text("""---
|
|
description: A test skill
|
|
---
|
|
Body
|
|
""")
|
|
with pytest.raises(ValidationError, match="Missing required field.*name"):
|
|
read_properties(skill_dir)
|
|
|
|
|
|
def test_missing_description(tmp_path):
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text("""---
|
|
name: my-skill
|
|
---
|
|
Body
|
|
""")
|
|
with pytest.raises(ValidationError, match="Missing required field.*description"):
|
|
read_properties(skill_dir)
|
|
|
|
|
|
def test_find_skill_md_prefers_uppercase(tmp_path):
|
|
"""SKILL.md should be preferred over skill.md when both exist."""
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text("uppercase")
|
|
(skill_dir / "skill.md").write_text("lowercase")
|
|
result = find_skill_md(skill_dir)
|
|
assert result is not None
|
|
assert result.name == "SKILL.md"
|
|
|
|
|
|
def test_find_skill_md_accepts_lowercase(tmp_path):
|
|
"""skill.md should be accepted when SKILL.md doesn't exist."""
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "skill.md").write_text("lowercase")
|
|
result = find_skill_md(skill_dir)
|
|
assert result is not None
|
|
# Check case-insensitively since some filesystems are case-insensitive
|
|
assert result.name.lower() == "skill.md"
|
|
|
|
|
|
def test_find_skill_md_returns_none_when_missing(tmp_path):
|
|
"""find_skill_md should return None when no skill.md exists."""
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
result = find_skill_md(skill_dir)
|
|
assert result is None
|
|
|
|
|
|
def test_read_properties_with_lowercase_skill_md(tmp_path):
|
|
"""read_properties should work with lowercase skill.md."""
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "skill.md").write_text("""---
|
|
name: my-skill
|
|
description: A test skill
|
|
---
|
|
# My Skill
|
|
""")
|
|
props = read_properties(skill_dir)
|
|
assert props.name == "my-skill"
|
|
assert props.description == "A test skill"
|
|
|
|
|
|
def test_read_with_allowed_tools(tmp_path):
|
|
"""allowed-tools should be parsed into SkillProperties."""
|
|
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
|
|
""")
|
|
props = read_properties(skill_dir)
|
|
assert props.allowed_tools == "Bash(jq:*) Bash(git:*)"
|
|
# Verify to_dict outputs as "allowed-tools" (hyphenated)
|
|
d = props.to_dict()
|
|
assert d["allowed-tools"] == "Bash(jq:*) Bash(git:*)"
|