Files
devplacepy/tests/unit/services/skills/parser.py
T

108 lines
4.1 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import pytest
from devplacepy.services.skills import limits
from devplacepy.services.skills.parser import SkillFormatError, parse_skill_md
def skill(frontmatter: str, body: str = "# Body\n\nDo the thing.") -> str:
return f"---\n{frontmatter}\n---\n{body}"
def test_minimal_skill_parses():
parsed = parse_skill_md(skill("name: my-skill\ndescription: Does a thing."))
assert parsed.name == "my-skill"
assert parsed.description == "Does a thing."
assert parsed.body.startswith("# Body")
assert parsed.metadata == {}
assert parsed.warnings == ()
def test_all_known_fields_parse():
parsed = parse_skill_md(
skill(
"name: full\n"
"description: Full skill\n"
"license: MIT\n"
"compatibility: any agent\n"
"allowed-tools: Read Write\n"
"metadata:\n"
" author: someone\n"
" version: '1.0'"
)
)
assert parsed.license == "MIT"
assert parsed.compatibility == "any agent"
assert parsed.allowed_tools == "Read Write"
assert parsed.metadata == {"author": "someone", "version": "1.0"}
def test_block_scalars():
folded = parse_skill_md(skill("name: a\ndescription: >\n first line\n second line"))
assert folded.description == "first line second line"
literal = parse_skill_md(skill("name: a\ndescription: |\n first\n second"))
assert literal.description == "first\nsecond"
def test_quoted_values_and_inline_comments():
parsed = parse_skill_md(skill('name: a\ndescription: "Quoted: value" '))
assert parsed.description == "Quoted: value"
parsed = parse_skill_md(skill("name: a\ndescription: plain # a comment"))
assert parsed.description == "plain"
def test_unknown_key_is_ignored_with_a_warning():
parsed = parse_skill_md(skill("name: a\ndescription: d\nfuture-key: x"))
assert parsed.warnings == ("Ignored unknown frontmatter key 'future-key'.",)
def test_bom_and_crlf_are_accepted():
text = "---\r\nname: a\r\ndescription: d\r\n---\r\nBody"
assert parse_skill_md(text).body == "Body"
@pytest.mark.parametrize(
"text,fragment",
[
("name: a\ndescription: d", "no YAML frontmatter"),
("---\nname: a\ndescription: d\n", "not closed"),
(skill("description: d"), "'name' is required"),
(skill("name: a"), "'description' is required"),
(skill("name: A_B\ndescription: d"), "lowercase letters"),
(skill("name: -a\ndescription: d"), "lowercase letters"),
(skill("name: a--b\ndescription: d"), "lowercase letters"),
(skill("name: a\ndescription: d\nname: b"), "duplicate frontmatter key"),
(skill("name: a\ndescription: [x]"), "flow collections"),
(skill("name: a\ndescription: d\nmetadata: x"), "indented map"),
(skill("name: a\ndescription: d\nmetadata:\n a: 1\n b: 2"), "flat map"),
(skill("name: a\ndescription: d\nmetadata:\n a: 1\n a: 2"), "duplicate metadata key"),
(skill("name: a\ndescription: \"unterminated"), "unterminated"),
(skill("name: a\ndescription: d\n\tbad: x"), "tab"),
(skill("name: a\ndescription: d") + "\x00", "NUL"),
],
)
def test_malformed_skills_are_rejected_with_a_reason(text, fragment):
with pytest.raises(SkillFormatError) as error:
parse_skill_md(text)
assert fragment in str(error.value)
def test_overlong_fields_are_rejected():
with pytest.raises(SkillFormatError):
parse_skill_md(skill(f"name: {'a' * (limits.NAME_MAX_CHARS + 1)}\ndescription: d"))
with pytest.raises(SkillFormatError):
parse_skill_md(skill(f"name: a\ndescription: {'d' * (limits.DESCRIPTION_MAX_CHARS + 1)}"))
def test_metadata_entry_cap():
entries = "\n".join(f" k{i}: v" for i in range(limits.METADATA_MAX_ENTRIES + 1))
with pytest.raises(SkillFormatError):
parse_skill_md(skill(f"name: a\ndescription: d\nmetadata:\n{entries}"))
def test_frontmatter_scan_is_bounded():
padding = "\n".join("# c" for _ in range(limits.FRONTMATTER_MAX_LINES + 5))
with pytest.raises(SkillFormatError):
parse_skill_md(f"---\nname: a\ndescription: d\n{padding}\n---\nBody")