mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
* CI: add skills Python checks job * Chore: add Python lint and test pre-commit hooks * Tests: fix skill-creator package test import path * Chore: add Python tooling config for skills scripts * CI: run all skills Python tests * Chore: run all skills Python tests in pre-commit * Chore: enable pytest discovery for all skills tests * Changelog: note skills Python quality harness
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Regression tests for skill packaging security behavior.
|
|
"""
|
|
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest import TestCase, main
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
if str(SCRIPT_DIR) not in sys.path:
|
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
|
|
|
|
fake_quick_validate = types.ModuleType("quick_validate")
|
|
fake_quick_validate.validate_skill = lambda _path: (True, "Skill is valid!")
|
|
sys.modules["quick_validate"] = fake_quick_validate
|
|
|
|
from package_skill import package_skill
|
|
|
|
|
|
class TestPackageSkillSecurity(TestCase):
|
|
def setUp(self):
|
|
self.temp_dir = Path(tempfile.mkdtemp(prefix="test_skill_"))
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
|
|
if self.temp_dir.exists():
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def create_skill(self, name="test-skill"):
|
|
skill_dir = self.temp_dir / name
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
(skill_dir / "SKILL.md").write_text("---\nname: test-skill\ndescription: test\n---\n")
|
|
(skill_dir / "script.py").write_text("print('ok')\n")
|
|
return skill_dir
|
|
|
|
def test_packages_normal_files(self):
|
|
skill_dir = self.create_skill("normal-skill")
|
|
out_dir = self.temp_dir / "out"
|
|
out_dir.mkdir()
|
|
|
|
result = package_skill(str(skill_dir), str(out_dir))
|
|
|
|
self.assertIsNotNone(result)
|
|
skill_file = out_dir / "normal-skill.skill"
|
|
self.assertTrue(skill_file.exists())
|
|
with zipfile.ZipFile(skill_file, "r") as archive:
|
|
names = set(archive.namelist())
|
|
self.assertIn("normal-skill/SKILL.md", names)
|
|
self.assertIn("normal-skill/script.py", names)
|
|
|
|
def test_rejects_symlink_to_external_file(self):
|
|
skill_dir = self.create_skill("symlink-file-skill")
|
|
outside = self.temp_dir / "outside-secret.txt"
|
|
outside.write_text("super-secret\n")
|
|
link = skill_dir / "loot.txt"
|
|
out_dir = self.temp_dir / "out"
|
|
out_dir.mkdir()
|
|
|
|
try:
|
|
link.symlink_to(outside)
|
|
except (OSError, NotImplementedError):
|
|
self.skipTest("symlink unsupported on this platform")
|
|
|
|
result = package_skill(str(skill_dir), str(out_dir))
|
|
self.assertIsNone(result)
|
|
|
|
def test_rejects_symlink_directory(self):
|
|
skill_dir = self.create_skill("symlink-dir-skill")
|
|
outside_dir = self.temp_dir / "outside"
|
|
outside_dir.mkdir()
|
|
(outside_dir / "secret.txt").write_text("secret\n")
|
|
link = skill_dir / "docs"
|
|
out_dir = self.temp_dir / "out"
|
|
out_dir.mkdir()
|
|
|
|
try:
|
|
link.symlink_to(outside_dir, target_is_directory=True)
|
|
except (OSError, NotImplementedError):
|
|
self.skipTest("symlink unsupported on this platform")
|
|
|
|
result = package_skill(str(skill_dir), str(out_dir))
|
|
self.assertIsNone(result)
|
|
|
|
def test_allows_nested_regular_files(self):
|
|
skill_dir = self.create_skill("nested-skill")
|
|
nested = skill_dir / "lib" / "helpers"
|
|
nested.mkdir(parents=True, exist_ok=True)
|
|
(nested / "util.py").write_text("def run():\n return 1\n")
|
|
out_dir = self.temp_dir / "out"
|
|
out_dir.mkdir()
|
|
|
|
result = package_skill(str(skill_dir), str(out_dir))
|
|
|
|
self.assertIsNotNone(result)
|
|
skill_file = out_dir / "nested-skill.skill"
|
|
with zipfile.ZipFile(skill_file, "r") as archive:
|
|
names = set(archive.namelist())
|
|
self.assertIn("nested-skill/lib/helpers/util.py", names)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|