diff --git a/tests/gateway/test_startup_no_eager_platform_install.py b/tests/gateway/test_startup_no_eager_platform_install.py new file mode 100644 index 00000000000..24ecb3f39fa --- /dev/null +++ b/tests/gateway/test_startup_no_eager_platform_install.py @@ -0,0 +1,100 @@ +"""Regression tests: ``_apply_env_overrides`` must not lazy-install platform +SDKs for platforms the user has not configured. + +For adapter plugins, ``PlatformEntry.check_fn`` doubles as the lazy-installer +(it pip-installs the platform SDK as a side effect — see e.g. +``plugins/platforms/discord/adapter.py::check_discord_requirements``). The +enablement sweep in ``_apply_env_overrides`` used to call ``check_fn`` for +*every* registered plugin platform unconditionally, so a single +``load_gateway_config()`` — which the desktop/dashboard readiness probe +(``GET /api/status``) awaits synchronously — pip-installed Discord, Telegram, +Slack, Feishu and Dingtalk even with ``platforms: none``. That blocked +startup until every install finished and made the desktop app time out and +boot-loop (stuck at 94%). + +The fix consults the cheap ``is_connected`` credential check FIRST and only +runs the install-triggering ``check_fn`` for platforms that are already +enabled or actually configured. These tests pin that contract. +""" + +from unittest.mock import MagicMock, patch + +import pytest + +from gateway.config import GatewayConfig, Platform, PlatformConfig, _apply_env_overrides +from gateway.platform_registry import PlatformEntry, platform_registry + + +@pytest.fixture +def isolated_registry(): + """Run with a registry containing only the entries the test registers.""" + original = dict(platform_registry._entries) + platform_registry._entries.clear() + try: + # ``_apply_env_overrides`` calls ``discover_plugins()`` (idempotent), + # which would re-register the real bundled platforms and clobber the + # fakes below. Neutralize it so the test controls the registry. + with patch("hermes_cli.plugins.discover_plugins", lambda *a, **k: None): + yield platform_registry + finally: + platform_registry._entries.clear() + platform_registry._entries.update(original) + + +def _register_fake_platform(name, *, check_fn, is_connected): + platform_registry.register( + PlatformEntry( + name=name, + label=name.title(), + adapter_factory=lambda cfg: MagicMock(), + check_fn=check_fn, + is_connected=is_connected, + source="plugin", + ) + ) + + +def test_unconfigured_platform_is_not_probed_for_install(isolated_registry): + # is_connected reports "no credentials" → the platform must be skipped + # without ever calling check_fn (which would lazy-install the SDK). + check_fn = MagicMock(return_value=True) + _register_fake_platform( + "discord", check_fn=check_fn, is_connected=lambda cfg: False + ) + + config = GatewayConfig() + _apply_env_overrides(config) + + check_fn.assert_not_called() + assert not config.platforms.get(Platform.DISCORD, PlatformConfig()).enabled + + +def test_configured_platform_is_still_installed_and_enabled(isolated_registry): + # is_connected reports "credentials present" → check_fn must run (so the + # SDK is verified/installed) and the platform is auto-enabled, exactly as + # before the fix. + check_fn = MagicMock(return_value=True) + _register_fake_platform( + "discord", check_fn=check_fn, is_connected=lambda cfg: True + ) + + config = GatewayConfig() + _apply_env_overrides(config) + + check_fn.assert_called_once() + assert config.platforms[Platform.DISCORD].enabled is True + + +def test_failed_install_does_not_enable_configured_platform(isolated_registry): + # Credentials present but the SDK genuinely cannot be installed/imported + # (check_fn returns False) → platform must not be enabled. + check_fn = MagicMock(return_value=False) + _register_fake_platform( + "discord", check_fn=check_fn, is_connected=lambda cfg: True + ) + + config = GatewayConfig() + _apply_env_overrides(config) + + check_fn.assert_called_once() + assert not config.platforms.get(Platform.DISCORD, PlatformConfig()).enabled