Files
skills/tests/test_skill_cleanup.py
T
Kevin Veen-BirkenbachandClaude Opus 5.5 c47393a73b feat(kvb-skill-cleanup): delete installed skills that renames and removals left behind
legacy.py reads the skills repository's git history (renames and deletions of skills/*/SKILL.md), follows rename chains to the current skill, and classifies every installed skill directory as current (shipped or locked), project (tracked in the repository that holds the root), legacy (with its successor), removed, or unknown. kvb-skill-cleanup scans ~/.claude/skills, ~/.agents/skills, ~/.codex/skills and the projects' .claude and .agents skills, asks the operator, and deletes by re-running the installer, by rm -r where the sandbox allows, or through kvb-org-operator; a rescan verifies. tests/test_skill_cleanup.py covers a two-step rename chain, a deletion, a locked, a project and an unknown skill, and invalid input; a broken one-step resolver fails it. Operator order 2026-11-03; four descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00

121 lines
4.4 KiB
Python

"""Exercise legacy.py: renames chained through history, deletions, project and unknown skills."""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-skill-cleanup" / "scripts" / "legacy.py"
@unittest.skipUnless(shutil.which("git"), "git not installed")
class TestLegacy(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
base = Path(self.tmp.name)
self.env = {
"PATH": "/usr/bin:/bin",
"HOME": str(base),
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@t",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@t",
}
self.repo = base / "skills-repo"
self._skill(self.repo / "skills" / "old")
(self.repo / "skills-lock.json").write_text(
json.dumps({"skills": {"caveman": {}}}), encoding="utf-8"
)
self._git(self.repo, "init", "-q", "-b", "main")
self._commit(self.repo, "add old")
self._git(self.repo, "mv", "skills/old", "skills/mid")
self._commit(self.repo, "rename old to mid")
self._git(self.repo, "mv", "skills/mid", "skills/kvb-x-new")
self._commit(self.repo, "rename mid to kvb-x-new")
self._skill(self.repo / "skills" / "gone")
self._commit(self.repo, "add gone")
self._git(self.repo, "rm", "-q", "-r", "skills/gone")
self._commit(self.repo, "remove gone")
self.home = base / "home" / ".claude" / "skills"
for name in ("old", "mid", "gone", "kvb-x-new", "caveman", "mine"):
self._skill(self.home / name)
(self.home / "notaskill").mkdir()
self.project = base / "project"
self._skill(self.project / "skills" / "p-own")
self._git(self.project, "init", "-q", "-b", "main")
self._commit(self.project, "own skill")
self.installed = self.project / ".claude" / "skills"
for name in ("p-own", "old"):
self._skill(self.installed / name)
def _skill(self, path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
(path / "SKILL.md").write_text(
f"---\nname: {path.name}\ndescription: probe\n---\n\nBody of the probe skill.\n",
encoding="utf-8",
)
def _git(self, cwd: Path, *args: str) -> None:
subprocess.run(["git", *args], check=True, capture_output=True, env=self.env, cwd=cwd)
def _commit(self, cwd: Path, message: str) -> None:
self._git(cwd, "add", "-A")
self._git(cwd, "commit", "-q", "-m", message)
def _run(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
check=False,
capture_output=True,
text=True,
env=self.env,
)
def test_every_installed_skill_gets_its_verdict(self):
result = self._run(
"--skills-repo", str(self.repo),
"--root", str(self.home),
"--root", str(self.installed),
"--json",
)
self.assertEqual(result.returncode, 0, result.stderr)
home, project = str(self.home), str(self.installed)
rows = sorted(
(str(Path(row["path"]).parent), Path(row["path"]).name, row["verdict"], row["successor"])
for row in json.loads(result.stdout)
)
self.assertEqual(
rows,
sorted(
[
(home, "caveman", "current", None),
(home, "gone", "removed", None),
(home, "kvb-x-new", "current", None),
(home, "mid", "legacy", "kvb-x-new"),
(home, "mine", "unknown", None),
(home, "old", "legacy", "kvb-x-new"),
(project, "old", "legacy", "kvb-x-new"),
(project, "p-own", "project", None),
]
),
)
def test_invalid_input_exits_2(self):
missing = self._run("--skills-repo", str(self.repo), "--root", str(self.home / "nope"))
self.assertEqual(missing.returncode, 2)
no_repo = self._run("--skills-repo", str(self.home), "--root", str(self.home))
self.assertEqual(no_repo.returncode, 2)
if __name__ == "__main__":
unittest.main()