diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 17458f18f9b1..8796ccabfda7 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -1566,6 +1566,11 @@ class SlackAdapter(BasePlatformAdapter): # Convert standard markdown → Slack mrkdwn formatted = self.format_message(content) + # Guard against empty/whitespace-only messages — Slack API + # returns ``no_text`` for chat.postMessage with blank text. + if not formatted or not formatted.strip(): + return SendResult(success=True) + # Split long messages, preserving code block boundaries chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH) @@ -5203,6 +5208,14 @@ async def _standalone_send( exc_info=True, ) + if not formatted or not formatted.strip(): + logger.debug("[Slack] _standalone_send: skipping empty/whitespace message") + return { + "success": True, + "platform": "slack", + "skipped": "empty_text", + } + try: import aiohttp except ImportError: diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index 8b827fbca29a..e148ad5b0778 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -4071,6 +4071,69 @@ class TestMessageSplitting: assert "" in kwargs["text"] +class TestEmptyTextGuard: + """Guard against Slack ``no_text`` errors when content is empty/whitespace.""" + + @pytest.mark.asyncio + async def test_send_skips_empty_string(self, adapter): + """Empty content must not call chat_postMessage.""" + adapter._app.client.chat_postMessage = AsyncMock(return_value={"ts": "ts1"}) + result = await adapter.send("C123", "") + assert result.success is True + adapter._app.client.chat_postMessage.assert_not_called() + + @pytest.mark.asyncio + async def test_send_skips_whitespace_only(self, adapter): + """Whitespace-only content must not call chat_postMessage.""" + adapter._app.client.chat_postMessage = AsyncMock(return_value={"ts": "ts1"}) + result = await adapter.send("C123", " \n\t ") + assert result.success is True + adapter._app.client.chat_postMessage.assert_not_called() + + @pytest.mark.asyncio + async def test_standalone_send_skips_empty(self, monkeypatch): + """_standalone_send returns success without HTTP call on empty text.""" + from plugins.platforms.slack.adapter import _standalone_send + from types import SimpleNamespace + + pconfig = SimpleNamespace(token="xoxb-test", extra={}) + + # Patch aiohttp so the import succeeds, but it should never be used. + mock_session = MagicMock() + mock_session.__aenter__ = AsyncMock(return_value=mock_session) + mock_session.__aexit__ = AsyncMock(return_value=False) + monkeypatch.setattr( + "plugins.platforms.slack.adapter.aiohttp", + MagicMock(ClientSession=MagicMock(return_value=mock_session)), + ) + + result = await _standalone_send(pconfig, "C123", "") + assert result.get("success") is True + assert result.get("skipped") == "empty_text" + mock_session.post.assert_not_called() + + @pytest.mark.asyncio + async def test_standalone_send_skips_whitespace(self, monkeypatch): + """_standalone_send returns success without HTTP call on whitespace.""" + from plugins.platforms.slack.adapter import _standalone_send + from types import SimpleNamespace + + pconfig = SimpleNamespace(token="xoxb-test", extra={}) + + mock_session = MagicMock() + mock_session.__aenter__ = AsyncMock(return_value=mock_session) + mock_session.__aexit__ = AsyncMock(return_value=False) + monkeypatch.setattr( + "plugins.platforms.slack.adapter.aiohttp", + MagicMock(ClientSession=MagicMock(return_value=mock_session)), + ) + + result = await _standalone_send(pconfig, "C123", " \n ") + assert result.get("success") is True + assert result.get("skipped") == "empty_text" + mock_session.post.assert_not_called() + + # --------------------------------------------------------------------------- # TestReplyBroadcast # ---------------------------------------------------------------------------