mirror of
https://github.com/prdlk/jobyssey.git
synced 2026-11-07 23:47:38 +00:00
test(tests): add extensive unit test suite covering commands, skills, and utilities
This commit is contained in:
1 parent
49cc7f65da
commit
1768cd05d6
25 files changed
+4565
No files matched your search
Executable
+173
@@ -0,0 +1,173 @@
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
LINTER_SCRIPT = REPO_ROOT / "tools" / "lint_skills.py"
|
||||
|
||||
|
||||
def run_linter(root: Path) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(root / "tools" / "lint_skills.py")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
class LinterRepoFixture(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.root = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, self.root, ignore_errors=True)
|
||||
|
||||
tools = self.root / "tools"
|
||||
tools.mkdir()
|
||||
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"
|
||||
" 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",
|
||||
)
|
||||
|
||||
command = self.root / ".claude" / "commands" / "setup.md"
|
||||
command.parent.mkdir(parents=True)
|
||||
command.write_text("# /setup - Test setup command\n", encoding="utf-8")
|
||||
|
||||
skill = self.root / ".claude" / "skills" / "example" / "SKILL.md"
|
||||
skill.parent.mkdir(parents=True)
|
||||
skill.write_text(
|
||||
"---\nname: example\ndescription: Example skill\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self.settings = self.root / ".claude" / "settings.json"
|
||||
self.write_settings({"permissions": {"allow": []}})
|
||||
|
||||
def write_settings(self, data):
|
||||
self.settings.write_text(json.dumps(data), encoding="utf-8")
|
||||
|
||||
|
||||
class SettingsShapeTests(LinterRepoFixture):
|
||||
def test_valid_settings_pass(self):
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("lint_skills: OK", result.stdout)
|
||||
|
||||
def test_invalid_json_fails_cleanly(self):
|
||||
self.settings.write_text("{not json", encoding="utf-8")
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn(".claude/settings.json", result.stdout)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_non_object_root_fails_cleanly(self):
|
||||
for data in ([], "settings", 1, None):
|
||||
with self.subTest(data=data):
|
||||
self.write_settings(data)
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("top-level JSON value to be an object", result.stdout)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_non_object_permissions_fails_cleanly(self):
|
||||
for permissions in ([], "permissions", 1, None):
|
||||
with self.subTest(permissions=permissions):
|
||||
self.write_settings({"permissions": permissions})
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("expected permissions to be an object", result.stdout)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_non_list_allow_fails_cleanly(self):
|
||||
for allow in ({}, "Bash(bun run:*)", 1, None):
|
||||
with self.subTest(allow=allow):
|
||||
self.write_settings({"permissions": {"allow": allow}})
|
||||
|
||||
result = run_linter(self.root)
|
||||
|
||||
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