96 lines
4.1 KiB
Python
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()
|