DevPlace CI / test (push) Failing after 31m42s
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>
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")
|