Files
skills/tests/test_naming.py
T
Kevin Veen-BirkenbachandClaude Opus 5.5 9e2452454e refactor(skills)!: rename kvb-git-changelog to kvb-release-changelog
A new release category groups what announces a release (changelog,
article, posts); the changelog skill is its first member. The naming
test lists the category and the README organigram places the skill
under it.

BREAKING CHANGE: /kvb-git-changelog is now /kvb-release-changelog;
make install replaces the installed copy, kvb-skill-cleanup removes the
legacy directory.

Evidence: make test green, ruff and markdownlint clean
(/tmp/rename-changelog-make-test-2.log); no reference to the old name
left in the tree; MEDUSA 5 files, 0 findings.

🎯 Fix: 97 %
💥 Break: 3 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:09:58 +00:00

87 lines
3.3 KiB
Python

"""Pin every first-party skill name to kvb-<category>-<name> and the Agent Skills name rules."""
from __future__ import annotations
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills"
README = REPO_ROOT / "README.md"
CATEGORIES = ("think", "create", "code", "ci", "git", "release", "check", "org", "agent", "skill", "coach")
UNCATEGORIZED = frozenset(
{
"kvb-cleanup",
"kvb-coach",
"kvb-dream",
"kvb-help",
"kvb-optimize",
"kvb-transcend",
}
)
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
NAME_LINE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
ORGANIGRAM_EDGE = re.compile(r"^\s*(\w+) --> \w+\[(kvb-[a-z0-9-]+)\]\s*$", re.MULTILINE)
class TestSkillNames(unittest.TestCase):
def setUp(self):
self.names = sorted(p.name for p in SKILLS.iterdir() if p.is_dir())
def test_name_field_matches_the_directory(self):
for name in self.names:
with self.subTest(skill=name):
text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8")
match = NAME_LINE.search(text)
self.assertIsNotNone(match, "SKILL.md has no name: line")
self.assertEqual(match.group(1), name)
def test_names_follow_the_agent_skills_rules(self):
for name in self.names:
with self.subTest(skill=name):
self.assertRegex(name, SPEC)
self.assertLessEqual(len(name), 64)
self.assertNotRegex(name, "anthropic|claude")
def test_names_use_a_listed_category(self):
for name in self.names:
if name in UNCATEGORIZED:
continue
with self.subTest(skill=name):
self.assertRegex(name, SCHEMA)
def test_skill_md_never_names_its_own_slash_command(self):
for name in self.names:
own = re.compile(rf"(?<![\w./~-])/{re.escape(name)}(?![\w/-])")
text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8")
with self.subTest(skill=name):
self.assertEqual(
[n for n, line in enumerate(text.splitlines(), 1) if own.search(line)], [],
f"/{name} is implied by the name; it only costs listing and context tokens",
)
def test_every_category_and_exception_is_in_use(self):
for category in CATEGORIES:
with self.subTest(category=category):
self.assertTrue(
any(n.startswith(f"kvb-{category}-") for n in self.names),
f"category {category} has no skill",
)
self.assertLessEqual(UNCATEGORIZED, set(self.names))
def test_readme_organigram_places_every_skill_under_its_category(self):
text = README.read_text(encoding="utf-8")
section = text.split("## Organigram", 1)[1].split("\n## ", 1)[0]
edges = ORGANIGRAM_EDGE.findall(section)
self.assertEqual(sorted(name for _, name in edges), self.names)
for parent, name in edges:
with self.subTest(skill=name):
expected = "none" if name in UNCATEGORIZED else name.split("-")[1]
self.assertEqual(parent, expected)
if __name__ == "__main__":
unittest.main()