"""Integration test — plugin against a real hermes-webui fork checkout. Skipped when hermes-webui isn't adjacent to the plugin repo (CI without the fork mounted). Validates the loader hook actually loads + registers our 7 public-API methods + reports the audio processor on startup. Run via: `pytest tests/integration -v` — make smoke uses the same harness. """ import sys from pathlib import Path import pytest PLUGIN_REPO = Path(__file__).resolve().parents[2] FORK_REPO = PLUGIN_REPO.parent / "hermes-webui" @pytest.fixture(scope="module") def loader(): """Import the fork's loader. Skip cleanly when fork not adjacent.""" if not (FORK_REPO / "api" / "svrnty_plugin_loader.py").is_file(): pytest.skip(f"hermes-webui fork not at {FORK_REPO}; integration env required") sys.path.insert(0, str(FORK_REPO)) try: from api import svrnty_plugin_loader as loader_mod except ImportError as e: pytest.skip(f"loader import failed: {e}") return loader_mod def test_loader_exposes_seven_method_contract(loader): """Public API surface must be exactly 7 methods (per protocol §5.1).""" api = loader._PluginAPI() methods = {m for m in dir(api) if not m.startswith("_")} expected = { "register_route", "register_static", "inject_script", "inject_stylesheet", "config_get", "logger", "register_audio_attachment_processor", } assert methods == expected, ( f"loader API drift: missing={expected - methods} extra={methods - expected}. " "Adding/removing methods requires a protocol PRD amendment." ) def test_loader_register_wires_our_plugin(loader, monkeypatch): """End-to-end: env var → import this plugin → register() fires plugin routes + processor.""" monkeypatch.setenv("HERMES_WEBUI_PYTHON_PLUGIN", "svrnty_hermes_webui_plugin") # Reset loader idempotency guard so we can re-run in-process loader._LOADED = False loader._ROUTES.clear() loader._STATIC.clear() loader._SCRIPTS.clear() loader._STYLESHEETS.clear() loader._AUDIO_PROCESSORS.clear() sys.path.insert(0, str(PLUGIN_REPO)) loader.load_plugin() # Core routes registered, including the /umbrella graph API pair. assert ("POST", "/api/transcribe") in loader._ROUTES assert ("GET", "/api/vault/status") in loader._ROUTES assert ("GET", "/api/canvas/status") in loader._ROUTES assert ("GET", "/api/canvas/tools") in loader._ROUTES assert ("GET", "/api/canvas/proxy") in loader._ROUTES assert ("POST", "/api/canvas/proxy") in loader._ROUTES assert ("PUT", "/api/canvas/proxy") in loader._ROUTES assert ("POST", "/api/canvas/command") in loader._ROUTES assert ("GET", "/api/canvas/design-context") in loader._ROUTES assert ("GET", "/api/canvas/events") in loader._ROUTES assert ("GET", "/api/umbrella") in loader._ROUTES assert ("GET", "/api/umbrella/doc") in loader._ROUTES assert ("GET", "/api/cortex-os/runtime-health") in loader._ROUTES # Static + injected URLs assert "svrnty" in loader._STATIC assert "/plugins/svrnty/app.css" in loader._STYLESHEETS assert "/plugins/svrnty/app.js" in loader._SCRIPTS assert "/plugins/svrnty/canvas.css" in loader._STYLESHEETS assert "/plugins/svrnty/canvas.js" in loader._SCRIPTS assert "/plugins/svrnty/umbrella_inline.css" in loader._STYLESHEETS assert "/plugins/svrnty/umbrella_inline.js" in loader._SCRIPTS assert "/plugins/svrnty/cortex-os/runtime-health/runtime_health.css" in loader._STYLESHEETS assert "/plugins/svrnty/cortex-os/runtime-health/runtime_health.js" in loader._SCRIPTS # Audio processor for voice-message transcription assert len(loader._AUDIO_PROCESSORS) == 1 def test_loader_noop_when_env_unset(loader, monkeypatch): """No env var = no plugin loaded. Upstream behavior fully preserved.""" monkeypatch.delenv("HERMES_WEBUI_PYTHON_PLUGIN", raising=False) loader._LOADED = False loader._ROUTES.clear() loader.load_plugin() assert loader._ROUTES == {}, "plugin must NOT load when env var unset"