Files
devplacepy/tests/unit/services/skills/parser.py
T
retoorandClaude Sonnet 5.5 f55c7ee5f4
DevPlace CI / test (push) Failing after 31m42s
Add community node protocol spec, Agent Skills, and make the failing tests hermetic
Community nodes (Phase 1 of plan.md): the protocol specification in
services/nodes/spec.py as the single source of truth, GET /nodes/spec.json,
a generated protocol docs page, the Community nodes API docs group, and the
implementation plan plus the residential proxy research. The no-open-proxy rule
is enforced in the spec (open_proxy cannot be set).

Agent Skills: register the Skills API docs group, cascade skills on account
deletion, add the nested CLAUDE.md, README and routes docs, and cover the
parser, the fetcher (end to end against a mock origin), the data layer and the
routes with 73 unit and 16 api tests.

Tests made hermetic with the repo's existing patterns instead of ambient
environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the
workspace tests install FakeBackend via runtime.set_backend, and the skills
install tests use run_async. DockerCliBackend now returns 127 (command not
found) when the CLI is missing instead of raising FileNotFoundError, so a host
without docker answers with a clean error rather than a 500. The Providers tab
empty state no longer names the Model routes tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 07:13:25 +02:00

108 lines
4.1 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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")