* fix: stabilize validation and tests on Windows * test: add Windows smoke coverage for skill activation * refactor: make setup_web script CommonJS * fix: repair aegisops-ai frontmatter * docs: add when-to-use guidance to core skills * docs: add when-to-use guidance to Apify skills * docs: add when-to-use guidance to Google and Expo skills * docs: add when-to-use guidance to Makepad skills * docs: add when-to-use guidance to git workflow skills * docs: add when-to-use guidance to fp-ts skills * docs: add when-to-use guidance to Three.js skills * docs: add when-to-use guidance to n8n skills * docs: add when-to-use guidance to health analysis skills * docs: add when-to-use guidance to writing and review skills * meta: sync generated catalog metadata * docs: add when-to-use guidance to Robius skills * docs: add when-to-use guidance to review and workflow skills * docs: add when-to-use guidance to science and data skills * docs: add when-to-use guidance to tooling and automation skills * docs: add when-to-use guidance to remaining skills * fix: gate bundle helper execution in Windows activation * chore: drop generated artifacts from contributor PR * docs(maintenance): Record PR 457 sweep Document the open issue triage, PR supersedence decision, local verification, and source-only cleanup that prepared PR #457 for re-running CI. --------- Co-authored-by: sickn33 <sickn33@users.noreply.github.com>
153 lines
6.5 KiB
Python
153 lines
6.5 KiB
Python
import importlib.util
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
|
TOOLS_SCRIPTS = REPO_ROOT / "tools" / "scripts"
|
|
if str(TOOLS_SCRIPTS) not in sys.path:
|
|
sys.path.insert(0, str(TOOLS_SCRIPTS))
|
|
|
|
|
|
def load_module(module_path: pathlib.Path, module_name: str):
|
|
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
plugin_compatibility = load_module(
|
|
TOOLS_SCRIPTS / "plugin_compatibility.py",
|
|
"plugin_compatibility_test",
|
|
)
|
|
|
|
|
|
class PluginCompatibilityTests(unittest.TestCase):
|
|
def _write_skill(self, skills_dir: pathlib.Path, skill_id: str, content: str) -> pathlib.Path:
|
|
skill_dir = skills_dir / skill_id
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
|
|
return skill_dir
|
|
|
|
def test_absolute_host_paths_block_both_targets(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skills_dir = pathlib.Path(temp_dir) / "skills"
|
|
self._write_skill(
|
|
skills_dir,
|
|
"absolute-path-skill",
|
|
"---\nname: absolute-path-skill\ndescription: Example\n---\nUse /Users/tester/private/file\n",
|
|
)
|
|
|
|
report = plugin_compatibility.build_report(skills_dir)
|
|
entry = report["skills"][0]
|
|
self.assertEqual(entry["targets"]["codex"], "blocked")
|
|
self.assertEqual(entry["targets"]["claude"], "blocked")
|
|
self.assertIn("absolute_host_path", entry["reasons"])
|
|
|
|
def test_claude_home_paths_only_block_codex(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skills_dir = pathlib.Path(temp_dir) / "skills"
|
|
self._write_skill(
|
|
skills_dir,
|
|
"claude-home-skill",
|
|
"---\nname: claude-home-skill\ndescription: Example\n---\nRead ~/.claude/projects/cache\n",
|
|
)
|
|
|
|
report = plugin_compatibility.build_report(skills_dir)
|
|
entry = report["skills"][0]
|
|
self.assertEqual(entry["targets"]["codex"], "blocked")
|
|
self.assertEqual(entry["targets"]["claude"], "supported")
|
|
self.assertIn("target_specific_home_path", entry["blocked_reasons"]["codex"])
|
|
|
|
def test_runtime_dependency_requires_explicit_manual_setup(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skills_dir = pathlib.Path(temp_dir) / "skills"
|
|
skill_dir = self._write_skill(
|
|
skills_dir,
|
|
"dependency-skill",
|
|
"---\nname: dependency-skill\ndescription: Example\n---\nbody\n",
|
|
)
|
|
(skill_dir / "requirements.txt").write_text("requests\n", encoding="utf-8")
|
|
|
|
report = plugin_compatibility.build_report(skills_dir)
|
|
entry = report["skills"][0]
|
|
self.assertEqual(entry["targets"]["codex"], "blocked")
|
|
self.assertEqual(entry["targets"]["claude"], "blocked")
|
|
self.assertIn("undeclared_runtime_dependency", entry["reasons"])
|
|
|
|
def test_relative_links_cannot_escape_skill_directory(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skills_dir = pathlib.Path(temp_dir) / "skills"
|
|
self._write_skill(
|
|
skills_dir,
|
|
"escaping-link-skill",
|
|
(
|
|
"---\n"
|
|
"name: escaping-link-skill\n"
|
|
"description: Example\n"
|
|
"---\n"
|
|
"Read [secret](../../outside/secret.txt)\n"
|
|
),
|
|
)
|
|
outside_dir = pathlib.Path(temp_dir) / "outside"
|
|
outside_dir.mkdir(parents=True, exist_ok=True)
|
|
(outside_dir / "secret.txt").write_text("secret", encoding="utf-8")
|
|
|
|
report = plugin_compatibility.build_report(skills_dir)
|
|
entry = report["skills"][0]
|
|
self.assertEqual(entry["targets"]["codex"], "blocked")
|
|
self.assertEqual(entry["targets"]["claude"], "blocked")
|
|
self.assertIn("escaped_local_reference", entry["reasons"])
|
|
|
|
def test_manual_setup_metadata_can_make_runtime_skill_supported(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skills_dir = pathlib.Path(temp_dir) / "skills"
|
|
skill_dir = self._write_skill(
|
|
skills_dir,
|
|
"manual-setup-skill",
|
|
(
|
|
"---\n"
|
|
"name: manual-setup-skill\n"
|
|
"description: Example\n"
|
|
"plugin:\n"
|
|
" setup:\n"
|
|
" type: manual\n"
|
|
" summary: Run the setup command once.\n"
|
|
" docs: SKILL.md\n"
|
|
"---\n"
|
|
"body\n"
|
|
),
|
|
)
|
|
(skill_dir / "package.json").write_text(json.dumps({"name": "manual-setup-skill"}), encoding="utf-8")
|
|
|
|
report = plugin_compatibility.build_report(skills_dir)
|
|
entry = report["skills"][0]
|
|
self.assertEqual(entry["targets"]["codex"], "supported")
|
|
self.assertEqual(entry["targets"]["claude"], "supported")
|
|
self.assertEqual(entry["setup"]["type"], "manual")
|
|
|
|
def test_repo_sample_skills_have_expected_status(self):
|
|
report = plugin_compatibility.build_report(REPO_ROOT / "skills")
|
|
entries = plugin_compatibility.compatibility_by_skill_id(report)
|
|
|
|
for skill_id in ("molykit", "claude-code-expert"):
|
|
self.assertEqual(entries[skill_id]["targets"]["codex"], "blocked")
|
|
self.assertEqual(entries[skill_id]["targets"]["claude"], "blocked")
|
|
self.assertIn("absolute_host_path", entries[skill_id]["reasons"])
|
|
|
|
self.assertEqual(entries["project-skill-audit"]["targets"]["codex"], "supported")
|
|
self.assertEqual(entries["project-skill-audit"]["targets"]["claude"], "blocked")
|
|
self.assertNotIn("absolute_host_path", entries["project-skill-audit"]["reasons"])
|
|
self.assertIn("target_specific_home_path", entries["project-skill-audit"]["blocked_reasons"]["claude"])
|
|
|
|
self.assertEqual(entries["playwright-skill"]["targets"]["codex"], "supported")
|
|
self.assertEqual(entries["playwright-skill"]["targets"]["claude"], "supported")
|
|
self.assertEqual(entries["playwright-skill"]["setup"]["type"], "manual")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|