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}