open-swe/tests/slack/test_slack_feedback.py
Adam Moussa ae1f883b4c
refactor: move tests into tests/<domain>/ layout
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.
2026-07-17 14:42:45 -04:00

260 lines
9.1 KiB
Python

import json
from typing import Any
import pytest
from agent.utils import slack_feedback
from agent.utils.slack_feedback import (
process_slack_reaction_added,
process_slack_reaction_removed,
)
from agent.webhooks import common as webhook_common
from agent.webhooks import slack_routes
class _FakeStore:
def __init__(self) -> None:
self.items: dict[tuple[tuple[str, ...], str], dict[str, Any]] = {}
async def get_item(self, namespace: tuple[str, ...], key: str) -> dict[str, Any] | None:
return self.items.get((namespace, key))
async def put_item(self, namespace: tuple[str, ...], key: str, value: dict[str, Any]) -> None:
self.items[(namespace, key)] = {"value": value}
class _FakeClient:
def __init__(self) -> None:
self.store = _FakeStore()
class _FakeBackgroundTasks:
def __init__(self) -> None:
self.tasks: list[tuple[Any, tuple[Any, ...]]] = []
def add_task(self, func: Any, *args: Any) -> None:
self.tasks.append((func, args))
class _FakeRequest:
def __init__(self, payload: dict[str, Any]) -> None:
self.headers: dict[str, str] = {}
self._body = json.dumps(payload).encode()
async def body(self) -> bytes:
return self._body
def _store_message_mapping(
client: _FakeClient,
channel_id: str,
message_ts: str,
*,
triggering_user_id: str | None = "U123",
) -> None:
value: dict[str, Any] = {"run_id": "run-1", "thread_ts": "1.000"}
if triggering_user_id:
value["triggering_user_id"] = triggering_user_id
client.store.items[(("slack_run_map", channel_id), f"message:{message_ts}")] = {"value": value}
def _reaction_event(reaction: str = "thumbsup") -> dict[str, Any]:
return {
"type": "reaction_added",
"reaction": reaction,
"user": "U123",
"item": {"type": "message", "channel": "C123", "ts": "2.000"},
}
@pytest.mark.asyncio
async def test_reaction_added_creates_feedback(monkeypatch: pytest.MonkeyPatch) -> None:
client = _FakeClient()
_store_message_mapping(client, "C123", "2.000")
created: dict[str, Any] = {}
def fake_create_feedback(
run_id: str,
key: str,
*,
score: float,
comment: str | None = None,
source_info: dict[str, Any] | None = None,
) -> bool:
created.update(
{
"run_id": run_id,
"key": key,
"score": score,
"comment": comment,
"source_info": source_info,
}
)
return True
monkeypatch.setattr(slack_feedback, "get_client", lambda url: client)
monkeypatch.setattr(slack_feedback, "create_langsmith_feedback", fake_create_feedback)
await process_slack_reaction_added(_reaction_event(), event_id="Ev1")
assert created["run_id"] == "run-1"
assert created["key"] == "slack_reaction:C123:U123:2.000"
assert created["score"] == 1.0
assert created["source_info"]["reactions"] == ["thumbsup"]
assert (("slack_reaction_events", "C123"), "Ev1") in client.store.items
@pytest.mark.asyncio
async def test_reaction_added_skips_duplicate_event(monkeypatch: pytest.MonkeyPatch) -> None:
client = _FakeClient()
_store_message_mapping(client, "C123", "2.000")
client.store.items[(("slack_reaction_events", "C123"), "Ev1")] = {"value": {"event_id": "Ev1"}}
def fail_create_feedback(*args: Any, **kwargs: Any) -> bool:
raise AssertionError("duplicate event should not create feedback")
monkeypatch.setattr(slack_feedback, "get_client", lambda url: client)
monkeypatch.setattr(slack_feedback, "create_langsmith_feedback", fail_create_feedback)
await process_slack_reaction_added(_reaction_event(), event_id="Ev1")
@pytest.mark.asyncio
async def test_reaction_removed_deletes_feedback_when_last_reaction_removed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client = _FakeClient()
_store_message_mapping(client, "C123", "2.000")
client.store.items[(("slack_reaction_state", "C123"), "run-1:U123:2.000")] = {
"value": {
"run_id": "run-1",
"user_id": "U123",
"message_ts": "2.000",
"reactions": ["thumbsup"],
}
}
deleted: dict[str, str] = {}
def fake_delete_feedback(run_id: str, key: str) -> bool:
deleted["run_id"] = run_id
deleted["key"] = key
return True
monkeypatch.setattr(slack_feedback, "get_client", lambda url: client)
monkeypatch.setattr(slack_feedback, "delete_langsmith_feedback", fake_delete_feedback)
await process_slack_reaction_removed(_reaction_event(), event_id="Ev2")
assert deleted == {"run_id": "run-1", "key": "slack_reaction:C123:U123:2.000"}
state = client.store.items[(("slack_reaction_state", "C123"), "run-1:U123:2.000")]
assert state["value"]["reactions"] == []
@pytest.mark.asyncio
async def test_reaction_without_message_mapping_is_ignored(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client = _FakeClient()
def fail_create_feedback(*args: Any, **kwargs: Any) -> bool:
raise AssertionError("unmapped message should not create feedback")
monkeypatch.setattr(slack_feedback, "get_client", lambda url: client)
monkeypatch.setattr(slack_feedback, "create_langsmith_feedback", fail_create_feedback)
await process_slack_reaction_added(_reaction_event(), event_id="Ev1")
@pytest.mark.asyncio
async def test_reaction_from_non_triggering_user_is_ignored(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client = _FakeClient()
_store_message_mapping(client, "C123", "2.000", triggering_user_id="UTRIGGER")
def fail_create_feedback(*args: Any, **kwargs: Any) -> bool:
raise AssertionError("non-triggering user should not create feedback")
monkeypatch.setattr(slack_feedback, "get_client", lambda url: client)
monkeypatch.setattr(slack_feedback, "create_langsmith_feedback", fail_create_feedback)
await process_slack_reaction_added(_reaction_event(), event_id="Ev1")
@pytest.mark.asyncio
async def test_conflicting_reactions_clear_feedback(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client = _FakeClient()
_store_message_mapping(client, "C123", "2.000")
client.store.items[(("slack_reaction_state", "C123"), "run-1:U123:2.000")] = {
"value": {
"run_id": "run-1",
"user_id": "U123",
"message_ts": "2.000",
"reactions": ["thumbsup"],
}
}
deleted: dict[str, str] = {}
def fail_create_feedback(*args: Any, **kwargs: Any) -> bool:
raise AssertionError("conflicting reactions must not record a numeric score")
def fake_delete_feedback(run_id: str, key: str) -> bool:
deleted["run_id"] = run_id
deleted["key"] = key
return True
monkeypatch.setattr(slack_feedback, "get_client", lambda url: client)
monkeypatch.setattr(slack_feedback, "create_langsmith_feedback", fail_create_feedback)
monkeypatch.setattr(slack_feedback, "delete_langsmith_feedback", fake_delete_feedback)
# User adds a thumbsdown alongside the existing thumbsup → conflicting.
event = {**_reaction_event("thumbsdown"), "type": "reaction_added"}
await process_slack_reaction_added(event, event_id="EvConflict")
assert deleted == {"run_id": "run-1", "key": "slack_reaction:C123:U123:2.000"}
@pytest.mark.asyncio
async def test_slack_webhook_queues_reaction_added(monkeypatch: pytest.MonkeyPatch) -> None:
event = _reaction_event("+1")
payload = {"type": "event_callback", "event_id": "Ev1", "event": event}
background_tasks = _FakeBackgroundTasks()
monkeypatch.setattr(webhook_common, "verify_slack_signature", lambda **kwargs: True)
response = await slack_routes.slack_webhook(_FakeRequest(payload), background_tasks)
assert response == {"status": "accepted", "message": "Reaction feedback queued"}
assert background_tasks.tasks == [(webhook_common.process_slack_reaction_added, (event, "Ev1"))]
@pytest.mark.asyncio
async def test_slack_webhook_queues_reaction_removed(monkeypatch: pytest.MonkeyPatch) -> None:
event = {**_reaction_event("-1"), "type": "reaction_removed"}
payload = {"type": "event_callback", "event_id": "Ev2", "event": event}
background_tasks = _FakeBackgroundTasks()
monkeypatch.setattr(webhook_common, "verify_slack_signature", lambda **kwargs: True)
response = await slack_routes.slack_webhook(_FakeRequest(payload), background_tasks)
assert response == {"status": "accepted", "message": "Reaction removal queued"}
assert background_tasks.tasks == [
(webhook_common.process_slack_reaction_removed, (event, "Ev2"))
]
@pytest.mark.asyncio
async def test_slack_webhook_ignores_untracked_reaction(monkeypatch: pytest.MonkeyPatch) -> None:
event = _reaction_event("eyes")
payload = {"type": "event_callback", "event_id": "Ev3", "event": event}
background_tasks = _FakeBackgroundTasks()
monkeypatch.setattr(webhook_common, "verify_slack_signature", lambda **kwargs: True)
response = await slack_routes.slack_webhook(_FakeRequest(payload), background_tasks)
assert response == {"status": "ignored", "reason": "Reaction not tracked for feedback"}
assert background_tasks.tasks == []