mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
Salvaged from PR #36180 (commits68dfeb4b16and86f437509bby arimu1), re-applied onto current main with the incidental black-reformat churn stripped out (~1,700 lines -> the semantic change + tests). Previously gateway/config.py enrolled the api_server platform on `api_server_enabled or api_server_key`, so API_SERVER_ENABLED=true with no key (or a weak/placeholder key) still loaded the platform: the adapter is instantiated (ResponseStore/SQLite opened in __init__), the reconnect watcher spins, and the startup guard refuses at connect() — logging errors forever. Now the platform is enrolled only when API_SERVER_KEY passes the same strength bar as the adapter's startup guard (has_usable_secret, min_length=16), via a shared _has_usable_api_server_key() helper. The no-op `lambda cfg: True` connected-checker for API_SERVER is also replaced with the same key check, so get_connected_platforms() only reports the platform "up" when it could actually start. Known limitation (intentionally out of scope): a YAML config with `platforms.api_server.enabled: true` and no key still loads the platform; this gate covers the env-override path only. Dropped from the original PR: EMAIL/SMS checker additions (scope creep beyond the PR title; absent on current main) and the wholesale black reformat of gateway/config.py and tests. Fixes #36111 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
"""
|
|
Verify that every gateway platform — built-in and plugin — has a connection
|
|
checker so ``GatewayConfig.get_connected_platforms()`` doesn't silently drop
|
|
platforms with bespoke auth requirements.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from gateway.config import Platform, _PLATFORM_CONNECTED_CHECKERS, _BUILTIN_PLATFORM_VALUES
|
|
|
|
|
|
def test_all_builtins_have_checker_or_generic_token_path():
|
|
"""Every built-in Platform member must be reachable by either:
|
|
|
|
1. The generic ``config.token or config.api_key`` check, OR
|
|
2. A platform-specific entry in ``_PLATFORM_CONNECTED_CHECKERS``.
|
|
|
|
This guarantees ``get_connected_platforms()`` doesn't silently ignore
|
|
a built-in just because nobody added it to the checker dict.
|
|
"""
|
|
# Platforms covered by the generic token/api_key branch
|
|
generic_token_values = {p.value for p in {
|
|
Platform.TELEGRAM,
|
|
Platform.DISCORD,
|
|
Platform.SLACK,
|
|
Platform.MATRIX,
|
|
Platform.MATTERMOST,
|
|
Platform.HOMEASSISTANT,
|
|
}}
|
|
|
|
# Platforms with a bespoke checker
|
|
checker_values = {p.value for p in set(_PLATFORM_CONNECTED_CHECKERS.keys())}
|
|
|
|
# Platforms whose connection check now comes from a registered plugin entry
|
|
# (is_connected / validate_config). Several adapters migrated out of core
|
|
# into bundled plugins (#41112); their checker moved with them to the
|
|
# platform registry, so get_connected_platforms() resolves them via the
|
|
# registry fallback rather than _PLATFORM_CONNECTED_CHECKERS.
|
|
plugin_checker_values: set[str] = set()
|
|
try:
|
|
from hermes_cli.plugins import discover_plugins
|
|
from gateway.platform_registry import platform_registry
|
|
discover_plugins()
|
|
for _entry in platform_registry.all_entries():
|
|
if _entry.is_connected is not None or _entry.validate_config is not None:
|
|
plugin_checker_values.add(_entry.name)
|
|
except Exception:
|
|
pass
|
|
|
|
# Every built-in should be in one of the sets
|
|
all_builtins = set(_BUILTIN_PLATFORM_VALUES)
|
|
missing = (
|
|
all_builtins
|
|
- generic_token_values
|
|
- checker_values
|
|
- plugin_checker_values
|
|
- {"local"}
|
|
)
|
|
|
|
assert not missing, (
|
|
f"Built-in platforms missing a connection checker: "
|
|
f"{sorted(missing)}. "
|
|
f"Add them to _PLATFORM_CONNECTED_CHECKERS or generic_token_platforms."
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items()))
|
|
def test_checker_handles_minimal_config(platform, checker):
|
|
"""Each bespoke checker must not crash on a minimal PlatformConfig."""
|
|
mock_config = MagicMock()
|
|
mock_config.extra = {}
|
|
mock_config.token = None
|
|
mock_config.api_key = None
|
|
mock_config.enabled = True
|
|
|
|
# Should return a bool without raising
|
|
result = checker(mock_config)
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
@pytest.mark.parametrize("platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items()))
|
|
def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
|
|
"""Each bespoke checker must return True when the config looks valid."""
|
|
mock_config = MagicMock()
|
|
mock_config.token = None
|
|
mock_config.api_key = None
|
|
mock_config.enabled = True
|
|
|
|
# Set up platform-specific mock extra fields so the checker succeeds
|
|
if platform == Platform.WEIXIN:
|
|
mock_config.extra = {"account_id": "123", "token": "***"}
|
|
elif platform == Platform.SIGNAL:
|
|
mock_config.extra = {"http_url": "http://signal:8080"}
|
|
elif platform == Platform.EMAIL:
|
|
mock_config.extra = {"address": "hermes@example.com"}
|
|
elif platform == Platform.SMS:
|
|
monkeypatch.setenv("TWILIO_ACCOUNT_SID", "ACtest")
|
|
mock_config.extra = {}
|
|
elif platform == Platform.API_SERVER:
|
|
mock_config.extra = {"key": "opensslrandhex32strongkey"}
|
|
elif platform in {
|
|
Platform.WEBHOOK,
|
|
Platform.WHATSAPP,
|
|
}:
|
|
mock_config.extra = {}
|
|
elif platform == Platform.MSGRAPH_WEBHOOK:
|
|
mock_config.extra = {"client_state": "expected-client-state"}
|
|
elif platform == Platform.FEISHU:
|
|
mock_config.extra = {"app_id": "app"}
|
|
elif platform == Platform.WECOM:
|
|
mock_config.extra = {"bot_id": "bot"}
|
|
elif platform == Platform.WECOM_CALLBACK:
|
|
mock_config.extra = {"corp_id": "corp"}
|
|
elif platform == Platform.BLUEBUBBLES:
|
|
mock_config.extra = {"server_url": "http://bb:1234", "password": "pw"}
|
|
elif platform == Platform.QQBOT:
|
|
mock_config.extra = {"app_id": "app", "client_secret": "sec"}
|
|
elif platform == Platform.YUANBAO:
|
|
mock_config.extra = {"app_id": "app", "app_secret": "sec"}
|
|
elif platform == Platform.DINGTALK:
|
|
mock_config.extra = {"client_id": "id", "client_secret": "sec"}
|
|
elif platform == Platform.RELAY:
|
|
mock_config.extra = {"relay_url": "wss://connector.example/relay"}
|
|
else:
|
|
pytest.skip(f"No synthetic config defined for {platform.value}")
|
|
|
|
result = checker(mock_config)
|
|
assert result is True, f"{platform.value} checker should return True with valid-looking config"
|
|
|
|
|
|
def test_api_server_checker_key_validity():
|
|
"""API_SERVER checker: missing, placeholder, short, and strong keys."""
|
|
checker = _PLATFORM_CONNECTED_CHECKERS[Platform.API_SERVER]
|
|
|
|
cfg = MagicMock()
|
|
|
|
# Missing
|
|
cfg.extra = {}
|
|
assert checker(cfg) is False
|
|
|
|
# Placeholder
|
|
cfg.extra = {"key": "changeme"}
|
|
assert checker(cfg) is False
|
|
|
|
# Too short (<16 chars)
|
|
cfg.extra = {"key": "shortkey"}
|
|
assert checker(cfg) is False
|
|
|
|
# Strong key (>=16 chars, not a placeholder)
|
|
cfg.extra = {"key": "opensslrandhex32strongkey"}
|
|
assert checker(cfg) is True
|