mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 18:33:15 +00:00
(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>
334 lines
11 KiB
Python
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}
|