galaxis-agent/tests/test_github_issue_webhook.py
2026-03-20 14:38:07 +09:00

316 lines
11 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import hmac
import json
from fastapi.testclient import TestClient
from agent import webapp
from agent.utils import github_comments
_TEST_WEBHOOK_SECRET = "test-secret-for-webhook"
def _sign_body(body: bytes, secret: str = _TEST_WEBHOOK_SECRET) -> str:
"""Compute the X-Hub-Signature-256 header value for raw bytes."""
sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return f"sha256={sig}"
def _post_github_webhook(client: TestClient, event_type: str, payload: dict) -> object:
"""Send a signed GitHub webhook POST request."""
body = json.dumps(payload, separators=(",", ":")).encode()
return client.post(
"/webhooks/github",
content=body,
headers={
"X-GitHub-Event": event_type,
"X-Hub-Signature-256": _sign_body(body),
"Content-Type": "application/json",
},
)
def test_generate_thread_id_from_github_issue_is_deterministic() -> None:
first = webapp.generate_thread_id_from_github_issue("12345")
second = webapp.generate_thread_id_from_github_issue("12345")
assert first == second
assert len(first) == 36
def test_build_github_issue_prompt_includes_issue_context() -> None:
prompt = webapp.build_github_issue_prompt(
{"owner": "langchain-ai", "name": "open-swe"},
42,
"12345",
"Fix the flaky test",
"The test is failing intermittently.",
[{"author": "octocat", "body": "Please take a look", "created_at": "2026-03-09T00:00:00Z"}],
github_login="octocat",
)
assert "Fix the flaky test" in prompt
assert "The test is failing intermittently." in prompt
assert "Please take a look" in prompt
assert "github_comment" in prompt
def test_build_github_issue_followup_prompt_only_includes_comment() -> None:
prompt = webapp.build_github_issue_followup_prompt("bracesproul", "Please handle this")
assert prompt == "**bracesproul:**\nPlease handle this"
assert "## Repository" not in prompt
assert "## Title" not in prompt
def test_github_webhook_accepts_issue_events(monkeypatch) -> None:
called: dict[str, object] = {}
async def fake_process_github_issue(payload: dict[str, object], event_type: str) -> None:
called["payload"] = payload
called["event_type"] = event_type
monkeypatch.setattr(webapp, "process_github_issue", fake_process_github_issue)
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
client = TestClient(webapp.app)
response = _post_github_webhook(
client,
"issues",
{
"action": "opened",
"issue": {
"id": 12345,
"number": 42,
"title": "@openswe fix the flaky test",
"body": "The test is failing intermittently.",
},
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
"sender": {"login": "octocat"},
},
)
assert response.status_code == 200
assert response.json()["status"] == "accepted"
assert called["event_type"] == "issues"
def test_github_webhook_ignores_issue_events_without_body_or_title_change(monkeypatch) -> None:
called = False
async def fake_process_github_issue(payload: dict[str, object], event_type: str) -> None:
nonlocal called
called = True
monkeypatch.setattr(webapp, "process_github_issue", fake_process_github_issue)
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
client = TestClient(webapp.app)
response = _post_github_webhook(
client,
"issues",
{
"action": "edited",
"changes": {"labels": {"from": []}},
"issue": {
"id": 12345,
"number": 42,
"title": "@openswe fix the flaky test",
"body": "The test is failing intermittently.",
},
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
"sender": {"login": "octocat"},
},
)
assert response.status_code == 200
assert response.json()["status"] == "ignored"
assert called is False
def test_github_webhook_accepts_issue_comment_events(monkeypatch) -> None:
called: dict[str, object] = {}
async def fake_process_github_issue(payload: dict[str, object], event_type: str) -> None:
called["payload"] = payload
called["event_type"] = event_type
monkeypatch.setattr(webapp, "process_github_issue", fake_process_github_issue)
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
client = TestClient(webapp.app)
response = _post_github_webhook(
client,
"issue_comment",
{
"issue": {"id": 12345, "number": 42, "title": "Fix the flaky test"},
"comment": {"body": "@openswe please handle this"},
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
"sender": {"login": "octocat"},
},
)
assert response.status_code == 200
assert response.json()["status"] == "accepted"
assert called["event_type"] == "issue_comment"
def test_process_github_issue_uses_resolved_user_token_for_reaction(monkeypatch) -> None:
captured: dict[str, object] = {}
async def fake_get_or_resolve_thread_github_token(thread_id: str, email: str) -> str | None:
captured["thread_id"] = thread_id
captured["email"] = email
return "user-token"
async def fake_get_github_app_installation_token() -> str | None:
return None
async def fake_react_to_github_comment(
repo_config: dict[str, str],
comment_id: int,
*,
event_type: str,
token: str,
pull_number: int | None = None,
node_id: str | None = None,
) -> bool:
captured["reaction_token"] = token
captured["comment_id"] = comment_id
return True
async def fake_fetch_issue_comments(
repo_config: dict[str, str], issue_number: int, *, token: str | None = None
) -> list[dict[str, object]]:
captured["fetch_token"] = token
return []
async def fake_is_thread_active(thread_id: str) -> bool:
return False
class _FakeRunsClient:
async def create(self, *args, **kwargs) -> None:
captured["run_created"] = True
class _FakeLangGraphClient:
runs = _FakeRunsClient()
monkeypatch.setattr(
webapp, "_get_or_resolve_thread_github_token", fake_get_or_resolve_thread_github_token
)
monkeypatch.setattr(
webapp, "get_github_app_installation_token", fake_get_github_app_installation_token
)
monkeypatch.setattr(webapp, "_thread_exists", lambda thread_id: asyncio.sleep(0, result=False))
monkeypatch.setattr(webapp, "react_to_github_comment", fake_react_to_github_comment)
monkeypatch.setattr(webapp, "fetch_issue_comments", fake_fetch_issue_comments)
monkeypatch.setattr(webapp, "is_thread_active", fake_is_thread_active)
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClient())
monkeypatch.setattr(webapp, "GITHUB_USER_EMAIL_MAP", {"octocat": "octocat@example.com"})
asyncio.run(
webapp.process_github_issue(
{
"issue": {
"id": 12345,
"number": 42,
"title": "Fix the flaky test",
"body": "The test is failing intermittently.",
"html_url": "https://github.com/langchain-ai/open-swe/issues/42",
},
"comment": {"id": 999, "body": "@openswe please handle this"},
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
"sender": {"login": "octocat"},
},
"issue_comment",
)
)
assert captured["reaction_token"] == "user-token"
assert captured["fetch_token"] == "user-token"
assert captured["comment_id"] == 999
assert captured["run_created"] is True
def test_process_github_issue_existing_thread_uses_followup_prompt(monkeypatch) -> None:
captured: dict[str, object] = {}
async def fake_get_or_resolve_thread_github_token(thread_id: str, email: str) -> str | None:
return "user-token"
async def fake_get_github_app_installation_token() -> str | None:
return None
async def fake_react_to_github_comment(
repo_config: dict[str, str],
comment_id: int,
*,
event_type: str,
token: str,
pull_number: int | None = None,
node_id: str | None = None,
) -> bool:
return True
async def fake_fetch_issue_comments(
repo_config: dict[str, str], issue_number: int, *, token: str | None = None
) -> list[dict[str, object]]:
raise AssertionError("fetch_issue_comments should not be called for follow-up prompts")
async def fake_thread_exists(thread_id: str) -> bool:
return True
async def fake_is_thread_active(thread_id: str) -> bool:
return False
class _FakeRunsClient:
async def create(self, *args, **kwargs) -> None:
captured["prompt"] = kwargs["input"]["messages"][0]["content"]
class _FakeLangGraphClient:
runs = _FakeRunsClient()
monkeypatch.setattr(
webapp, "_get_or_resolve_thread_github_token", fake_get_or_resolve_thread_github_token
)
monkeypatch.setattr(
webapp, "get_github_app_installation_token", fake_get_github_app_installation_token
)
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
monkeypatch.setattr(webapp, "react_to_github_comment", fake_react_to_github_comment)
monkeypatch.setattr(webapp, "fetch_issue_comments", fake_fetch_issue_comments)
monkeypatch.setattr(webapp, "is_thread_active", fake_is_thread_active)
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClient())
monkeypatch.setattr(webapp, "GITHUB_USER_EMAIL_MAP", {"octocat": "octocat@example.com"})
monkeypatch.setattr(
github_comments, "GITHUB_USER_EMAIL_MAP", {"octocat": "octocat@example.com"}
)
asyncio.run(
webapp.process_github_issue(
{
"issue": {
"id": 12345,
"number": 42,
"title": "Fix the flaky test",
"body": "The test is failing intermittently.",
"html_url": "https://github.com/langchain-ai/open-swe/issues/42",
},
"comment": {
"id": 999,
"body": "@openswe please handle this",
"user": {"login": "octocat"},
},
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
"sender": {"login": "octocat"},
},
"issue_comment",
)
)
assert captured["prompt"] == "**octocat:**\n@openswe please handle this"
assert "## Repository" not in captured["prompt"]