mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
test(lint-skills): cover check_skill and check_command, not just settings
The linter's main job - frontmatter keys, allowed-tools targets, the command title rule - had zero assertions; deleting the missing- allowed-tools error left the suite green. The fixture's yaml stub now parses the flat frontmatter the fixtures write instead of returning a canned mapping, and four new cases pin both check functions. Mutation-verified against the real linter. Review finding F23 (2026-08-19). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c20458d768
commit
9a074b262d
@@ -15,6 +15,12 @@ per-file diff commands.
|
||||
|
||||
### Added
|
||||
|
||||
- **Tests for `lint_skills.py`'s skill and command checks** - only `check_settings()`
|
||||
had coverage; the linter's main job (frontmatter keys, `allowed-tools` targets
|
||||
existing, the `# /<name>` command title rule) was unasserted, so deleting the
|
||||
missing-allowed-tools error survived the whole suite. Four new cases in
|
||||
`tests/test_lint_skills.py`, with the fixture's yaml stub upgraded to parse the real
|
||||
frontmatter. Mutation-verified.
|
||||
- **Discriminating tests for `robots_check`'s tie-break and browser-UA fallback** - the
|
||||
existing tie test put Disallow first, the one ordering that cannot detect deletion of
|
||||
the tie-break clause; and the browser-readback recovery that `09-web-research.md`
|
||||
|
||||
@@ -29,11 +29,19 @@ class LinterRepoFixture(unittest.TestCase):
|
||||
shutil.copy(LINTER_SCRIPT, tools / "lint_skills.py")
|
||||
# The Python-test CI job does not install PyYAML; the separate lint job
|
||||
# does. These settings-focused tests only need a valid frontmatter map.
|
||||
# The stub parses simple "key: value" lines, enough for the flat
|
||||
# frontmatter these fixtures write, so the checks under test see the
|
||||
# actual file content instead of a canned mapping.
|
||||
(tools / "yaml.py").write_text(
|
||||
"class YAMLError(Exception):\n"
|
||||
" pass\n\n"
|
||||
"def safe_load(_text):\n"
|
||||
" return {'name': 'example', 'description': 'Example skill'}\n",
|
||||
"def safe_load(text):\n"
|
||||
" result = {}\n"
|
||||
" for line in (text or '').splitlines():\n"
|
||||
" if ':' in line:\n"
|
||||
" key, _, value = line.partition(':')\n"
|
||||
" result[key.strip()] = value.strip()\n"
|
||||
" return result\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
@@ -103,5 +111,63 @@ class SettingsShapeTests(LinterRepoFixture):
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("expected permissions.allow to be a list", result.stdout)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
class SkillAndCommandCheckTests(LinterRepoFixture):
|
||||
"""check_skill()/check_command() are the linter's main job and were
|
||||
previously untested - only check_settings() had coverage, so deleting
|
||||
e.g. the missing-allowed-tools error survived the whole suite (review
|
||||
finding F23, 2026-08-19)."""
|
||||
|
||||
def write_skill(self, frontmatter: str):
|
||||
skill = self.root / ".claude" / "skills" / "example" / "SKILL.md"
|
||||
skill.write_text(frontmatter, encoding="utf-8")
|
||||
|
||||
def test_allowed_tools_referencing_a_missing_file_fails(self):
|
||||
self.write_skill(
|
||||
"---\n"
|
||||
"name: example\n"
|
||||
"description: Example skill\n"
|
||||
"allowed-tools: Bash(bun run .claude/skills/example/DOES_NOT_EXIST.ts *)\n"
|
||||
"---\n"
|
||||
)
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
||||
self.assertIn("allowed-tools references a missing file", result.stdout)
|
||||
self.assertIn("DOES_NOT_EXIST.ts", result.stdout)
|
||||
|
||||
def test_allowed_tools_referencing_an_existing_file_passes(self):
|
||||
target = self.root / ".claude" / "skills" / "example" / "cli.ts"
|
||||
target.write_text("// present\n", encoding="utf-8")
|
||||
self.write_skill(
|
||||
"---\n"
|
||||
"name: example\n"
|
||||
"description: Example skill\n"
|
||||
"allowed-tools: Bash(bun run .claude/skills/example/cli.ts *)\n"
|
||||
"---\n"
|
||||
)
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_frontmatter_missing_description_fails(self):
|
||||
self.write_skill("---\nname: example\ndescription:\n---\n")
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("missing required key 'description'", result.stdout)
|
||||
|
||||
def test_command_without_slash_title_fails(self):
|
||||
command = self.root / ".claude" / "commands" / "setup.md"
|
||||
command.write_text("# setup - missing the slash\n", encoding="utf-8")
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("must start with a '# /<name>' title", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user