mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 22:03:14 +00:00
Applies the plan's C5 step: git mv every test per the domain-reorg
move-map (movemap-m50.txt) into tests/{agent,analyzer,auth,dashboard,
github,middleware,models,reviewer,sandbox,slack,tools,webhooks}/, plus
the 13 fork-only placements from the scoping report §2c (Atlassian
webhook tests -> tests/webhooks/, test_atlassian_connect.py and
test_auth_error_leak.py -> tests/auth/, jira/confluence util tests ->
tests/tools/, test_repo_binding_isolation.py -> tests/sandbox/,
bot-identity/autofix tests -> tests/github/).
Path-only move: the only content edits are parents[1] -> parents[2]
fixes in test_e2b_integration.py and test_daytona_integration.py,
required because their __file__-relative ROOT path gained one more
directory level in the move.
Monkeypatch retargets for these files were already completed in C4;
none remained outstanding here.
313 lines
11 KiB
Python
313 lines
11 KiB
Python
"""Tests for the Slack Assistants API integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from agent.utils import slack as slack_utils
|
|
|
|
|
|
def _ok_response() -> MagicMock:
|
|
response = MagicMock()
|
|
response.json.return_value = {"ok": True, "ts": "1.0"}
|
|
response.raise_for_status.return_value = None
|
|
return response
|
|
|
|
|
|
def _err_response(error: str = "channel_not_found") -> MagicMock:
|
|
response = MagicMock()
|
|
response.json.return_value = {"ok": False, "error": error}
|
|
response.raise_for_status.return_value = None
|
|
return response
|
|
|
|
|
|
def _rate_limited_response(retry_after: str | None = None) -> MagicMock:
|
|
response = MagicMock()
|
|
response.status_code = 429
|
|
response.headers = {"Retry-After": retry_after} if retry_after else {}
|
|
return response
|
|
|
|
|
|
def _async_client_cm(post_response: MagicMock) -> AsyncMock:
|
|
client_cm = AsyncMock()
|
|
client_cm.__aenter__.return_value = client_cm
|
|
client_cm.post = AsyncMock(return_value=post_response)
|
|
return client_cm
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_slack_assistant_status_noop_when_no_token(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
ok = await slack_utils.set_slack_assistant_status("C1", "1.0", "thinking…")
|
|
|
|
assert ok is False
|
|
client_cm.post.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_slack_assistant_status_calls_correct_endpoint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
ok = await slack_utils.set_slack_assistant_status("C1", "1.0", "thinking…")
|
|
|
|
assert ok is True
|
|
client_cm.post.assert_awaited_once()
|
|
args, kwargs = client_cm.post.call_args
|
|
assert args[0].endswith("/assistant.threads.setStatus")
|
|
assert kwargs["json"] == {
|
|
"channel_id": "C1",
|
|
"thread_ts": "1.0",
|
|
"status": "thinking…",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_slack_assistant_status_passes_loading_messages(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
await slack_utils.set_slack_assistant_status(
|
|
"C1", "1.0", "thinking…", loading_messages=["a", "b", "c"]
|
|
)
|
|
|
|
_, kwargs = client_cm.post.call_args
|
|
assert kwargs["json"]["loading_messages"] == ["a", "b", "c"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_slack_assistant_status_caps_loading_messages_at_10(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
await slack_utils.set_slack_assistant_status(
|
|
"C1", "1.0", loading_messages=[f"m{i}" for i in range(15)]
|
|
)
|
|
|
|
_, kwargs = client_cm.post.call_args
|
|
assert len(kwargs["json"]["loading_messages"]) == 10
|
|
assert kwargs["json"]["loading_messages"][0] == "m0"
|
|
assert kwargs["json"]["loading_messages"][-1] == "m9"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_slack_assistant_status_omits_loading_messages_when_unset(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
await slack_utils.set_slack_assistant_status("C1", "1.0", "thinking…")
|
|
|
|
_, kwargs = client_cm.post.call_args
|
|
assert "loading_messages" not in kwargs["json"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_slack_assistant_status_returns_false_on_slack_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_err_response("invalid_thread"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
ok = await slack_utils.set_slack_assistant_status("C1", "1.0", "thinking…")
|
|
|
|
assert ok is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_does_not_call_set_status(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Slack auto-clears the indicator on post; no extra setStatus call needed."""
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
ok = await slack_utils.post_slack_thread_reply("C1", "1.0", "hello")
|
|
|
|
assert ok is True
|
|
assert client_cm.post.await_count == 1
|
|
assert client_cm.post.call_args.args[0].endswith("/chat.postMessage")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_slack_message_calls_chat_update(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.update_slack_message(
|
|
"C1", "1.1", "moved", unfurl_links=False, unfurl_media=False
|
|
)
|
|
|
|
assert result == (True, None)
|
|
assert client_cm.post.await_count == 1
|
|
assert client_cm.post.call_args.args[0].endswith("/chat.update")
|
|
assert client_cm.post.call_args.kwargs["json"] == {
|
|
"channel": "C1",
|
|
"ts": "1.1",
|
|
"text": "moved",
|
|
"unfurl_links": False,
|
|
"unfurl_media": False,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_returns_missing_token_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts("C1", "1.0", "hello")
|
|
|
|
assert result == (None, "missing_slack_bot_token")
|
|
client_cm.post.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_returns_slack_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_err_response("msg_too_long"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts("C1", "1.0", "hello")
|
|
|
|
assert result == (None, "msg_too_long")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_returns_rate_limited_with_retry_after(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_rate_limited_response(retry_after="30"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts("C1", "1.0", "hello")
|
|
|
|
assert result == (None, "rate_limited: 30")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_returns_rate_limited_without_retry_after(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_rate_limited_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts("C1", "1.0", "hello")
|
|
|
|
assert result == (None, "rate_limited")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_normalizes_ratelimited_body_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_err_response("ratelimited"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts("C1", "1.0", "hello")
|
|
|
|
assert result == (None, "rate_limited")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_returns_http_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
client_cm.post = AsyncMock(side_effect=slack_utils.httpx.ConnectError("boom"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts("C1", "1.0", "hello")
|
|
|
|
assert result == (None, "http_error: ConnectError")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_thread_reply_with_ts_sends_blocks(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
blocks = [{"type": "section", "text": {"type": "mrkdwn", "text": "Pick"}}]
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_thread_reply_with_ts(
|
|
"C1", "1.0", "Pick", blocks=blocks
|
|
)
|
|
|
|
assert result == ("1.0", None)
|
|
assert client_cm.post.call_args.kwargs["json"]["blocks"] == blocks
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_top_level_message_with_ts_omits_thread_ts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_ok_response())
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_top_level_message_with_ts("C1", "hello")
|
|
|
|
assert result == ("1.0", None)
|
|
payload = client_cm.post.call_args.kwargs["json"]
|
|
assert payload["channel"] == "C1"
|
|
assert payload["text"] == "hello"
|
|
assert "thread_ts" not in payload
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_slack_top_level_message_with_ts_returns_slack_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_err_response("msg_too_long"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
result = await slack_utils.post_slack_top_level_message_with_ts("C1", "hello")
|
|
|
|
assert result == (None, "msg_too_long")
|
|
|
|
|
|
async def test_post_slack_thread_reply_preserves_bool_return_on_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(slack_utils, "SLACK_BOT_TOKEN", "xoxb-test")
|
|
|
|
client_cm = _async_client_cm(_err_response("channel_not_found"))
|
|
with patch.object(slack_utils.httpx, "AsyncClient", return_value=client_cm):
|
|
ok = await slack_utils.post_slack_thread_reply("C1", "1.0", "hello")
|
|
|
|
assert ok is False
|