mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
* feat: add GitHub PR comment trigger and reply support * refactor: improve readability of GitHub integration * linting * fix: resolve github token from thread metadata and improve PR trigger flow * fix: fall back to OAuth for GitHub webhook when no token in thread metadata * give me commit message github integeration working without a breaking * auth.py refactor * fix: validate cached GitHub token before use to handle expiry * liniting * ci unitest formatting * feat: post PR comments as GitHub App bot instead of user OAuth token * resolved comments * slack resolveed comments * Refactor docstring and comments in get_slack_repo_config Removed unnecessary comments and cleaned up docstring formatting. * cr * cr * cr * cr --------- Co-authored-by: bracesproul <braceasproul@gmail.com>
290 lines
10 KiB
Python
290 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from agent import webapp
|
|
from agent.utils import github_comments
|
|
|
|
|
|
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", "")
|
|
|
|
client = TestClient(webapp.app)
|
|
response = client.post(
|
|
"/webhooks/github",
|
|
headers={"X-GitHub-Event": "issues"},
|
|
json={
|
|
"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", "")
|
|
|
|
client = TestClient(webapp.app)
|
|
response = client.post(
|
|
"/webhooks/github",
|
|
headers={"X-GitHub-Event": "issues"},
|
|
json={
|
|
"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", "")
|
|
|
|
client = TestClient(webapp.app)
|
|
response = client.post(
|
|
"/webhooks/github",
|
|
headers={"X-GitHub-Event": "issue_comment"},
|
|
json={
|
|
"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"]
|