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