108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
# 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")
|