import importlib import importlib.util import os import shutil import sys from pathlib import Path import pytest # Point at the plugin directory _PLUGIN_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "../../.hermes-plugin") ) sys.path.insert(0, _PLUGIN_DIR) BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes" def _load_plugin(): """Re-import plugin module fresh.""" if "__init__" in sys.modules: del sys.modules["__init__"] return importlib.import_module("__init__") def _fire_pre_llm(ctx, **kwargs): hook = ctx._hooks["pre_llm_call"] defaults = { "session_id": "s1", "user_message": "hi", "conversation_history": [], "is_first_turn": False, "model": "test-model", "platform": "cli", } defaults.update(kwargs) return hook(**defaults) class TestPluginRegistration: def test_register_attaches_only_pre_llm_call_hook(self, mock_ctx): plugin = _load_plugin() plugin.register(mock_ctx) assert list(mock_ctx._hooks.keys()) == ["pre_llm_call"] def test_register_registers_every_stock_skill_as_path(self, mock_ctx): plugin = _load_plugin() plugin.register(mock_ctx) # The conftest mock raises on non-Path (mirroring hermes' real # register_skill), so reaching these asserts proves every # registration passed a pathlib.Path. assert "using-superpowers" in mock_ctx._skills assert "brainstorming" in mock_ctx._skills for name, path in mock_ctx._skills.items(): assert isinstance(path, Path) assert path.name == "SKILL.md" assert path.parent.name == name assert path.is_file() def test_registered_skills_match_skill_directories(self, mock_ctx): plugin = _load_plugin() plugin.register(mock_ctx) skills_root = plugin._skills_dir() expected = { entry for entry in os.listdir(skills_root) if os.path.isfile(os.path.join(skills_root, entry, "SKILL.md")) } assert set(mock_ctx._skills.keys()) == expected class TestBootstrapInjection: def test_first_turn_returns_bootstrap_context(self, mock_ctx): plugin = _load_plugin() plugin.register(mock_ctx) result = _fire_pre_llm(mock_ctx, is_first_turn=True) assert isinstance(result, dict) content = result["context"] assert BOOTSTRAP_MARKER in content assert content.startswith("") assert content.rstrip().endswith("") def test_later_turns_return_none(self, mock_ctx): plugin = _load_plugin() plugin.register(mock_ctx) assert _fire_pre_llm(mock_ctx, is_first_turn=False) is None assert _fire_pre_llm(mock_ctx, is_first_turn=None) is None def test_hook_tolerates_future_kwargs(self, mock_ctx): plugin = _load_plugin() plugin.register(mock_ctx) result = _fire_pre_llm( mock_ctx, is_first_turn=True, telemetry_schema_version=3 ) assert BOOTSTRAP_MARKER in result["context"] class TestLayoutResolution: def _stage(self, tmp_path, layout): """Copy the plugin module + a minimal skills tree in the given layout.""" src_skills = Path(_PLUGIN_DIR).parent / "skills" if layout == "clone": plugdir = tmp_path / "superpowers" / ".hermes-plugin" else: # flat: module at the plugin dir root, skills nested inside it plugdir = tmp_path / "superpowers" skills = tmp_path / "superpowers" / "skills" plugdir.mkdir(parents=True, exist_ok=True) shutil.copy(Path(_PLUGIN_DIR) / "__init__.py", plugdir / "__init__.py") for skill in ("using-superpowers", "brainstorming"): shutil.copytree(src_skills / skill, skills / skill) return plugdir def _load_from(self, plugdir): spec = importlib.util.spec_from_file_location( f"hermes_plugin_test_{plugdir.parent.name}_{plugdir.name}", plugdir / "__init__.py", ) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def test_clone_layout_resolves_sibling_skills(self, tmp_path, mock_ctx): # git-clone install: .hermes-plugin/ and skills/ are siblings. plugdir = self._stage(tmp_path, "clone") mod = self._load_from(plugdir) mod.register(mock_ctx) assert "using-superpowers" in mock_ctx._skills def test_flat_layout_resolves_nested_skills(self, tmp_path, mock_ctx): # flattened install: module at the plugin dir root, skills/ inside it. plugdir = self._stage(tmp_path, "flat") mod = self._load_from(plugdir) mod.register(mock_ctx) assert "using-superpowers" in mock_ctx._skills def test_missing_skills_raises_loudly(self, tmp_path, mock_ctx): plugdir = tmp_path / "superpowers" plugdir.mkdir(parents=True) shutil.copy(Path(_PLUGIN_DIR) / "__init__.py", plugdir / "__init__.py") mod = self._load_from(plugdir) with pytest.raises(RuntimeError, match="cannot find the skills"): mod.register(mock_ctx)