open-swe/tests/test_slack_thread_reply_tool.py
open-swe[bot] eb92947806
fix: propagate Slack reply errors (#1358)
* fix: propagate Slack reply errors

Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>

* fix: classify Slack 429 as rate_limited with retry-after

Slack chat.postMessage rate limiting returns HTTP 429, which raise_for_status
turned into a generic http_error and told the agent to retry immediately,
ignoring Slack's retry window. Special-case 429 (threading Retry-After) and
normalize the ratelimited body code before the generic HTTP path so the
existing rate_limited hint actually fires.

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
2026-05-28 23:11:42 +00:00

121 lines
3.9 KiB
Python

from __future__ import annotations
import importlib
from typing import Any
import pytest
slack_reply_tool = importlib.import_module("agent.tools.slack_thread_reply")
def _config() -> dict[str, Any]:
return {
"configurable": {
"slack_thread": {
"channel_id": "C1",
"thread_ts": "1.0",
}
}
}
def test_slack_thread_reply_returns_structured_error_for_msg_too_long(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_post_and_store_mapping(
channel_id: str, thread_ts: str, message: str
) -> tuple[str | None, str | None]:
return None, "msg_too_long"
monkeypatch.setattr(slack_reply_tool, "get_config", _config)
monkeypatch.setattr(slack_reply_tool, "_post_and_store_mapping", fake_post_and_store_mapping)
result = slack_reply_tool.slack_thread_reply("hello")
assert result == {
"success": False,
"error": "msg_too_long",
"slack_error": "msg_too_long",
"message_chars": 5,
"hint": "Slack rejected the message as too long; retry with a shorter message.",
}
@pytest.mark.parametrize("slack_error", ["channel_not_found", "not_in_channel"])
def test_slack_thread_reply_hints_not_to_retry_channel_errors(
slack_error: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_post_and_store_mapping(
channel_id: str, thread_ts: str, message: str
) -> tuple[str | None, str | None]:
return None, slack_error
monkeypatch.setattr(slack_reply_tool, "get_config", _config)
monkeypatch.setattr(slack_reply_tool, "_post_and_store_mapping", fake_post_and_store_mapping)
result = slack_reply_tool.slack_thread_reply("hello")
assert result["success"] is False
assert result["error"] == slack_error
assert result["slack_error"] == slack_error
assert result["message_chars"] == 5
assert "do not retry" in result["hint"]
assert "trace output" in result["hint"]
def test_slack_thread_reply_rate_limited_hint_includes_retry_after(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_post_and_store_mapping(
channel_id: str, thread_ts: str, message: str
) -> tuple[str | None, str | None]:
return None, "rate_limited: 30"
monkeypatch.setattr(slack_reply_tool, "get_config", _config)
monkeypatch.setattr(slack_reply_tool, "_post_and_store_mapping", fake_post_and_store_mapping)
result = slack_reply_tool.slack_thread_reply("hello")
assert result["success"] is False
assert result["error"] == "rate_limited: 30"
assert result["slack_error"] == "rate_limited: 30"
assert "30s" in result["hint"]
assert "wait" in result["hint"]
def test_slack_thread_reply_rate_limited_hint_without_retry_after(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_post_and_store_mapping(
channel_id: str, thread_ts: str, message: str
) -> tuple[str | None, str | None]:
return None, "rate_limited"
monkeypatch.setattr(slack_reply_tool, "get_config", _config)
monkeypatch.setattr(slack_reply_tool, "_post_and_store_mapping", fake_post_and_store_mapping)
result = slack_reply_tool.slack_thread_reply("hello")
assert result["success"] is False
assert result["slack_error"] == "rate_limited"
assert "wait" in result["hint"]
def test_slack_thread_reply_uses_post_failed_without_slack_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_post_and_store_mapping(
channel_id: str, thread_ts: str, message: str
) -> tuple[str | None, str | None]:
return None, None
monkeypatch.setattr(slack_reply_tool, "get_config", _config)
monkeypatch.setattr(slack_reply_tool, "_post_and_store_mapping", fake_post_and_store_mapping)
result = slack_reply_tool.slack_thread_reply("hello")
assert result["success"] is False
assert result["error"] == "post failed"
assert result["slack_error"] is None
assert result["message_chars"] == 5