HERMES-GIELIE/tests/test_issue5057_moa_webui_route.py
nesquena-hermes dbfa33a999 feat(moa): route /moa through WebUI via server-resolved MoA config (#5070, #5057)
Adds a dedicated GET /api/commands/moa/resolve endpoint + resolve_moa_config()
that reads the MoA preset/usage server-side. The /moa send-path passes only a
moa_config:true boolean to /api/chat/start; the server re-resolves the real
config itself (never trusts a browser-supplied dict) and threads it per-turn into
the live agent run. Fails closed (409) on gateway-backed sessions.

Replaces the originally-proposed broad 43-command subprocess approach (bounced)
with this narrow, security-reviewed /moa route. Maintainer fixes on top of the
contributor's converged design: moa_config is added to run_conversation only
when not None (older-agent TypeError safety) at the main call AND both auth
self-heal retry sites, and forwarded through the legacy-journal runtime adapter.

Gate: Codex SAFE (after the maintainer fixes) + full suite 10784 pass + the
activity-stream regression gate clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-28 00:23:51 +00:00

129 lines
5.6 KiB
Python

"""Tests for /moa WebUI route: resolve_moa_config and GET /api/commands/moa/resolve."""
import json
import re
import sys
import urllib.request
from pathlib import Path
from types import ModuleType
from typing import Any, cast
import pytest
from tests.conftest import TEST_BASE, requires_agent_modules
def _install_fake_moa_config(monkeypatch, *, default_preset="moa-default", usage_text="Usage: /moa <prompt>"):
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
hermes_cli_pkg.__path__ = []
moa_config = ModuleType("hermes_cli.moa_config")
def normalize_moa_config(cfg):
return {"default_preset": default_preset}
def moa_usage():
return usage_text
moa_config_any = cast(Any, moa_config)
moa_config_any.normalize_moa_config = normalize_moa_config
moa_config_any.moa_usage = moa_usage
monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli_pkg)
monkeypatch.setitem(sys.modules, "hermes_cli.moa_config", moa_config)
def _install_fake_hermes_config(monkeypatch, cfg_data=None):
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
hermes_cli_pkg.__path__ = []
config_mod = ModuleType("hermes_cli.config")
def load_config():
if cfg_data is None:
raise RuntimeError("no config")
return cfg_data
config_mod_any = cast(Any, config_mod)
config_mod_any.load_config = load_config
monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli_pkg)
monkeypatch.setitem(sys.modules, "hermes_cli.config", config_mod)
def test_resolve_moa_config_returns_expected_shape(monkeypatch):
_install_fake_moa_config(monkeypatch, default_preset="moa-fast", usage_text="/moa <prompt> -- run with MoA")
_install_fake_hermes_config(monkeypatch, cfg_data={"moa": {}})
from api.commands import resolve_moa_config
result = resolve_moa_config()
assert result["default_preset"] == "moa-fast"
assert result["preset"] == "moa-fast"
assert result["usage"] == "/moa <prompt> -- run with MoA"
assert "model" not in result
assert "model_provider" not in result
def test_resolve_moa_config_degrades_without_config(monkeypatch):
_install_fake_moa_config(monkeypatch, default_preset="moa-default-cfg")
_install_fake_hermes_config(monkeypatch, cfg_data=None)
from api.commands import resolve_moa_config
result = resolve_moa_config()
assert result["default_preset"] == "moa-default-cfg"
assert result["preset"] == "moa-default-cfg"
def test_resolve_moa_config_raises_when_moa_unavailable(monkeypatch):
monkeypatch.setitem(sys.modules, "hermes_cli.moa_config", None)
from api.commands import resolve_moa_config
with pytest.raises(RuntimeError, match="MoA runtime unavailable"):
resolve_moa_config()
@requires_agent_modules
def test_moa_resolve_endpoint_returns_200():
with urllib.request.urlopen(TEST_BASE + "/api/commands/moa/resolve", timeout=10) as r:
assert r.status == 200
body = json.loads(r.read())
assert "default_preset" in body
assert "preset" in body
assert isinstance(body.get("usage"), str)
assert isinstance(body.get("reference_models"), list)
def test_moa_not_in_agent_commands_webui():
js_path = Path(__file__).resolve().parent.parent / "static" / "messages.js"
source = js_path.read_text(encoding="utf-8")
match = re.search(r"_AGENT_COMMANDS_RUN_ON_WEBUI\s*=\s*new\s+Set\(\[([^\]]+)\]\)", source)
assert match, "_AGENT_COMMANDS_RUN_ON_WEBUI not found in messages.js"
entries = match.group(1)
assert "'moa'" not in entries and '"moa"' not in entries
def test_no_subprocess_in_moa_code_paths():
commands_path = Path(__file__).resolve().parent.parent / "api" / "commands.py"
source = commands_path.read_text(encoding="utf-8")
match = re.search(r"def resolve_moa_config\b.*?(?=\ndef |\Z)", source, re.DOTALL)
assert match, "resolve_moa_config not found in commands.py"
func_body = match.group(0)
assert "process_command" not in func_body
assert "HermesCLI" not in func_body
assert "subprocess" not in func_body
def test_moa_config_is_per_turn_not_persisted():
"""moa_config stays per-turn, but the server re-resolves it instead of
trusting a client-echoed dict."""
streaming_path = Path(__file__).resolve().parent.parent / "api" / "streaming.py"
source = streaming_path.read_text(encoding="utf-8")
# moa_config is threaded into the live agent turn as a per-turn kwarg. It is
# added CONDITIONALLY (only when not None) so a normal send never trips a
# TypeError on an older hermes-agent whose run_conversation() predates the
# kwarg — so accept either the direct kwarg form or the conditional-dict form.
assert (
re.search(r"run_conversation\([\s\S]*?moa_config=moa_config", source)
or re.search(r'if moa_config is not None:[\s\S]*?\["moa_config"\]\s*=\s*moa_config', source)
), "run_conversation must receive moa_config as a per-turn kwarg (directly or conditionally)"
routes_path = Path(__file__).resolve().parent.parent / "api" / "routes.py"
routes_source = routes_path.read_text(encoding="utf-8")
assert re.search(r"if body\.get\(\"moa_config\"\):[\s\S]*?moa_config = resolve_moa_config\(\)", routes_source), \
"chat-start must re-resolve MoA config server-side instead of trusting the browser payload"
assert "MoA override is unavailable on gateway-backed sessions" in routes_source
js_path = Path(__file__).resolve().parent.parent / "static" / "messages.js"
js_source = js_path.read_text(encoding="utf-8")
assert "moa_config:_pendingMoaConfig?true:undefined" in js_source
assert "_pendingMoaConfig=null" in js_source