from __future__ import annotations import json from typing import Any import pytest from agent import webapp from agent.utils import github_feedback from agent.utils.github_feedback import ( process_github_reaction_added, process_github_reaction_removed, ) 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} async def delete_item(self, namespace: tuple[str, ...], key: str) -> None: self.items.pop((namespace, key), None) 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 = { "X-GitHub-Event": "reaction", "X-GitHub-Delivery": "delivery-1", "X-Hub-Signature-256": "sig", } self._body = json.dumps(payload).encode() async def body(self) -> bytes: return self._body def _reaction_payload(content: str = "+1", action: str = "created") -> dict[str, Any]: return { "action": action, "reaction": {"content": content}, "repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"}, "pull_request": {"number": 7}, "comment": {"id": 123, "pull_request_url": "https://api.github.com/repos/o/r/pulls/7"}, "sender": {"login": "reviewer"}, } @pytest.mark.asyncio async def test_github_reaction_added_creates_langsmith_feedback( monkeypatch: pytest.MonkeyPatch, ) -> None: client = _FakeClient() 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(github_feedback, "get_client", lambda url: client) async def fake_list_findings(thread_id: str) -> list[dict[str, Any]]: return [ { "id": "f1", "github_review_comment_id": 123, "github_review_run_id": "run-1", } ] monkeypatch.setattr( github_feedback, "list_findings", fake_list_findings, ) monkeypatch.setattr(github_feedback, "create_langsmith_feedback", fake_create_feedback) await process_github_reaction_added(_reaction_payload(), delivery_id="delivery-1") assert created["run_id"] == "run-1" assert created["key"] == "github_reaction:langchain-ai/open-swe:reviewer:123" assert created["score"] == 1.0 assert created["source_info"]["finding_id"] == "f1" assert (("github_reaction_events", "langchain-ai/open-swe"), "delivery-1") in client.store.items @pytest.mark.asyncio async def test_github_reaction_removed_deletes_langsmith_feedback( monkeypatch: pytest.MonkeyPatch, ) -> None: client = _FakeClient() client.store.items[ (("github_reaction_state", "langchain-ai/open-swe"), "run-1:reviewer:123") ] = { "value": { "run_id": "run-1", "user_login": "reviewer", "comment_id": 123, "reactions": ["+1"], } } 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(github_feedback, "get_client", lambda url: client) async def fake_list_findings(thread_id: str) -> list[dict[str, Any]]: return [ { "id": "f1", "github_review_comment_id": 123, "github_review_run_id": "run-1", } ] monkeypatch.setattr( github_feedback, "list_findings", fake_list_findings, ) monkeypatch.setattr(github_feedback, "delete_langsmith_feedback", fake_delete_feedback) await process_github_reaction_removed( _reaction_payload(action="deleted"), delivery_id="delivery-2" ) assert deleted == { "run_id": "run-1", "key": "github_reaction:langchain-ai/open-swe:reviewer:123", } assert ( ("github_reaction_state", "langchain-ai/open-swe"), "run-1:reviewer:123", ) not in client.store.items @pytest.mark.asyncio async def test_github_webhook_ignores_reaction_event(monkeypatch: pytest.MonkeyPatch) -> None: payload = _reaction_payload() background_tasks = _FakeBackgroundTasks() monkeypatch.setattr(webapp, "verify_github_signature", lambda *args, **kwargs: True) response = await webapp.github_webhook(_FakeRequest(payload), background_tasks) assert response == {"status": "ignored", "reason": "Unsupported event type: reaction"} assert background_tasks.tasks == []