202 lines
6.0 KiBLFS
Python
202 lines
6.0 KiBLFS
Python
#!/usr/bin/env python3
|
|
"""Behavioral tests for the task skill frontmatter linter.
|
|
|
|
Run with the stdlib test runner (no third-party deps, matching the linter):
|
|
|
|
python3 .github/scripts/test_lint_skill_frontmatter.py
|
|
|
|
Each case writes a temporary ``<dir>/SKILL.md`` and asserts on ``lint_one``.
|
|
The directory name matters: the linter enforces ``name == directory``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
import lint_skill_frontmatter as lsf
|
|
|
|
|
|
def _lint(body: str, dir_name: str = "my-skill") -> list[str]:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
skill_dir = Path(tmp) / dir_name
|
|
skill_dir.mkdir()
|
|
skill_md = skill_dir / "SKILL.md"
|
|
skill_md.write_text(body, encoding="utf-8")
|
|
return lsf.lint_one(skill_md)
|
|
|
|
|
|
# Each case: (label, frontmatter+body, directory name).
|
|
PASS_CASES = [
|
|
(
|
|
"scalar name + description",
|
|
"---\nname: my-skill\ndescription: Do a thing.\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"block-scalar description",
|
|
"---\nname: my-skill\ndescription: |\n Multi-line\n description.\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"folded-scalar description",
|
|
"---\nname: my-skill\ndescription: >\n Folded text.\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"quoted name",
|
|
'---\nname: "my-skill"\ndescription: ok\n---\n# Body\n',
|
|
"my-skill",
|
|
),
|
|
(
|
|
"compatibility scalar string",
|
|
"---\nname: my-skill\ndescription: ok\ncompatibility: Requires git and docker\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"license scalar string",
|
|
"---\nname: my-skill\ndescription: ok\nlicense: Apache-2.0\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"inline-flow metadata",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata: {a: b, c: d}\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"empty inline metadata",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata: {}\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"block-mapping metadata with scalar values",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata:\n version: '2.1.2'\n author: someone\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"block-mapping metadata with nested hook values",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata:\n hooks:\n Stop:\n - command: run.sh\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"inline metadata value that is a collection",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata: {a: [1, 2]}\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
# Regressions: valid YAML the hand-rolled parser used to reject.
|
|
(
|
|
"trailing comment on name",
|
|
"---\nname: my-skill # canonical\ndescription: ok\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"trailing comment on inline metadata",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata: {a: b} # note\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"quoted comma inside inline metadata value",
|
|
'---\nname: my-skill\ndescription: ok\nmetadata: {tags: "a, b"}\n---\n# Body\n',
|
|
"my-skill",
|
|
),
|
|
]
|
|
|
|
FAIL_CASES = [
|
|
(
|
|
"name does not match directory",
|
|
"---\nname: other-name\ndescription: ok\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"name with invalid characters",
|
|
"---\nname: My_Skill\ndescription: ok\n---\n# Body\n",
|
|
"My_Skill",
|
|
),
|
|
(
|
|
"missing name",
|
|
"---\ndescription: ok\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"missing description",
|
|
"---\nname: my-skill\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"missing opening marker",
|
|
"name: my-skill\ndescription: ok\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"missing closing marker",
|
|
"---\nname: my-skill\ndescription: ok\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"compatibility as nested mapping",
|
|
"---\nname: my-skill\ndescription: ok\ncompatibility:\n python-version: 3.8+\n platforms: [macos, linux]\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"license as nested mapping",
|
|
"---\nname: my-skill\ndescription: ok\nlicense:\n type: MIT\n url: http://example\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"metadata as a list",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata:\n - a\n - b\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
(
|
|
"metadata as a bare scalar",
|
|
"---\nname: my-skill\ndescription: ok\nmetadata: just-a-string\n---\n# Body\n",
|
|
"my-skill",
|
|
),
|
|
]
|
|
|
|
|
|
class FrontmatterLinterTests(unittest.TestCase):
|
|
def test_valid_frontmatter_passes(self) -> None:
|
|
for label, body, dir_name in PASS_CASES:
|
|
with self.subTest(case=label):
|
|
errors = _lint(body, dir_name)
|
|
self.assertEqual(errors, [], f"{label}: expected no errors, got {errors}")
|
|
|
|
def test_invalid_frontmatter_fails(self) -> None:
|
|
for label, body, dir_name in FAIL_CASES:
|
|
with self.subTest(case=label):
|
|
errors = _lint(body, dir_name)
|
|
self.assertTrue(errors, f"{label}: expected at least one error, got none")
|
|
|
|
def test_indented_marker_in_block_scalar_does_not_truncate(self) -> None:
|
|
"""A ``---`` line inside a block scalar must not end the frontmatter.
|
|
|
|
If it did, fields after it (here an invalid ``compatibility``) would
|
|
escape validation entirely.
|
|
"""
|
|
body = (
|
|
"---\n"
|
|
"name: my-skill\n"
|
|
"description: |\n"
|
|
" intro\n"
|
|
" ---\n"
|
|
" more\n"
|
|
"compatibility:\n"
|
|
" nested: bad\n"
|
|
"---\n"
|
|
"# Body\n"
|
|
)
|
|
errors = _lint(body)
|
|
self.assertTrue(
|
|
any("compatibility" in e for e in errors),
|
|
f"expected compatibility to be validated past the inline '---', got {errors}",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|