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.
66 lines
2.4 KiB
Python
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
|