#!/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 ``
/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()