mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 16:31:56 +08:00
feat(gateway): refine Platform._missing_ and platform-connected dispatch Restricts plugin-name acceptance to bundled plugin scan + registry (no arbitrary string -> enum-pollution), pulls per-platform connectivity checks into a _PLATFORM_CONNECTED_CHECKERS lambda map with a clean _is_platform_connected method, and adds tests covering the checker map, plugin platform interface, and IRC setup wizard.
310 lines
12 KiB
Python
310 lines
12 KiB
Python
"""Tests for IRC gateway configuration via `hermes setup gateway` UI.
|
|
|
|
Covers the full plugin-platform discovery → status → configure flow so that
|
|
a fresh Hermes install (no state, no env vars) can set up IRC through the
|
|
interactive setup menus.
|
|
"""
|
|
|
|
import os
|
|
import pytest
|
|
|
|
from gateway.platform_registry import PlatformEntry, platform_registry
|
|
|
|
|
|
def _register_irc_platform(**overrides):
|
|
"""Manually register the IRC platform entry as if discover_plugins() found it.
|
|
|
|
Tests run outside the normal plugin-discovery path, so we inject the entry
|
|
directly into the singleton registry and yield its dict shape.
|
|
"""
|
|
needs_enable = overrides.pop("needs_enable", False)
|
|
defaults = dict(
|
|
name="irc",
|
|
label="IRC",
|
|
adapter_factory=lambda cfg: None,
|
|
check_fn=lambda: bool(os.getenv("IRC_SERVER", "") and os.getenv("IRC_CHANNEL", "")),
|
|
validate_config=None,
|
|
required_env=["IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"],
|
|
install_hint="No extra packages needed (stdlib only)",
|
|
setup_fn=lambda: None,
|
|
source="plugin",
|
|
plugin_name="irc_platform",
|
|
allowed_users_env="IRC_ALLOWED_USERS",
|
|
allow_all_env="IRC_ALLOW_ALL_USERS",
|
|
max_message_length=450,
|
|
pii_safe=False,
|
|
emoji="💬",
|
|
allow_update_command=True,
|
|
platform_hint="You are chatting via IRC.",
|
|
)
|
|
defaults.update(overrides)
|
|
entry = PlatformEntry(**defaults)
|
|
platform_registry.register(entry)
|
|
return {
|
|
"key": entry.name,
|
|
"label": entry.label,
|
|
"emoji": entry.emoji,
|
|
"token_var": entry.required_env[0] if entry.required_env else "",
|
|
"install_hint": entry.install_hint,
|
|
"_registry_entry": entry,
|
|
"needs_enable": needs_enable,
|
|
}
|
|
|
|
|
|
def _unregister_irc_platform():
|
|
platform_registry.unregister("irc")
|
|
|
|
|
|
# ── Fresh-install discovery ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestIRCFreshInstallDiscovery:
|
|
"""IRC appears in the setup menu on a brand-new Hermes install."""
|
|
|
|
def test_irc_appears_in_all_platforms(self, monkeypatch):
|
|
"""When the IRC plugin is registered, _all_platforms() surfaces it."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
_register_irc_platform()
|
|
try:
|
|
# Ensure no stale env vars leak in
|
|
for key in ("IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
platforms = gateway_mod._all_platforms()
|
|
keys = {p["key"] for p in platforms}
|
|
assert "irc" in keys
|
|
|
|
irc_plat = next(p for p in platforms if p["key"] == "irc")
|
|
assert irc_plat["label"] == "IRC"
|
|
assert irc_plat["emoji"] == "💬"
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
def test_irc_status_not_configured_when_fresh(self, monkeypatch):
|
|
"""On a fresh install with no env vars, IRC shows 'not configured'."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
plat = _register_irc_platform()
|
|
try:
|
|
for key in ("IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
status = gateway_mod._platform_status(plat)
|
|
assert status == "not configured"
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
def test_irc_status_configured_when_env_set(self, monkeypatch):
|
|
"""After the user sets IRC_SERVER and IRC_CHANNEL, status is 'configured'."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
plat = _register_irc_platform()
|
|
try:
|
|
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
|
|
monkeypatch.setenv("IRC_CHANNEL", "#hermes")
|
|
monkeypatch.setenv("IRC_NICKNAME", "hermes-bot")
|
|
|
|
status = gateway_mod._platform_status(plat)
|
|
assert status == "configured"
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
def test_irc_status_partial_when_only_server_set(self, monkeypatch):
|
|
"""If only IRC_SERVER is set, the platform is still not configured."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
plat = _register_irc_platform()
|
|
try:
|
|
monkeypatch.delenv("IRC_CHANNEL", raising=False)
|
|
monkeypatch.delenv("IRC_NICKNAME", raising=False)
|
|
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
|
|
|
|
status = gateway_mod._platform_status(plat)
|
|
assert status == "not configured"
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
|
|
# ── Plugin-disabled flow ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestIRCPluginDisabledFlow:
|
|
"""When the IRC plugin is disabled, setup offers to enable it."""
|
|
|
|
def test_disabled_plugin_shows_enable_prompt(self, monkeypatch):
|
|
"""A disabled plugin platform surfaces 'plugin disabled — select to enable'."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
plat = _register_irc_platform(needs_enable=True)
|
|
try:
|
|
for key in ("IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
status = gateway_mod._platform_status(plat)
|
|
assert "plugin disabled" in status.lower()
|
|
assert "select to enable" in status.lower()
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
def test_disabled_but_already_configured_shows_configured(self, monkeypatch):
|
|
"""If the plugin is disabled but env vars are already present, show 'configured'."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
plat = _register_irc_platform(needs_enable=True)
|
|
try:
|
|
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
|
|
monkeypatch.setenv("IRC_CHANNEL", "#hermes")
|
|
|
|
status = gateway_mod._platform_status(plat)
|
|
assert status == "configured"
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
|
|
# ── Interactive setup dispatch ──────────────────────────────────────────────
|
|
|
|
|
|
class TestIRCInteractiveSetup:
|
|
"""The setup UI dispatches to IRC's interactive_setup() correctly."""
|
|
|
|
def test_configure_platform_dispatches_to_irc_setup_fn(self, monkeypatch, capsys):
|
|
"""_configure_platform() calls the IRC plugin's setup_fn when selected."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
calls = []
|
|
|
|
def fake_setup():
|
|
calls.append("setup_called")
|
|
print("IRC setup complete!")
|
|
|
|
plat = _register_irc_platform(setup_fn=fake_setup)
|
|
try:
|
|
gateway_mod._configure_platform(plat)
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
assert "setup_called" in calls
|
|
out = capsys.readouterr().out
|
|
assert "IRC setup complete!" in out
|
|
|
|
def test_configure_platform_enables_disabled_plugin_first(self, monkeypatch, capsys, tmp_path):
|
|
"""If the plugin is disabled, _configure_platform enables it before running setup."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
from hermes_cli.config import save_config, load_config
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
# Ensure plugins.enabled exists but does NOT include irc_platform
|
|
cfg = load_config()
|
|
cfg.setdefault("plugins", {})["enabled"] = ["some_other_plugin"]
|
|
save_config(cfg)
|
|
|
|
calls = []
|
|
|
|
def fake_setup():
|
|
calls.append("setup_called")
|
|
|
|
plat = _register_irc_platform(setup_fn=fake_setup, needs_enable=True)
|
|
try:
|
|
gateway_mod._configure_platform(plat)
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
assert "setup_called" in calls
|
|
# Plugin should now be enabled
|
|
reloaded = load_config()
|
|
assert "irc_platform" in reloaded.get("plugins", {}).get("enabled", [])
|
|
|
|
def test_configure_platform_fallback_when_no_setup_fn(self, monkeypatch, capsys):
|
|
"""A plugin with no setup_fn falls back to env-var instructions."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
|
|
plat = _register_irc_platform(setup_fn=None)
|
|
try:
|
|
gateway_mod._configure_platform(plat)
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "IRC" in out
|
|
assert "IRC_SERVER" in out
|
|
|
|
|
|
# ── End-to-end fresh-install gateway setup ──────────────────────────────────
|
|
|
|
|
|
class TestIRCGatewaySetupFreshInstall:
|
|
"""Simulate the full `hermes setup gateway` experience with IRC present."""
|
|
|
|
def test_setup_gateway_shows_irc_in_platform_menu(self, monkeypatch, capsys, tmp_path):
|
|
"""The gateway setup menu lists IRC among the available platforms."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
from hermes_cli import setup as setup_mod
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
_register_irc_platform()
|
|
try:
|
|
for key in ("IRC_SERVER", "IRC_CHANNEL", "IRC_NICKNAME"):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
# Sanity-check: IRC must be visible to _all_platforms()
|
|
platforms = gateway_mod._all_platforms()
|
|
assert any(p["key"] == "irc" for p in platforms), \
|
|
f"IRC not in platforms: {[p['key'] for p in platforms]}"
|
|
|
|
# Capture what prompt_checklist is asked to display
|
|
checklist_calls = []
|
|
|
|
def capture_prompt_checklist(question, choices, pre_selected=None):
|
|
checklist_calls.append({"question": question, "choices": choices})
|
|
return [] # nothing selected → clean exit
|
|
|
|
monkeypatch.setattr(setup_mod, "prompt_yes_no", lambda *a, **kw: False)
|
|
monkeypatch.setattr(setup_mod, "prompt_checklist", capture_prompt_checklist)
|
|
monkeypatch.setattr(gateway_mod, "supports_systemd_services", lambda: False)
|
|
monkeypatch.setattr(gateway_mod, "is_macos", lambda: False)
|
|
monkeypatch.setattr(gateway_mod, "_is_service_installed", lambda: False)
|
|
monkeypatch.setattr(gateway_mod, "_is_service_running", lambda: False)
|
|
|
|
setup_mod.setup_gateway({})
|
|
|
|
# Find the platform-selection prompt
|
|
platform_prompt = next(
|
|
(c for c in checklist_calls if "platform" in c["question"].lower()),
|
|
None,
|
|
)
|
|
assert platform_prompt is not None, \
|
|
f"No platform prompt found in {checklist_calls}"
|
|
choices_text = "\n".join(platform_prompt["choices"])
|
|
assert "IRC" in choices_text
|
|
assert "💬" in choices_text
|
|
assert "not configured" in choices_text.lower()
|
|
finally:
|
|
_unregister_irc_platform()
|
|
|
|
def test_setup_gateway_irc_counts_as_messaging_platform(self, monkeypatch, capsys, tmp_path):
|
|
"""When IRC is configured, setup_gateway counts it as a messaging platform."""
|
|
import hermes_cli.gateway as gateway_mod
|
|
from hermes_cli import setup as setup_mod
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
_register_irc_platform()
|
|
try:
|
|
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
|
|
monkeypatch.setenv("IRC_CHANNEL", "#hermes")
|
|
monkeypatch.setenv("IRC_NICKNAME", "hermes-bot")
|
|
|
|
monkeypatch.setattr(setup_mod, "prompt_yes_no", lambda *a, **kw: False)
|
|
monkeypatch.setattr(setup_mod, "prompt_choice", lambda *a, **kw: 0)
|
|
monkeypatch.setattr(gateway_mod, "supports_systemd_services", lambda: False)
|
|
monkeypatch.setattr(gateway_mod, "is_macos", lambda: False)
|
|
monkeypatch.setattr(gateway_mod, "_is_service_installed", lambda: False)
|
|
monkeypatch.setattr(gateway_mod, "_is_service_running", lambda: False)
|
|
|
|
setup_mod.setup_gateway({})
|
|
|
|
out = capsys.readouterr().out
|
|
assert "Messaging platforms configured!" in out
|
|
finally:
|
|
_unregister_irc_platform()
|