open-swe/tests/test_slack_thread_reply_tool.py
Adam Moussa 7b9eff62e9
fix: enforce terse Slack tool messages (#1717) (#197)
(cherry picked from commit 092abafa4cb955c3823f727d35d7bad94e1147ab)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
2026-07-16 17:51:17 -04:00

334 lines
11 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_prompt_requires_slack_only_terseness() -> None:
prompt = slack_reply_tool.slack_thread_reply.__doc__ or ""
assert "as terse as possible" in prompt
assert "default to one sentence" in prompt
assert "specific to Slack tool messages" in prompt
assert "not normal web UI assistant messages" in prompt
async 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,
*,
blocks: list[dict[str, Any]] | None = None,
) -> 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 = await 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"])
async 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,
*,
blocks: list[dict[str, Any]] | None = None,
) -> 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 = await 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"]
async 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,
*,
blocks: list[dict[str, Any]] | None = None,
) -> 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 = await 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"]
async 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,
*,
blocks: list[dict[str, Any]] | None = None,
) -> 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 = await slack_reply_tool.slack_thread_reply("hello")
assert result["success"] is False
assert result["slack_error"] == "rate_limited"
assert "wait" in result["hint"]
async 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,
*,
blocks: list[dict[str, Any]] | None = None,
) -> 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 = await 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
async def test_slack_thread_reply_builds_option_blocks(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
async def fake_post_and_store_mapping(
channel_id: str,
thread_ts: str,
message: str,
*,
blocks: list[dict[str, Any]] | None = None,
) -> tuple[str | None, str | None]:
captured.update(
{"channel_id": channel_id, "thread_ts": thread_ts, "message": message, "blocks": blocks}
)
return "2.0", None
monkeypatch.setattr(slack_reply_tool, "get_config", _config)
monkeypatch.setattr(slack_reply_tool, "_post_and_store_mapping", fake_post_and_store_mapping)
result = await slack_reply_tool.slack_thread_reply("Pick one", options=["A", "B"])
assert result == {"success": True}
assert captured["channel_id"] == "C1"
assert captured["thread_ts"] == "1.0"
assert captured["message"] == "Pick one"
actions = captured["blocks"][1]
assert actions["type"] == "actions"
assert [button["text"]["text"] for button in actions["elements"]] == ["A", "B"]
assert actions["elements"][0]["action_id"] == "open_swe_option_select"
def _channel_only_config() -> dict[str, Any]:
return {"configurable": {"slack_thread": {"channel_id": "C9"}}}
def _channel_run_config() -> dict[str, Any]:
return {
"configurable": {
"slack_thread": {"channel_id": "C9"},
"thread_id": "run-1",
}
}
@pytest.fixture(autouse=True)
def _reset_top_level_posts() -> Any:
slack_reply_tool._top_level_posts.clear()
yield
slack_reply_tool._top_level_posts.clear()
async def test_slack_thread_reply_requires_channel_id(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(slack_reply_tool, "get_config", lambda: {"configurable": {}})
result = await slack_reply_tool.slack_thread_reply("hello")
assert result["success"] is False
assert result["error"] == "Missing slack_thread.channel_id in config"
async def test_slack_thread_reply_posts_top_level_when_no_thread_ts(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
async def fake_top_level(
channel_id: str,
text: str,
*,
blocks: list[dict[str, Any]] | None = None,
) -> tuple[str | None, str | None]:
captured.update({"channel_id": channel_id, "text": text, "blocks": blocks})
return "3.0", None
async def fail_thread_reply(*args: Any, **kwargs: Any) -> tuple[str | None, str | None]:
raise AssertionError("should not post a thread reply without thread_ts")
monkeypatch.setattr(slack_reply_tool, "get_config", _channel_only_config)
monkeypatch.setattr(slack_reply_tool, "post_slack_top_level_message_with_ts", fake_top_level)
monkeypatch.setattr(slack_reply_tool, "post_slack_thread_reply_with_ts", fail_thread_reply)
result = await slack_reply_tool.slack_thread_reply("Scheduled report")
assert result == {"success": True}
assert captured["channel_id"] == "C9"
assert captured["text"] == "Scheduled report"
async def test_slack_thread_reply_top_level_surfaces_not_in_channel(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_top_level(
channel_id: str,
text: str,
*,
blocks: list[dict[str, Any]] | None = None,
) -> tuple[str | None, str | None]:
return None, "not_in_channel"
monkeypatch.setattr(slack_reply_tool, "get_config", _channel_only_config)
monkeypatch.setattr(slack_reply_tool, "post_slack_top_level_message_with_ts", fake_top_level)
result = await slack_reply_tool.slack_thread_reply("Scheduled report")
assert result["success"] is False
assert result["slack_error"] == "not_in_channel"
assert "do not retry" in result["hint"]
async def test_slack_thread_reply_top_level_drops_interactive_blocks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
async def fake_top_level(
channel_id: str,
text: str,
*,
blocks: list[dict[str, Any]] | None = None,
) -> tuple[str | None, str | None]:
captured["blocks"] = blocks
return "3.0", None
monkeypatch.setattr(slack_reply_tool, "get_config", _channel_only_config)
monkeypatch.setattr(slack_reply_tool, "post_slack_top_level_message_with_ts", fake_top_level)
options_result = await slack_reply_tool.slack_thread_reply("Pick", options=["A", "B"])
assert options_result == {"success": True}
assert captured["blocks"] is None
approval_result = await slack_reply_tool.slack_thread_reply("Plan?", plan_approval=True)
assert approval_result == {"success": True}
assert captured["blocks"] is None
async def test_slack_thread_reply_allows_only_one_top_level_post_per_run(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls = 0
async def fake_top_level(
channel_id: str,
text: str,
*,
blocks: list[dict[str, Any]] | None = None,
) -> tuple[str | None, str | None]:
nonlocal calls
calls += 1
return "3.0", None
monkeypatch.setattr(slack_reply_tool, "get_config", _channel_run_config)
monkeypatch.setattr(slack_reply_tool, "post_slack_top_level_message_with_ts", fake_top_level)
first = await slack_reply_tool.slack_thread_reply("First")
second = await slack_reply_tool.slack_thread_reply("Second")
assert first == {"success": True}
assert second["success"] is False
assert "Only one top-level message per run" in second["hint"]
assert calls == 1
async def test_slack_thread_reply_failed_top_level_post_does_not_consume_slot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
results = iter([(None, "rate_limited"), ("3.0", None)])
async def fake_top_level(
channel_id: str,
text: str,
*,
blocks: list[dict[str, Any]] | None = None,
) -> tuple[str | None, str | None]:
return next(results)
monkeypatch.setattr(slack_reply_tool, "get_config", _channel_run_config)
monkeypatch.setattr(slack_reply_tool, "post_slack_top_level_message_with_ts", fake_top_level)
first = await slack_reply_tool.slack_thread_reply("First")
second = await slack_reply_tool.slack_thread_reply("Retry")
assert first["success"] is False
assert second == {"success": True}