mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 22:03:14 +00:00
* 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>
121 lines
3.9 KiB
Python
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
|