Files
hermes-agent/tests/hermes_cli/test_plugin_api_hot_mount.py
Teknium 8620effe53 fix(dashboard): hot-mount plugin APIs without redirect, ahead of the SPA catch-all
Rework of the hot-mount fallback so a plugin installed/enabled after `hermes serve`
started actually serves on the next request:

- Mount bookkeeping is read from the live route table instead of a module-level
  `_plugin_api_mounted_names` set. A test that restored its `app.router.routes`
  snapshot left the set claiming "example" was mounted, so the next test's
  remount was skipped and its probe 404'd
  (TestPluginAPISecretScopeProductionMount after TestPluginAPIAuth).
- Hot-mounted routes are spliced in just ahead of the fallback route. Appending
  them put them behind the SPA catch-all `/{full_path:path}`, so every GET kept
  missing them.
- The fallback is a raw ASGI app that re-dispatches the same request after a
  successful mount instead of answering 307: Desktop's API transport never
  follows a 3xx and failed the first poll.
- Code-health BLE001: the enable-set read in the hot path no longer swallows
  errors; the plugin-import boundary keeps its catch-all with exc_info and a
  reviewed waiver.
2026-10-07 16:33:07 -07:00

66 lines
2.4 KiB
Python

"""A plugin installed and enabled after the web server imported must serve its backend API on the
first request — no restart — and stop serving once disabled."""
import json
import pytest
from hermes_cli import web_server
@pytest.fixture
def client(_isolate_hermes_home):
from starlette.testclient import TestClient
from hermes_cli.web_server import _SESSION_HEADER_NAME, _SESSION_TOKEN, app
original_routes = list(app.router.routes)
web_server._dashboard_plugins_cache = None
c = TestClient(app)
c.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN
try:
yield c
finally:
app.router.routes[:] = original_routes
web_server._dashboard_plugins_cache = None
def _install_late_plugin(home, *, enabled: bool) -> None:
dash = home / "plugins" / "late-probe" / "dashboard"
dash.mkdir(parents=True)
(dash / "manifest.json").write_text(json.dumps({"name": "late-probe", "api": "plugin_api.py"}))
(dash / "plugin_api.py").write_text(
"from fastapi import APIRouter\nrouter = APIRouter()\n"
"@router.get('/ping')\nasync def ping():\n return {'pong': True}\n"
)
from hermes_cli.config import load_config, save_config
cfg = load_config()
cfg.setdefault("plugins", {})["enabled"] = ["late-probe"] if enabled else []
save_config(cfg)
def test_plugin_enabled_after_startup_serves_without_restart(client):
from hermes_constants import get_hermes_home
assert client.get("/api/plugins/late-probe/ping").status_code == 404
_install_late_plugin(get_hermes_home(), enabled=True)
first = client.get("/api/plugins/late-probe/ping", follow_redirects=False)
assert (first.status_code, first.json()) == (200, {"pong": True})
assert client.get("/api/plugins/late-probe/missing").status_code == 404
from hermes_cli.config import load_config, save_config
cfg = load_config()
cfg["plugins"]["enabled"], cfg["plugins"]["disabled"] = [], ["late-probe"]
save_config(cfg)
assert client.get("/api/plugins/late-probe/ping").status_code == 404
def test_installed_but_not_enabled_plugin_is_never_imported(client):
from hermes_constants import get_hermes_home
_install_late_plugin(get_hermes_home(), enabled=False)
(get_hermes_home() / "plugins" / "late-probe" / "dashboard" / "plugin_api.py").write_text(
"raise SystemExit('imported a not-enabled plugin')\n")
assert client.get("/api/plugins/late-probe/ping").status_code == 404