diff --git a/plugins/platforms/raft/adapter.py b/plugins/platforms/raft/adapter.py index 5623cef0e5e..67e34b2a906 100644 --- a/plugins/platforms/raft/adapter.py +++ b/plugins/platforms/raft/adapter.py @@ -98,12 +98,20 @@ _RAFT_PROMPT_TURN_IDS: set[str] = set() def check_raft_requirements() -> bool: - """Check if Raft channel dependencies are available.""" + """Check if Raft channel dependencies are available. + + Intentionally silent on failure — this is a passive probe registered as + the platform's ``check_fn``. It is called on every + ``load_gateway_config()`` (message handling, display lookups, agent + turns), so logging here floods the logs for every user without the + ``raft`` CLI installed. The caller (``gateway/platform_registry.py`` + ``create_adapter()``) emits its own warning when requirements are not met + and an adapter is actually requested. This matches the convention used by + other platform adapters (e.g. ``teams/adapter.py``). + """ if not AIOHTTP_AVAILABLE: - logger.warning("[raft] aiohttp is not installed — install with: pip install aiohttp") return False if not shutil.which("raft"): - logger.warning("[raft] raft CLI not found in PATH — install from https://raft.build") return False return True diff --git a/tests/plugins/test_raft_check_fn_silent.py b/tests/plugins/test_raft_check_fn_silent.py new file mode 100644 index 00000000000..76a906a9c54 --- /dev/null +++ b/tests/plugins/test_raft_check_fn_silent.py @@ -0,0 +1,75 @@ +"""Regression tests for the raft platform plugin's check_fn. + +The raft platform adapter's ``check_raft_requirements()`` is registered as +the platform's ``check_fn``. This function is invoked on every +``load_gateway_config()`` call (dozens of times during normal gateway +operation). It must therefore be a *silent* predicate — returning True/False +without logging — otherwise every user without the ``raft`` CLI installed +gets their logs flooded with WARNING messages every few seconds. + +See: https://github.com/NousResearch/hermes-agent/issues/49234 +""" + +import logging +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def raft_check(): + """Import check_raft_requirements fresh (adapter self-manages sys.path).""" + from plugins.platforms.raft.adapter import check_raft_requirements + + return check_raft_requirements + + +def test_check_returns_false_when_raft_cli_missing(raft_check): + """check_fn returns False when raft CLI is not in PATH.""" + with patch("plugins.platforms.raft.adapter.shutil.which", return_value=None), \ + patch("plugins.platforms.raft.adapter.AIOHTTP_AVAILABLE", True): + assert raft_check() is False + + +def test_check_returns_false_when_aiohttp_missing(raft_check): + """check_fn returns False when aiohttp dependency is unavailable.""" + with patch("plugins.platforms.raft.adapter.AIOHTTP_AVAILABLE", False): + assert raft_check() is False + + +def test_check_returns_true_when_all_deps_present(raft_check): + """check_fn returns True when all dependencies are available.""" + with patch("plugins.platforms.raft.adapter.shutil.which", return_value="/usr/bin/raft"), \ + patch("plugins.platforms.raft.adapter.AIOHTTP_AVAILABLE", True): + assert raft_check() is True + + +def test_check_silent_when_raft_cli_missing(raft_check, caplog): + """check_fn must NOT log a WARNING when raft CLI is missing. + + This is the regression guard for issue #49234 — logging inside check_fn + causes log spam because the function is called on every config load. + """ + with patch("plugins.platforms.raft.adapter.shutil.which", return_value=None), \ + patch("plugins.platforms.raft.adapter.AIOHTTP_AVAILABLE", True): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.raft.adapter"): + raft_check() + + warnings = [r for r in caplog.records if r.levelno >= logging.WARNING] + assert warnings == [], ( + f"check_raft_requirements must be silent (no WARNING logs), " + f"but emitted: {[r.getMessage() for r in warnings]}" + ) + + +def test_check_silent_when_aiohttp_missing(raft_check, caplog): + """check_fn must NOT log a WARNING when aiohttp is missing.""" + with patch("plugins.platforms.raft.adapter.AIOHTTP_AVAILABLE", False): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.raft.adapter"): + raft_check() + + warnings = [r for r in caplog.records if r.levelno >= logging.WARNING] + assert warnings == [], ( + f"check_raft_requirements must be silent (no WARNING logs), " + f"but emitted: {[r.getMessage() for r in warnings]}" + )