fix(slack): guard against empty text in chat.postMessage

Slack API returns `no_text` error when `chat.postMessage` is called
with empty or whitespace-only text. This happens when cron delivery
posts a response before the agent produces content (e.g. malformed
turn before [SILENT]).

Add early-return guards in both code paths:
- `_standalone_send()`: out-of-process cron delivery via Web API
- `send()`: in-process gateway adapter

Both paths now skip the API call and return success when the formatted
message is empty or whitespace-only.

Fixes #52663
This commit is contained in:
liuhao1024 2026-06-26 04:19:14 +08:00 • committed by Teknium
parent c13af09177
commit a2d8b4b1f6
2 changed files with 76 additions and 0 deletions

View file

@ -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:

View file

@ -4071,6 +4071,69 @@ class TestMessageSplitting:
assert "<https://en.wikipedia.org/wiki/Foo_(bar)|Foo>" 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
# ---------------------------------------------------------------------------