mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 18:33:15 +00:00
Avoid treating regex-parsed Slack text as authoritative routing or allowlist input so repository mentions remain model context and access is bounded by installation permissions.
675 lines
24 KiB
Python
675 lines
24 KiB
Python
import asyncio
|
|
|
|
import pytest
|
|
|
|
from agent import webapp
|
|
from agent.utils import slack as slack_utils
|
|
from agent.utils.slack import (
|
|
TRACE_REPLY_TIPS,
|
|
convert_mentions_to_slack_format,
|
|
format_slack_messages_for_prompt,
|
|
looks_like_slack_pr_review_command,
|
|
parse_github_pr_url,
|
|
parse_slack_review_command,
|
|
post_slack_trace_reply,
|
|
replace_bot_mention_with_username,
|
|
select_slack_context_messages,
|
|
strip_bot_mention,
|
|
)
|
|
from agent.webapp import generate_thread_id_from_slack_thread
|
|
|
|
|
|
class _FakeNotFoundError(Exception):
|
|
status_code = 404
|
|
|
|
|
|
class _FakeThreadsClient:
|
|
def __init__(self, thread: dict | None = None, raise_not_found: bool = False) -> None:
|
|
self.thread = thread
|
|
self.raise_not_found = raise_not_found
|
|
self.requested_thread_id: str | None = None
|
|
|
|
async def get(self, thread_id: str) -> dict:
|
|
self.requested_thread_id = thread_id
|
|
if self.raise_not_found:
|
|
raise _FakeNotFoundError("not found")
|
|
if self.thread is None:
|
|
raise AssertionError("thread must be provided when raise_not_found is False")
|
|
return self.thread
|
|
|
|
|
|
class _FakeClient:
|
|
def __init__(self, threads_client: _FakeThreadsClient) -> None:
|
|
self.threads = threads_client
|
|
|
|
|
|
def test_generate_thread_id_from_slack_thread_is_deterministic() -> None:
|
|
channel_id = "C12345"
|
|
thread_ts = "1730900000.123456"
|
|
first = generate_thread_id_from_slack_thread(channel_id, thread_ts)
|
|
second = generate_thread_id_from_slack_thread(channel_id, thread_ts)
|
|
assert first == second
|
|
assert len(first) == 36
|
|
|
|
|
|
def test_select_slack_context_messages_uses_thread_start_when_no_prior_mention() -> None:
|
|
bot_user_id = "UBOT"
|
|
messages = [
|
|
{"ts": "1.0", "text": "hello", "user": "U1"},
|
|
{"ts": "2.0", "text": "context", "user": "U2"},
|
|
{"ts": "3.0", "text": "<@UBOT> please help", "user": "U1"},
|
|
]
|
|
|
|
selected, mode = select_slack_context_messages(messages, "3.0", bot_user_id)
|
|
|
|
assert mode == "thread_start"
|
|
assert [item["ts"] for item in selected] == ["1.0", "2.0", "3.0"]
|
|
|
|
|
|
def test_select_slack_context_messages_uses_previous_mention_boundary() -> None:
|
|
bot_user_id = "UBOT"
|
|
messages = [
|
|
{"ts": "1.0", "text": "hello", "user": "U1"},
|
|
{"ts": "2.0", "text": "<@UBOT> first request", "user": "U1"},
|
|
{"ts": "3.0", "text": "extra context", "user": "U2"},
|
|
{"ts": "4.0", "text": "<@UBOT> second request", "user": "U3"},
|
|
]
|
|
|
|
selected, mode = select_slack_context_messages(messages, "4.0", bot_user_id)
|
|
|
|
assert mode == "last_mention"
|
|
assert [item["ts"] for item in selected] == ["2.0", "3.0", "4.0"]
|
|
|
|
|
|
def test_select_slack_context_messages_ignores_messages_after_current_event() -> None:
|
|
bot_user_id = "UBOT"
|
|
messages = [
|
|
{"ts": "1.0", "text": "<@UBOT> first request", "user": "U1"},
|
|
{"ts": "2.0", "text": "follow-up", "user": "U2"},
|
|
{"ts": "3.0", "text": "<@UBOT> second request", "user": "U3"},
|
|
{"ts": "4.0", "text": "after event", "user": "U4"},
|
|
]
|
|
|
|
selected, mode = select_slack_context_messages(messages, "3.0", bot_user_id)
|
|
|
|
assert mode == "last_mention"
|
|
assert [item["ts"] for item in selected] == ["1.0", "2.0", "3.0"]
|
|
|
|
|
|
def test_strip_bot_mention_removes_bot_tag() -> None:
|
|
assert strip_bot_mention("<@UBOT> please check", "UBOT") == "please check"
|
|
|
|
|
|
def test_strip_bot_mention_removes_bot_username_tag() -> None:
|
|
assert (
|
|
strip_bot_mention("@open-swe please check", "UBOT", bot_username="open-swe")
|
|
== "please check"
|
|
)
|
|
|
|
|
|
def test_replace_bot_mention_with_username() -> None:
|
|
assert (
|
|
replace_bot_mention_with_username("<@UBOT> can you help?", "UBOT", "open-swe")
|
|
== "@open-swe can you help?"
|
|
)
|
|
|
|
|
|
def test_convert_mentions_to_slack_format_basic() -> None:
|
|
assert (
|
|
convert_mentions_to_slack_format("Hey @Brace Sproul(U06KD8BFY95), check this")
|
|
== "Hey <@U06KD8BFY95>, check this"
|
|
)
|
|
|
|
|
|
def test_convert_mentions_to_slack_format_multiple() -> None:
|
|
text = "@Alice(U111) and @Bob(U222) please review"
|
|
assert convert_mentions_to_slack_format(text) == "<@U111> and <@U222> please review"
|
|
|
|
|
|
def test_convert_mentions_to_slack_format_no_match() -> None:
|
|
text = "No mentions here, just @plain text"
|
|
assert convert_mentions_to_slack_format(text) == text
|
|
|
|
|
|
def test_convert_mentions_to_slack_format_preserves_existing_slack_mentions() -> None:
|
|
text = "Already tagged <@U06KD8BFY95> correctly"
|
|
assert convert_mentions_to_slack_format(text) == text
|
|
|
|
|
|
def test_parse_github_pr_url_raw_url() -> None:
|
|
pr_ref = parse_github_pr_url("https://github.com/langchain-ai/open-swe/pull/1244")
|
|
|
|
assert pr_ref is not None
|
|
assert pr_ref.owner == "langchain-ai"
|
|
assert pr_ref.repo == "open-swe"
|
|
assert pr_ref.number == 1244
|
|
assert pr_ref.url == "https://github.com/langchain-ai/open-swe/pull/1244"
|
|
|
|
|
|
def test_parse_github_pr_url_slack_formatted_link() -> None:
|
|
pr_ref = parse_github_pr_url("<https://github.com/langchain-ai/open-swe/pull/1244|PR>")
|
|
|
|
assert pr_ref is not None
|
|
assert pr_ref.owner == "langchain-ai"
|
|
assert pr_ref.repo == "open-swe"
|
|
assert pr_ref.number == 1244
|
|
|
|
|
|
def test_parse_slack_review_command_requires_exact_review_command() -> None:
|
|
pr_ref = parse_slack_review_command("review https://github.com/langchain-ai/open-swe/pull/1244")
|
|
|
|
assert pr_ref is not None
|
|
assert pr_ref.owner == "langchain-ai"
|
|
assert pr_ref.repo == "open-swe"
|
|
assert pr_ref.number == 1244
|
|
assert (
|
|
parse_slack_review_command(
|
|
"please review https://github.com/langchain-ai/open-swe/pull/1244"
|
|
)
|
|
is None
|
|
)
|
|
assert (
|
|
parse_slack_review_command("review https://github.com/langchain-ai/open-swe/issues/1244")
|
|
is None
|
|
)
|
|
|
|
|
|
def test_parse_slack_review_command_supports_slack_link() -> None:
|
|
pr_ref = parse_slack_review_command(
|
|
"review <https://github.com/langchain-ai/open-swe/pull/1244|PR>"
|
|
)
|
|
|
|
assert pr_ref is not None
|
|
assert pr_ref.url == "https://github.com/langchain-ai/open-swe/pull/1244"
|
|
|
|
|
|
def test_parse_slack_review_command_supports_slack_wrapped_raw_link() -> None:
|
|
pr_ref = parse_slack_review_command(
|
|
"review <https://github.com/langchain-ai/open-swe/pull/1244>"
|
|
)
|
|
|
|
assert pr_ref is not None
|
|
assert pr_ref.url == "https://github.com/langchain-ai/open-swe/pull/1244"
|
|
|
|
|
|
def test_looks_like_slack_pr_review_command_validates_github_host() -> None:
|
|
assert looks_like_slack_pr_review_command(
|
|
"review https://github.com/langchain-ai/open-swe/issues/1244"
|
|
)
|
|
assert not looks_like_slack_pr_review_command(
|
|
"review https://example.com/redirect?next=https://github.com/langchain-ai/open-swe/pull/1244"
|
|
)
|
|
|
|
|
|
def test_format_slack_messages_for_prompt_uses_name_and_id() -> None:
|
|
formatted = format_slack_messages_for_prompt(
|
|
[{"ts": "1.0", "text": "hello", "user": "U123"}],
|
|
{"U123": "alice"},
|
|
)
|
|
|
|
assert formatted == "@alice(U123): hello"
|
|
|
|
|
|
def test_format_slack_messages_for_prompt_replaces_bot_id_mention_in_text() -> None:
|
|
formatted = format_slack_messages_for_prompt(
|
|
[{"ts": "1.0", "text": "<@UBOT> status update?", "user": "U123"}],
|
|
{"U123": "alice"},
|
|
bot_user_id="UBOT",
|
|
bot_username="open-swe",
|
|
)
|
|
|
|
assert formatted == "@alice(U123): @open-swe status update?"
|
|
|
|
|
|
def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
posted: list[dict] = []
|
|
|
|
async def fake_post_slack_thread_reply_with_ts(
|
|
channel_id: str,
|
|
thread_ts: str,
|
|
text: str,
|
|
*,
|
|
unfurl_links: bool = True,
|
|
unfurl_media: bool = True,
|
|
) -> str | None:
|
|
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
|
return "1.1"
|
|
|
|
monkeypatch.setattr(
|
|
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
|
)
|
|
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: None)
|
|
|
|
asyncio.run(post_slack_trace_reply("C123", "1.0", "thread-id"))
|
|
|
|
assert len(posted) == 1
|
|
text = posted[0]["text"]
|
|
assert text.startswith("_Tip: ") and text.endswith("_")
|
|
assert any(tip in text for tip in TRACE_REPLY_TIPS)
|
|
assert posted[0]["unfurl_links"] is False
|
|
assert posted[0]["unfurl_media"] is False
|
|
|
|
|
|
def test_post_slack_trace_reply_includes_trace_link_and_tip(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
posted: list[dict] = []
|
|
|
|
async def fake_post_slack_thread_reply_with_ts(
|
|
channel_id: str,
|
|
thread_ts: str,
|
|
text: str,
|
|
*,
|
|
unfurl_links: bool = True,
|
|
unfurl_media: bool = True,
|
|
) -> str | None:
|
|
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
|
return "1.1"
|
|
|
|
monkeypatch.setattr(
|
|
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
|
)
|
|
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: "https://smith/x")
|
|
|
|
asyncio.run(post_slack_trace_reply("C123", "1.0", "thread-id"))
|
|
|
|
assert len(posted) == 1
|
|
text = posted[0]["text"]
|
|
head, _, tip_line = text.partition("\n")
|
|
assert head == "<https://smith/x|View trace>"
|
|
assert tip_line.startswith("_Tip: ") and tip_line.endswith("_")
|
|
assert any(tip in tip_line for tip in TRACE_REPLY_TIPS)
|
|
assert posted[0]["unfurl_links"] is False
|
|
assert posted[0]["unfurl_media"] is False
|
|
|
|
|
|
def test_select_slack_context_messages_detects_username_mention() -> None:
|
|
selected, mode = select_slack_context_messages(
|
|
[
|
|
{"ts": "1.0", "text": "@open-swe first request", "user": "U1"},
|
|
{"ts": "2.0", "text": "follow up", "user": "U2"},
|
|
{"ts": "3.0", "text": "@open-swe second request", "user": "U3"},
|
|
],
|
|
"3.0",
|
|
bot_user_id="UBOT",
|
|
bot_username="open-swe",
|
|
)
|
|
|
|
assert mode == "last_mention"
|
|
assert [item["ts"] for item in selected] == ["1.0", "2.0", "3.0"]
|
|
|
|
|
|
def test_get_slack_repo_config_uses_existing_thread_repo(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
threads_client = _FakeThreadsClient(
|
|
thread={"metadata": {"repo": {"owner": "saved-owner", "name": "saved-repo"}}}
|
|
)
|
|
|
|
posted = False
|
|
|
|
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, text: str) -> bool:
|
|
nonlocal posted
|
|
posted = True
|
|
return True
|
|
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client))
|
|
monkeypatch.setattr(
|
|
webapp, "post_slack_thread_reply", fake_post_slack_thread_reply, raising=False
|
|
)
|
|
|
|
repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234"))
|
|
|
|
assert repo == {"owner": "saved-owner", "name": "saved-repo"}
|
|
assert threads_client.requested_thread_id == generate_thread_id_from_slack_thread(
|
|
"C123", "1.234"
|
|
)
|
|
assert not posted
|
|
|
|
|
|
def test_get_slack_repo_config_new_thread_uses_default(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
threads_client = _FakeThreadsClient(raise_not_found=True)
|
|
monkeypatch.setattr(webapp, "SLACK_REPO_OWNER", "default-owner")
|
|
monkeypatch.setattr(webapp, "SLACK_REPO_NAME", "default-repo")
|
|
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client))
|
|
|
|
repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234"))
|
|
|
|
assert repo == {"owner": "default-owner", "name": "default-repo"}
|
|
|
|
|
|
def test_get_slack_repo_config_existing_thread_without_repo_uses_default(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
threads_client = _FakeThreadsClient(thread={"metadata": {}})
|
|
monkeypatch.setattr(webapp, "SLACK_REPO_OWNER", "default-owner")
|
|
monkeypatch.setattr(webapp, "SLACK_REPO_NAME", "default-repo")
|
|
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client))
|
|
|
|
repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234"))
|
|
|
|
assert repo == {"owner": "default-owner", "name": "default-repo"}
|
|
assert threads_client.requested_thread_id == generate_thread_id_from_slack_thread(
|
|
"C123", "1.234"
|
|
)
|
|
|
|
|
|
def test_get_slack_repo_config_ignores_repo_syntax_in_message(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
threads_client = _FakeThreadsClient(
|
|
thread={"metadata": {"repo": {"owner": "saved-owner", "name": "saved-repo"}}}
|
|
)
|
|
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client))
|
|
|
|
repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234"))
|
|
|
|
assert repo == {"owner": "saved-owner", "name": "saved-repo"}
|
|
|
|
|
|
def test_get_slack_repo_config_applies_profile_default_repo(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
threads_client = _FakeThreadsClient(thread={"metadata": {}})
|
|
|
|
async def fake_get_slack_user_info(user_id: str) -> dict:
|
|
return {"profile": {"email": "mason@example.com"}}
|
|
|
|
def fake_resolve_login_from_email(email: str | None) -> str | None:
|
|
return "mason"
|
|
|
|
async def fake_get_profile_default_repo(login: str | None) -> dict[str, str] | None:
|
|
assert login == "mason"
|
|
return {"owner": "profile-owner", "name": "profile-repo"}
|
|
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client))
|
|
monkeypatch.setattr(webapp, "get_slack_user_info", fake_get_slack_user_info)
|
|
monkeypatch.setattr(webapp, "resolve_login_from_email", fake_resolve_login_from_email)
|
|
monkeypatch.setattr(webapp, "get_profile_default_repo", fake_get_profile_default_repo)
|
|
|
|
repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234", slack_user_id="U123"))
|
|
|
|
assert repo == {"owner": "profile-owner", "name": "profile-repo"}
|
|
|
|
|
|
def _setup_slack_mention_fakes(
|
|
monkeypatch: pytest.MonkeyPatch, captured: dict[str, object]
|
|
) -> None:
|
|
async def fake_get_slack_user_info(user_id: str) -> dict:
|
|
return {
|
|
"profile": {
|
|
"email": "mason@example.com",
|
|
"display_name": "Mason",
|
|
}
|
|
}
|
|
|
|
async def fake_fetch_slack_thread_messages(channel_id: str, thread_ts: str) -> list[dict]:
|
|
captured["fetch_thread"] = {"channel_id": channel_id, "thread_ts": thread_ts}
|
|
return [
|
|
{"ts": "1700000000.000100", "text": "<@UBOT> first request", "user": "U123"},
|
|
{"ts": "1700000000.000150", "text": "context", "user": "U456"},
|
|
{
|
|
"ts": "1700000000.000200",
|
|
"text": "<@UBOT> continue on the branch",
|
|
"user": "U123",
|
|
},
|
|
]
|
|
|
|
async def fake_get_slack_user_names(user_ids: list[str]) -> dict[str, str]:
|
|
captured["user_ids"] = user_ids
|
|
return {"U123": "Mason", "U456": "Teammate"}
|
|
|
|
async def fake_resolve_slack_links_in_context(
|
|
context_messages: list[dict], user_names_by_id: dict[str, str]
|
|
) -> tuple[str, list[str]]:
|
|
captured["context_messages"] = context_messages
|
|
captured["user_names_by_id"] = user_names_by_id
|
|
return "", []
|
|
|
|
async def fake_is_thread_active(thread_id: str) -> bool:
|
|
captured["active_thread_id"] = thread_id
|
|
return False
|
|
|
|
async def fake_post_slack_trace_reply(channel_id: str, thread_ts: str, thread_id: str) -> None:
|
|
captured["trace_reply"] = {
|
|
"channel_id": channel_id,
|
|
"thread_ts": thread_ts,
|
|
"thread_id": thread_id,
|
|
}
|
|
|
|
class _FakeRunsClient:
|
|
async def create(self, thread_id: str, graph: str, **kwargs) -> dict[str, str]:
|
|
captured["run_create"] = {
|
|
"thread_id": thread_id,
|
|
"graph": graph,
|
|
"kwargs": kwargs,
|
|
}
|
|
return {"run_id": "run-123"}
|
|
|
|
class _FakeThreadsClientForProcess:
|
|
async def update(self, *, thread_id: str, metadata: dict) -> None:
|
|
captured["metadata_update"] = {"thread_id": thread_id, "metadata": metadata}
|
|
|
|
class _FakeLangGraphClientForProcess:
|
|
runs = _FakeRunsClient()
|
|
threads = _FakeThreadsClientForProcess()
|
|
|
|
monkeypatch.setattr(webapp, "SLACK_BOT_USERNAME", "open-swe")
|
|
monkeypatch.setattr(webapp, "get_slack_user_info", fake_get_slack_user_info)
|
|
monkeypatch.setattr(webapp, "fetch_slack_thread_messages", fake_fetch_slack_thread_messages)
|
|
monkeypatch.setattr(webapp, "get_slack_user_names", fake_get_slack_user_names)
|
|
monkeypatch.setattr(
|
|
webapp, "resolve_slack_links_in_context", fake_resolve_slack_links_in_context
|
|
)
|
|
monkeypatch.setattr(webapp, "is_thread_active", fake_is_thread_active)
|
|
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClientForProcess())
|
|
|
|
|
|
def test_process_slack_mention_creates_thread_first_run_with_trace_reply(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
_setup_slack_mention_fakes(monkeypatch, captured)
|
|
|
|
async def fake_thread_exists(thread_id: str) -> bool:
|
|
captured["thread_exists_check"] = thread_id
|
|
return False
|
|
|
|
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
|
|
|
|
thread_ts = "1700000000.000100"
|
|
event_ts = "1700000000.000200"
|
|
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
|
|
|
|
asyncio.run(
|
|
webapp.process_slack_mention(
|
|
{
|
|
"channel_id": "C123",
|
|
"thread_ts": thread_ts,
|
|
"event_ts": event_ts,
|
|
"user_id": "U123",
|
|
"text": "<@UBOT> continue on the branch",
|
|
"bot_user_id": "UBOT",
|
|
},
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
)
|
|
|
|
assert captured["thread_exists_check"] == expected_thread_id
|
|
assert captured["fetch_thread"] == {"channel_id": "C123", "thread_ts": thread_ts}
|
|
assert captured["active_thread_id"] == expected_thread_id
|
|
assert captured["metadata_update"] == {
|
|
"thread_id": expected_thread_id,
|
|
"metadata": {"repo": {"owner": "langchain-ai", "name": "open-swe"}},
|
|
}
|
|
assert captured["trace_reply"] == {
|
|
"channel_id": "C123",
|
|
"thread_ts": thread_ts,
|
|
"thread_id": expected_thread_id,
|
|
}
|
|
|
|
run_create = captured["run_create"]
|
|
assert isinstance(run_create, dict)
|
|
assert run_create["thread_id"] == expected_thread_id
|
|
assert run_create["graph"] == "agent"
|
|
kwargs = run_create["kwargs"]
|
|
assert kwargs["if_not_exists"] == "create"
|
|
assert "multitask_strategy" not in kwargs
|
|
assert kwargs["config"]["configurable"]["slack_thread"]["thread_ts"] == thread_ts
|
|
prompt_block = kwargs["input"]["messages"][0]["content"][0]
|
|
assert "## Default Repository Hint\nlangchain-ai/open-swe" in prompt_block["text"]
|
|
assert (
|
|
"Use this only if the Slack conversation does not identify a different repository."
|
|
in (prompt_block["text"])
|
|
)
|
|
assert prompt_block["text"].count("## Slack Thread") == 1
|
|
assert f"Thread TS: {thread_ts}" in prompt_block["text"]
|
|
assert "## Latest Mention Request\ncontinue on the branch" in prompt_block["text"]
|
|
|
|
|
|
def test_process_slack_mention_skips_trace_reply_on_followup_mention(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Subsequent mentions in a Slack thread should not post 'Working on it!'."""
|
|
captured: dict[str, object] = {}
|
|
_setup_slack_mention_fakes(monkeypatch, captured)
|
|
|
|
async def fake_thread_exists(thread_id: str) -> bool:
|
|
captured["thread_exists_check"] = thread_id
|
|
return True
|
|
|
|
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
|
|
|
|
thread_ts = "1700000000.000100"
|
|
event_ts = "1700000000.000300"
|
|
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
|
|
|
|
asyncio.run(
|
|
webapp.process_slack_mention(
|
|
{
|
|
"channel_id": "C123",
|
|
"thread_ts": thread_ts,
|
|
"event_ts": event_ts,
|
|
"user_id": "U123",
|
|
"text": "<@UBOT> follow up question",
|
|
"bot_user_id": "UBOT",
|
|
},
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
)
|
|
|
|
assert captured["thread_exists_check"] == expected_thread_id
|
|
assert "trace_reply" not in captured
|
|
run_create = captured["run_create"]
|
|
assert isinstance(run_create, dict)
|
|
assert run_create["thread_id"] == expected_thread_id
|
|
|
|
|
|
def test_process_slack_mention_queues_active_thread_message(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
|
|
async def fake_get_slack_user_info(user_id: str) -> dict:
|
|
return {
|
|
"profile": {
|
|
"email": "mason@example.com",
|
|
"display_name": "Mason",
|
|
}
|
|
}
|
|
|
|
async def fake_fetch_slack_thread_messages(channel_id: str, thread_ts: str) -> list[dict]:
|
|
return [
|
|
{"ts": "1700000000.000100", "text": "<@UBOT> first request", "user": "U123"},
|
|
{
|
|
"ts": "1700000000.000200",
|
|
"text": "<@UBOT> include this screenshot https://example.com/image.png",
|
|
"user": "U123",
|
|
},
|
|
]
|
|
|
|
async def fake_get_slack_user_names(user_ids: list[str]) -> dict[str, str]:
|
|
captured["user_ids"] = user_ids
|
|
return {"U123": "Mason"}
|
|
|
|
async def fake_resolve_slack_links_in_context(
|
|
context_messages: list[dict], user_names_by_id: dict[str, str]
|
|
) -> tuple[str, list[str]]:
|
|
captured["context_messages"] = context_messages
|
|
return "", []
|
|
|
|
async def fake_fetch_image_block(image_url: str, http_client: object) -> None:
|
|
captured["image_url"] = image_url
|
|
return None
|
|
|
|
async def fake_is_thread_active(thread_id: str) -> bool:
|
|
captured["active_thread_id"] = thread_id
|
|
return True
|
|
|
|
async def fake_queue_message_for_thread(thread_id: str, message_content: object) -> bool:
|
|
captured["queued"] = {"thread_id": thread_id, "message_content": message_content}
|
|
return True
|
|
|
|
async def fake_post_slack_trace_reply(*args, **kwargs) -> None:
|
|
raise AssertionError("trace reply should not be posted for queued mid-run Slack messages")
|
|
|
|
async def fake_thread_exists(thread_id: str) -> bool:
|
|
return True
|
|
|
|
class _FakeRunsClient:
|
|
async def create(self, *args, **kwargs) -> None:
|
|
raise AssertionError("run should not be created for active Slack threads")
|
|
|
|
class _FakeThreadsClientForProcess:
|
|
async def update(self, *, thread_id: str, metadata: dict) -> None:
|
|
captured["metadata_update"] = {"thread_id": thread_id, "metadata": metadata}
|
|
|
|
class _FakeLangGraphClientForProcess:
|
|
runs = _FakeRunsClient()
|
|
threads = _FakeThreadsClientForProcess()
|
|
|
|
monkeypatch.setattr(webapp, "SLACK_BOT_USERNAME", "open-swe")
|
|
monkeypatch.setattr(webapp, "get_slack_user_info", fake_get_slack_user_info)
|
|
monkeypatch.setattr(webapp, "fetch_slack_thread_messages", fake_fetch_slack_thread_messages)
|
|
monkeypatch.setattr(webapp, "get_slack_user_names", fake_get_slack_user_names)
|
|
monkeypatch.setattr(
|
|
webapp, "resolve_slack_links_in_context", fake_resolve_slack_links_in_context
|
|
)
|
|
monkeypatch.setattr(webapp, "fetch_image_block", fake_fetch_image_block)
|
|
monkeypatch.setattr(webapp, "is_thread_active", fake_is_thread_active)
|
|
monkeypatch.setattr(webapp, "queue_message_for_thread", fake_queue_message_for_thread)
|
|
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
|
|
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
|
|
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClientForProcess())
|
|
|
|
thread_ts = "1700000000.000100"
|
|
event_ts = "1700000000.000200"
|
|
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
|
|
|
|
asyncio.run(
|
|
webapp.process_slack_mention(
|
|
{
|
|
"channel_id": "C123",
|
|
"thread_ts": thread_ts,
|
|
"event_ts": event_ts,
|
|
"user_id": "U123",
|
|
"text": "<@UBOT> include this screenshot https://example.com/image.png",
|
|
"bot_user_id": "UBOT",
|
|
},
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
)
|
|
|
|
assert captured["active_thread_id"] == expected_thread_id
|
|
assert captured["queued"]["thread_id"] == expected_thread_id
|
|
queued_payload = captured["queued"]["message_content"]
|
|
assert queued_payload["image_urls"] == ["https://example.com/image.png"]
|
|
assert "## Latest Mention Request\ninclude this screenshot" in queued_payload["text"]
|