Files
2026-10-15 08:57:23 +02:00

96 lines
4.1 KiB
Python

"""Tests for scan_scripts.py: header, floor, test presence, network and model-id rules."""
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).resolve().parents[1] / "scan_scripts.py"
GOOD = '# /// script\n# requires-python = ">=3.11"\n# ///\n"""Good."""\nimport json\nprint(json.dumps({}))\n'
OLD_FLOOR = '# /// script\n# requires-python = ">=3.9"\n# ///\nprint(1)\n'
NO_FLOOR = "# /// script\n# dependencies = []\n# ///\nprint(1)\n"
NO_HEADER = '#!/usr/bin/env python3\nimport requests\nMODEL = "claude-opus-4-1"\nprint(requests, MODEL)\n'
BROKEN = '# /// script\n# requires-python = ">=3.11"\n# ///\ndef (:\n'
CUSTOM_IO = '# /// script\n# requires-python = ">=3.11"\n# ///\nfrom pathlib import Path\np = Path("_bmad/custom") / "demo.user.toml"\nprint(p)\n'
def make_skill(root: Path, scripts: dict[str, str], tests: tuple[str, ...] = ()) -> Path:
skill = root / "bmad-demo"
(skill / "scripts" / "tests").mkdir(parents=True)
for name, text in scripts.items():
(skill / "scripts" / name).write_text(text, encoding="utf-8")
for name in tests:
(skill / "scripts" / "tests" / name).write_text("import unittest\n", encoding="utf-8")
return skill
def scan(skill: Path):
res = subprocess.run([sys.executable, str(SCRIPT), str(skill)], capture_output=True, text=True)
return res.returncode, json.loads(res.stdout)
class ScanScriptsTest(unittest.TestCase):
def test_custom_io_fires_on_override_paths(self):
with tempfile.TemporaryDirectory() as tmp:
skill = make_skill(Path(tmp), {"formats.py": CUSTOM_IO}, ("test_formats.py",))
code, data = scan(skill)
self.assertEqual(code, 1)
hits = sorted((f["rule"], f["text"]) for f in data["findings"])
self.assertEqual(hits, [("custom-io", "_bmad/custom"), ("custom-io", "demo.user.toml")])
def test_clean_script(self):
with tempfile.TemporaryDirectory() as tmp:
skill = make_skill(Path(tmp), {"good.py": GOOD}, ("test_good.py",))
code, data = scan(skill)
self.assertEqual(code, 0, data)
self.assertEqual(data["findings"], [])
self.assertEqual(
data["scripts"], [{"path": "scripts/good.py", "has_pep723": True, "floor": ">=3.11", "has_test": True}]
)
def test_rules_fire(self):
with tempfile.TemporaryDirectory() as tmp:
skill = make_skill(
Path(tmp),
{"old.py": OLD_FLOOR, "nofloor.py": NO_FLOOR, "bare.py": NO_HEADER, "broken.py": BROKEN},
("test_old.py", "test_nofloor.py", "test_broken.py"),
)
code, data = scan(skill)
self.assertEqual(code, 1)
found = sorted((f["path"], f["rule"]) for f in data["findings"])
self.assertEqual(
found,
[
("scripts/bare.py", "model-id"),
("scripts/bare.py", "network-call"),
("scripts/bare.py", "pep723-missing"),
("scripts/bare.py", "test-missing"),
("scripts/broken.py", "syntax-error"),
("scripts/nofloor.py", "pep723-floor"),
("scripts/old.py", "pep723-floor"),
],
)
by_path = {s["path"]: s for s in data["scripts"]}
self.assertFalse(by_path["scripts/bare.py"]["has_pep723"])
self.assertEqual(by_path["scripts/old.py"]["floor"], ">=3.9")
self.assertIsNone(by_path["scripts/nofloor.py"]["floor"])
def test_no_scripts_folder_is_clean(self):
with tempfile.TemporaryDirectory() as tmp:
skill = Path(tmp) / "bmad-demo"
skill.mkdir()
code, data = scan(skill)
self.assertEqual(code, 0)
self.assertEqual(data, {"skill": "bmad-demo", "scripts": [], "findings": []})
def test_not_a_directory(self):
res = subprocess.run([sys.executable, str(SCRIPT), "/nonexistent"], capture_output=True, text=True)
self.assertEqual(res.returncode, 2)
if __name__ == "__main__":
unittest.main()