From 77beb6a0858d4b30d803abc200658039d38e7768 Mon Sep 17 00:00:00 2001 From: x7peeps Date: Wed, 22 Jul 2026 03:53:43 -0700 Subject: [PATCH] fix(slack): set non-retryable fatal error on missing Slack credentials Missing SLACK_BOT_TOKEN / SLACK_APP_TOKEN is a permanent configuration error, not a transient outage. Without a fatal-error marker the gateway queued Slack for background reconnection and looped forever (#66696). Set _set_fatal_error(..., retryable=False) so the reconnect watcher drops it from the retry queue, and point the log/error text at `hermes gateway setup` / the profile's ~/.hermes/.env. Salvaged from PR #66720 by @x7peeps. Fixes #66696. --- plugins/platforms/slack/adapter.py | 28 +++++++++++++- tests/gateway/test_slack.py | 61 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 7ce10bb94814..e78d3c57524c 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -1131,10 +1131,34 @@ class SlackAdapter(BasePlatformAdapter): app_token = os.getenv("SLACK_APP_TOKEN") if not raw_token: - logger.error("[Slack] SLACK_BOT_TOKEN not set") + logger.error( + "[Slack] SLACK_BOT_TOKEN not set — this is a permanent config " + "error; set SLACK_BOT_TOKEN via `hermes gateway setup` " + "or in the active profile's ~/.hermes/.env file, then restart " + "the gateway.", + ) + self._set_fatal_error( + "missing_slack_bot_token", + "SLACK_BOT_TOKEN not configured. Use `hermes gateway setup` " + "or add it to your active profile's ~/.hermes/.env file, " + "then restart the gateway.", + retryable=False, + ) return False if not app_token: - logger.error("[Slack] SLACK_APP_TOKEN not set") + logger.error( + "[Slack] SLACK_APP_TOKEN not set — this is a permanent config " + "error; set SLACK_APP_TOKEN via `hermes gateway setup` " + "or in the active profile's ~/.hermes/.env file, then restart " + "the gateway.", + ) + self._set_fatal_error( + "missing_slack_app_token", + "SLACK_APP_TOKEN not configured. Use `hermes gateway setup` " + "or add it to your active profile's ~/.hermes/.env file, " + "then restart the gateway.", + retryable=False, + ) return False proxy_url = _resolve_slack_proxy_url() diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index 6f5488c03a4b..d8c25a4a9d04 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -5192,3 +5192,64 @@ class TestThreadContextAppMessages: ) assert "hello" in content # the real message survives; empty bot msg dropped + + +# --------------------------------------------------------------------------- +# Missing-credential handling — fatal-error contract +# --------------------------------------------------------------------------- + + +class TestMissingCredentials: + """Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN must set a non-retryable fatal error.""" + + @pytest.mark.asyncio + async def test_missing_bot_token_sets_fatal_error(self): + """When SLACK_BOT_TOKEN is absent from both config and env, connect() + must set fatal_error with code 'missing_slack_bot_token' and retryable=False.""" + config = PlatformConfig(enabled=True, token=None) # no bot token + adapter = SlackAdapter(config) + + fatal_errors = [] + + def capture_fatal(code, message, *, retryable): + fatal_errors.append({"code": code, "message": message, "retryable": retryable}) + + with ( + patch.object(adapter, "_set_fatal_error", side_effect=capture_fatal), + patch.dict(os.environ, {}, clear=True), + ): + result = await adapter.connect() + + assert result is False + assert len(fatal_errors) == 1 + assert fatal_errors[0]["code"] == "missing_slack_bot_token" + assert fatal_errors[0]["retryable"] is False + assert "SLACK_BOT_TOKEN" in fatal_errors[0]["message"] + assert "hermes gateway setup" in fatal_errors[0]["message"].lower() or ".env" in fatal_errors[0]["message"] + + @pytest.mark.asyncio + async def test_missing_app_token_sets_fatal_error(self): + """When SLACK_APP_TOKEN is absent but SLACK_BOT_TOKEN is present, + connect() must set fatal_error with code 'missing_slack_app_token' + and retryable=False.""" + config = PlatformConfig(enabled=True, token="xoxb-fake") + adapter = SlackAdapter(config) + + fatal_errors = [] + + def capture_fatal(code, message, *, retryable): + fatal_errors.append({"code": code, "message": message, "retryable": retryable}) + + with ( + patch.object(adapter, "_set_fatal_error", side_effect=capture_fatal), + patch.dict(os.environ, {"SLACK_BOT_TOKEN": "xoxb-fake"}, clear=True), + ): + result = await adapter.connect() + + assert result is False + assert len(fatal_errors) == 1 + assert fatal_errors[0]["code"] == "missing_slack_app_token" + assert fatal_errors[0]["retryable"] is False + assert "SLACK_APP_TOKEN" in fatal_errors[0]["message"] + assert "hermes gateway setup" in fatal_errors[0]["message"].lower() or ".env" in fatal_errors[0]["message"] +