mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
fix: remove manual review trigger surfaces (#1396)
Open SWE Review now runs from automated PR triggers, so drop the old Slack/GitHub review keyword entrypoints and keep PR comments on the regular agent path.
This commit is contained in:
parent
f4b27c0fee
commit
8913926dba
6 changed files with 22 additions and 596 deletions
|
|
@ -25,7 +25,6 @@ __all__ = [
|
|||
"fetch_pr_comments_since_last_tag",
|
||||
"format_github_comment_body_for_prompt",
|
||||
"get_thread_id_from_branch",
|
||||
"parse_github_review_command",
|
||||
"post_github_comment",
|
||||
"react_to_github_comment",
|
||||
"sanitize_github_comment_body",
|
||||
|
|
@ -33,8 +32,6 @@ __all__ = [
|
|||
]
|
||||
|
||||
OPEN_SWE_TAGS = ("@openswe", "@open-swe", "@openswe-dev")
|
||||
_OPEN_SWE_MENTION_RE = re.compile(r"(?i)@(?:openswe-dev|open-swe|openswe)\b")
|
||||
_REVIEW_COMMAND_RE = re.compile(r"(?i)\Areview(?:\s+(https?://\S+))?\s*\Z")
|
||||
UNTRUSTED_GITHUB_COMMENT_OPEN_TAG = "<dangerous-external-untrusted-users-comment>"
|
||||
UNTRUSTED_GITHUB_COMMENT_CLOSE_TAG = "</dangerous-external-untrusted-users-comment>"
|
||||
_SANITIZED_UNTRUSTED_GITHUB_COMMENT_OPEN_TAG = "[blocked-untrusted-comment-tag-open]"
|
||||
|
|
@ -76,23 +73,6 @@ def get_thread_id_from_branch(branch_name: str) -> str | None:
|
|||
return match.group(0) if match else None
|
||||
|
||||
|
||||
def parse_github_review_command(body: str) -> tuple[bool, str | None]:
|
||||
"""Detect ``@open-swe review [URL]`` in a GitHub comment body.
|
||||
|
||||
Returns ``(is_review_command, optional_pr_url)``. When ``is_review_command``
|
||||
is True and the URL is None, the command applies to the PR being commented
|
||||
on. The URL — if present — is returned as-is so the caller can validate it
|
||||
with ``parse_github_pr_url``.
|
||||
"""
|
||||
if not body:
|
||||
return False, None
|
||||
stripped = _OPEN_SWE_MENTION_RE.sub("", body).strip()
|
||||
match = _REVIEW_COMMAND_RE.match(stripped)
|
||||
if not match:
|
||||
return False, None
|
||||
return True, match.group(1) or None
|
||||
|
||||
|
||||
def sanitize_github_comment_body(body: str) -> str:
|
||||
"""Strip reserved trust wrapper tags from raw GitHub comment bodies."""
|
||||
sanitized = body.replace(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
SLACK_API_BASE_URL = "https://slack.com/api"
|
||||
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", "")
|
||||
GITHUB_PR_URL_RE = re.compile(r"https?://(?:www\.)?github\.com/[^\s<>|]+/[^\s<>|]+/pull/\d+")
|
||||
URL_RE = re.compile(r"https?://[^\s<>|]+")
|
||||
|
||||
|
||||
DEFAULT_ASSISTANT_STATUS = "is thinking…"
|
||||
|
||||
# Curated rotating loading strings shown by Slack while the indicator is active.
|
||||
|
|
@ -174,36 +170,6 @@ def parse_github_pr_url(url: str) -> GitHubPrRef | None:
|
|||
)
|
||||
|
||||
|
||||
def parse_slack_review_command(text: str) -> GitHubPrRef | None:
|
||||
stripped = text.strip()
|
||||
command_match = re.fullmatch(r"(?is)review\s+(.+)", stripped)
|
||||
if not command_match:
|
||||
return None
|
||||
|
||||
rest = command_match.group(1).strip()
|
||||
url_match = GITHUB_PR_URL_RE.search(rest)
|
||||
if not url_match:
|
||||
return None
|
||||
|
||||
trailing_text = rest[url_match.end() :].strip()
|
||||
if trailing_text and trailing_text != ">" and not trailing_text.startswith("|"):
|
||||
return None
|
||||
|
||||
return parse_github_pr_url(url_match.group(0))
|
||||
|
||||
|
||||
def looks_like_slack_pr_review_command(text: str) -> bool:
|
||||
stripped = text.strip()
|
||||
if not re.match(r"(?is)^review\b", stripped):
|
||||
return False
|
||||
for match in URL_RE.finditer(stripped):
|
||||
parsed = urlparse(match.group(0).strip("<>"))
|
||||
host = (parsed.hostname or "").lower()
|
||||
if parsed.scheme in {"http", "https"} and host in {"github.com", "www.github.com"}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def select_slack_context_messages(
|
||||
messages: list[dict[str, Any]],
|
||||
current_message_ts: str,
|
||||
|
|
|
|||
145
agent/webapp.py
145
agent/webapp.py
|
|
@ -55,7 +55,6 @@ from .utils.auth import (
|
|||
persist_encrypted_github_token,
|
||||
resolve_github_token_from_email,
|
||||
)
|
||||
from .utils.authorship import OPEN_SWE_BOT_NAME
|
||||
from .utils.comments import get_recent_comments
|
||||
from .utils.github_app import (
|
||||
get_github_app_installation_token,
|
||||
|
|
@ -70,7 +69,6 @@ from .utils.github_comments import (
|
|||
fetch_pr_comments_since_last_tag,
|
||||
format_github_comment_body_for_prompt,
|
||||
get_thread_id_from_branch,
|
||||
parse_github_review_command,
|
||||
react_to_github_comment,
|
||||
sanitize_github_comment_body,
|
||||
verify_github_signature,
|
||||
|
|
@ -88,7 +86,6 @@ from .utils.slack import (
|
|||
format_slack_messages_for_prompt,
|
||||
get_slack_user_info,
|
||||
get_slack_user_names,
|
||||
parse_github_pr_url,
|
||||
post_slack_thread_reply,
|
||||
post_slack_trace_reply,
|
||||
resolve_slack_links_in_context,
|
||||
|
|
@ -1163,33 +1160,6 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
|
|||
)
|
||||
|
||||
|
||||
async def process_slack_pr_review_request(
|
||||
pr_ref: GitHubPrRef, channel_id: str, thread_ts: str
|
||||
) -> None:
|
||||
await set_slack_assistant_status(channel_id, thread_ts)
|
||||
result = await trigger_pr_review_from_ref(
|
||||
pr_ref,
|
||||
source="slack",
|
||||
slack_channel_id=channel_id,
|
||||
slack_thread_ts=thread_ts,
|
||||
)
|
||||
if result.get("success"):
|
||||
thread_id = result.get("thread_id")
|
||||
if isinstance(thread_id, str) and thread_id:
|
||||
await post_slack_trace_reply(
|
||||
channel_id, thread_ts, thread_id, include_dashboard_link=False
|
||||
)
|
||||
await set_slack_assistant_status(channel_id, thread_ts)
|
||||
return
|
||||
|
||||
await post_slack_thread_reply(
|
||||
channel_id,
|
||||
thread_ts,
|
||||
f"Could not start review for <{pr_ref.url}|{pr_ref.owner}/{pr_ref.repo}#{pr_ref.number}>: "
|
||||
f"{result.get('error', 'unknown error')}.",
|
||||
)
|
||||
|
||||
|
||||
def verify_linear_signature(body: bytes, signature: str, secret: str) -> bool:
|
||||
"""Verify the Linear webhook signature.
|
||||
|
||||
|
|
@ -1494,7 +1464,6 @@ _SUPPORTED_GH_EVENTS = frozenset(
|
|||
_SUPPORTED_GH_ISSUE_ACTIONS = frozenset(["edited", "opened", "reopened"])
|
||||
_SUPPORTED_GH_PULL_REQUEST_ACTIONS = frozenset(
|
||||
[
|
||||
"review_requested",
|
||||
"opened",
|
||||
"ready_for_review",
|
||||
"converted_to_draft",
|
||||
|
|
@ -1619,12 +1588,6 @@ async def _trigger_or_queue_run(
|
|||
logger.info("LangGraph run created for thread %s from GitHub PR comment", thread_id)
|
||||
|
||||
|
||||
def _is_open_swe_reviewer_request(payload: dict[str, Any]) -> bool:
|
||||
reviewer = payload.get("requested_reviewer") or {}
|
||||
login = reviewer.get("login", "") if isinstance(reviewer, dict) else ""
|
||||
return login.lower() == OPEN_SWE_BOT_NAME.lower()
|
||||
|
||||
|
||||
def build_github_pr_review_prompt(
|
||||
repo_config: dict[str, str],
|
||||
pr_number: int,
|
||||
|
|
@ -2005,11 +1968,6 @@ async def _dispatch_first_review_from_pr_payload(payload: dict[str, Any], *, sou
|
|||
logger.info("Reviewer run created for thread %s (source=%s)", thread_id, source)
|
||||
|
||||
|
||||
async def process_github_pr_review_request(payload: dict[str, Any]) -> None:
|
||||
"""Trigger the reviewer agent when the Open SWE bot is requested on a PR."""
|
||||
await _dispatch_first_review_from_pr_payload(payload, source="github")
|
||||
|
||||
|
||||
async def process_github_pr_ready(payload: dict[str, Any]) -> None:
|
||||
"""Auto-review a PR that has just been opened or marked ready-for-review.
|
||||
|
||||
|
|
@ -2033,75 +1991,6 @@ async def process_github_pr_ready(payload: dict[str, Any]) -> None:
|
|||
await _dispatch_first_review_from_pr_payload(payload, source="github")
|
||||
|
||||
|
||||
async def process_github_pr_review_command(
|
||||
payload: dict[str, Any],
|
||||
event_type: str,
|
||||
pr_url_override: str | None,
|
||||
) -> None:
|
||||
"""Trigger the reviewer when a PR comment contains ``@open-swe review``.
|
||||
|
||||
``pr_url_override`` is the optional URL token that followed ``review``. If
|
||||
set, the review targets that PR; otherwise the comment's own PR is used.
|
||||
"""
|
||||
repo = payload.get("repository", {})
|
||||
repo_config = {
|
||||
"owner": repo.get("owner", {}).get("login", ""),
|
||||
"name": repo.get("name", ""),
|
||||
}
|
||||
pr_data = payload.get("pull_request") or payload.get("issue", {})
|
||||
sender = payload.get("sender", {})
|
||||
github_login = sender.get("login", "")
|
||||
github_user_id = sender.get("id")
|
||||
|
||||
pr_ref: GitHubPrRef | None = None
|
||||
if pr_url_override:
|
||||
pr_ref = parse_github_pr_url(pr_url_override)
|
||||
if pr_ref is None:
|
||||
logger.info("Ignoring @open-swe review with unparseable URL %s", pr_url_override)
|
||||
return
|
||||
else:
|
||||
pr_number = pr_data.get("number")
|
||||
if not pr_number:
|
||||
logger.warning("@open-swe review command missing pr_number, skipping")
|
||||
return
|
||||
pr_ref = GitHubPrRef(
|
||||
owner=repo_config["owner"],
|
||||
repo=repo_config["name"],
|
||||
number=pr_number,
|
||||
url=pr_data.get("html_url", "") or pr_data.get("url", ""),
|
||||
)
|
||||
|
||||
comment = payload.get("comment") or payload.get("review", {})
|
||||
comment_id = comment.get("id")
|
||||
node_id = comment.get("node_id") if event_type == "pull_request_review" else None
|
||||
if comment_id:
|
||||
app_token = await get_github_app_installation_token()
|
||||
if app_token:
|
||||
await react_to_github_comment(
|
||||
repo_config,
|
||||
comment_id,
|
||||
event_type=event_type,
|
||||
token=app_token,
|
||||
pull_number=pr_data.get("number"),
|
||||
node_id=node_id,
|
||||
)
|
||||
|
||||
result = await trigger_pr_review_from_ref(
|
||||
pr_ref,
|
||||
source="github",
|
||||
github_login=github_login,
|
||||
github_user_id=github_user_id,
|
||||
)
|
||||
if not result.get("success"):
|
||||
logger.warning(
|
||||
"Failed to trigger reviewer from @open-swe review on %s/%s#%s: %s",
|
||||
pr_ref.owner,
|
||||
pr_ref.repo,
|
||||
pr_ref.number,
|
||||
result.get("error"),
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_open_pr_for_branch(
|
||||
repo_config: dict[str, str], head_ref: str, *, token: str
|
||||
) -> dict[str, Any] | None:
|
||||
|
|
@ -2541,20 +2430,9 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
|
|||
else:
|
||||
logger.warning("Failed to persist branch_name metadata for thread %s", thread_id)
|
||||
|
||||
comment = payload.get("comment") or payload.get("review", {})
|
||||
is_review_request, _pr_url_override = parse_github_review_command(comment.get("body") or "")
|
||||
email = await email_for_login(github_login) or ""
|
||||
if email:
|
||||
github_token = await _get_or_resolve_thread_github_token(thread_id, email)
|
||||
elif is_review_request:
|
||||
github_token, expires_at = await get_github_app_installation_token_with_expiry()
|
||||
if github_token:
|
||||
try:
|
||||
await persist_encrypted_github_token(thread_id, github_token, expires_at=expires_at)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Could not persist bot token for PR review request thread %s", thread_id
|
||||
)
|
||||
else:
|
||||
logger.warning("No email mapping for GitHub user '%s', skipping", github_login)
|
||||
return
|
||||
|
|
@ -3011,24 +2889,11 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
|
|||
logger.info("Accepted GitHub PR %s webhook, scheduling auto-review task", action)
|
||||
background_tasks.add_task(process_github_pr_ready, payload)
|
||||
return {"status": "accepted", "message": f"Processing PR {action} for auto-review"}
|
||||
if not _is_open_swe_reviewer_request(payload):
|
||||
logger.info("Ignoring PR review request for a different reviewer")
|
||||
return {"status": "ignored", "reason": "Review request is not for open-swe bot"}
|
||||
if not await _is_repo_enabled_for_review(webhook_repo_config):
|
||||
logger.warning(
|
||||
"Rejecting GitHub reviewer webhook: repo '%s/%s' not enabled for review",
|
||||
webhook_repo_config.get("owner"),
|
||||
webhook_repo_config.get("name"),
|
||||
)
|
||||
return {"status": "ignored", "reason": "Repository not enabled for review"}
|
||||
|
||||
gate_rejection = await _enforce_public_repo_org_gate(payload, "pull_request")
|
||||
if gate_rejection is not None:
|
||||
return gate_rejection
|
||||
|
||||
logger.info("Accepted GitHub PR review request webhook, scheduling reviewer task")
|
||||
background_tasks.add_task(process_github_pr_review_request, payload)
|
||||
return {"status": "accepted", "message": "Processing GitHub PR review request"}
|
||||
logger.info("Ignoring unsupported GitHub pull_request action: %s", action)
|
||||
return {
|
||||
"status": "ignored",
|
||||
"reason": f"Unsupported GitHub pull_request action: {action}",
|
||||
}
|
||||
|
||||
if event_type == "push":
|
||||
if not await _is_repo_enabled_for_review(webhook_repo_config):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from fastapi.testclient import TestClient
|
|||
|
||||
from agent import webapp
|
||||
from agent.tools import request_pr_review as request_pr_review_tool
|
||||
from agent.utils import github_comments
|
||||
from agent.utils import slack as slack_utils
|
||||
from agent.utils.slack import GitHubPrRef
|
||||
|
||||
|
|
@ -501,23 +500,8 @@ def test_github_webhook_ignores_unsupported_comment_action(monkeypatch) -> None:
|
|||
}
|
||||
|
||||
|
||||
def test_github_webhook_blocks_reviewer_repo_not_enabled_in_dashboard(monkeypatch) -> None:
|
||||
called = False
|
||||
|
||||
async def fake_process_github_pr_review_request(payload: dict[str, object]) -> None:
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_request", fake_process_github_pr_review_request
|
||||
)
|
||||
def test_github_webhook_ignores_review_requested(monkeypatch) -> None:
|
||||
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
|
||||
|
||||
async def fake_is_review_repo_enabled(_owner: str, _name: str) -> bool:
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(webapp, "is_review_repo_enabled", fake_is_review_repo_enabled)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
|
|
@ -537,77 +521,10 @@ def test_github_webhook_blocks_reviewer_repo_not_enabled_in_dashboard(monkeypatc
|
|||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ignored", "reason": "Repository not enabled for review"}
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_github_webhook_accepts_open_swe_review_requested(monkeypatch) -> None:
|
||||
called: dict[str, object] = {}
|
||||
|
||||
async def fake_process_github_pr_review_request(payload: dict[str, object]) -> None:
|
||||
called["payload"] = payload
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_request", fake_process_github_pr_review_request
|
||||
)
|
||||
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"pull_request",
|
||||
{
|
||||
"action": "review_requested",
|
||||
"requested_reviewer": {"login": "open-swe[bot]"},
|
||||
"pull_request": {
|
||||
"number": 1244,
|
||||
"html_url": "https://github.com/langchain-ai/open-swe/pull/1244",
|
||||
"base": {"sha": "base-sha"},
|
||||
"head": {"sha": "head-sha", "ref": "feature-branch"},
|
||||
},
|
||||
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
|
||||
"sender": {"login": "octocat"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "accepted"
|
||||
assert called["payload"]["requested_reviewer"]["login"] == "open-swe[bot]"
|
||||
|
||||
|
||||
def test_github_webhook_ignores_review_requested_for_other_reviewer(monkeypatch) -> None:
|
||||
called = False
|
||||
|
||||
async def fake_process_github_pr_review_request(payload: dict[str, object]) -> None:
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_request", fake_process_github_pr_review_request
|
||||
)
|
||||
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"pull_request",
|
||||
{
|
||||
"action": "review_requested",
|
||||
"requested_reviewer": {"login": "someone-else"},
|
||||
"pull_request": {
|
||||
"number": 1244,
|
||||
"html_url": "https://github.com/langchain-ai/open-swe/pull/1244",
|
||||
"base": {"sha": "base-sha"},
|
||||
"head": {"sha": "head-sha", "ref": "feature-branch"},
|
||||
},
|
||||
"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
|
||||
assert response.json() == {
|
||||
"status": "ignored",
|
||||
"reason": "Unsupported GitHub pull_request action: review_requested",
|
||||
}
|
||||
|
||||
|
||||
def test_slack_webhook_routes_review_command_to_agent(monkeypatch) -> None:
|
||||
|
|
@ -816,76 +733,9 @@ def test_slack_webhook_threaded_followup_uses_parent_thread_ts(monkeypatch) -> N
|
|||
assert event_data["event_ts"] == "1700000000.000200"
|
||||
|
||||
|
||||
def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None:
|
||||
def test_process_github_pr_ready_creates_reviewer_run(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_trigger_pr_review_from_ref(
|
||||
pr_ref: GitHubPrRef,
|
||||
*,
|
||||
source: str,
|
||||
github_login: str = "",
|
||||
github_user_id: int | None = None,
|
||||
slack_channel_id: str = "",
|
||||
slack_thread_ts: str = "",
|
||||
) -> dict[str, object]:
|
||||
captured["pr_ref"] = pr_ref
|
||||
captured["source"] = source
|
||||
captured["slack_channel_id"] = slack_channel_id
|
||||
captured["slack_thread_ts"] = slack_thread_ts
|
||||
return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url}
|
||||
|
||||
async def fake_post_slack_trace_reply(
|
||||
channel_id: str, thread_ts: str, thread_id: str, *, include_dashboard_link: bool = True
|
||||
) -> None:
|
||||
captured["trace_reply"] = {
|
||||
"channel_id": channel_id,
|
||||
"thread_ts": thread_ts,
|
||||
"thread_id": thread_id,
|
||||
"include_dashboard_link": include_dashboard_link,
|
||||
}
|
||||
|
||||
async def fake_set_slack_assistant_status(channel_id: str, thread_ts: str) -> bool:
|
||||
captured.setdefault("status_calls", []).append((channel_id, thread_ts))
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(webapp, "trigger_pr_review_from_ref", fake_trigger_pr_review_from_ref)
|
||||
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
|
||||
monkeypatch.setattr(webapp, "set_slack_assistant_status", fake_set_slack_assistant_status)
|
||||
|
||||
asyncio.run(
|
||||
webapp.process_slack_pr_review_request(
|
||||
GitHubPrRef(
|
||||
owner="langchain-ai",
|
||||
repo="open-swe",
|
||||
number=1244,
|
||||
url="https://github.com/langchain-ai/open-swe/pull/1244",
|
||||
),
|
||||
"C123",
|
||||
"1700000000.000100",
|
||||
)
|
||||
)
|
||||
|
||||
assert captured["source"] == "slack"
|
||||
assert captured["slack_channel_id"] == "C123"
|
||||
assert captured["slack_thread_ts"] == "1700000000.000100"
|
||||
assert captured["trace_reply"] == {
|
||||
"channel_id": "C123",
|
||||
"thread_ts": "1700000000.000100",
|
||||
"thread_id": "reviewer-thread-id",
|
||||
"include_dashboard_link": False,
|
||||
}
|
||||
assert captured["status_calls"] == [
|
||||
("C123", "1700000000.000100"),
|
||||
("C123", "1700000000.000100"),
|
||||
]
|
||||
|
||||
|
||||
def test_process_github_pr_review_request_creates_reviewer_run(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_get_github_app_installation_token() -> str | None:
|
||||
return "app-token"
|
||||
|
||||
async def fake_get_github_app_installation_token_with_expiry() -> tuple[str | None, str | None]:
|
||||
return "app-token", None
|
||||
|
||||
|
|
@ -919,9 +769,6 @@ def test_process_github_pr_review_request_creates_reviewer_run(monkeypatch) -> N
|
|||
captured["set_metadata_thread_id"] = thread_id
|
||||
captured["set_metadata_kwargs"] = kwargs
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "get_github_app_installation_token", fake_get_github_app_installation_token
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
webapp,
|
||||
"get_github_app_installation_token_with_expiry",
|
||||
|
|
@ -935,10 +782,9 @@ def test_process_github_pr_review_request_creates_reviewer_run(monkeypatch) -> N
|
|||
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClient())
|
||||
|
||||
asyncio.run(
|
||||
webapp.process_github_pr_review_request(
|
||||
webapp.process_github_pr_ready(
|
||||
{
|
||||
"action": "review_requested",
|
||||
"requested_reviewer": {"login": "open-swe[bot]"},
|
||||
"action": "opened",
|
||||
"pull_request": {
|
||||
"number": 1244,
|
||||
"html_url": "https://github.com/langchain-ai/open-swe/pull/1244",
|
||||
|
|
@ -1158,7 +1004,7 @@ def test_request_pr_review_tool_uses_shared_trigger(monkeypatch) -> None:
|
|||
assert result["success"] is True
|
||||
|
||||
|
||||
def test_process_github_pr_comment_review_request_without_email_uses_app_token(
|
||||
def test_process_github_pr_comment_without_email_skips(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
|
@ -1174,15 +1020,6 @@ def test_process_github_pr_comment_review_request_without_email_uses_app_token(
|
|||
None,
|
||||
)
|
||||
|
||||
async def fake_get_app_token_with_expiry() -> tuple[str, str]:
|
||||
return "app-token", "2026-01-01T00:00:00Z"
|
||||
|
||||
async def fake_persist_token(
|
||||
thread_id: str, token: str, *, expires_at: str | None = None
|
||||
) -> str:
|
||||
captured["persisted"] = {"thread_id": thread_id, "token": token, "expires_at": expires_at}
|
||||
return "encrypted"
|
||||
|
||||
async def fake_react(*args, **kwargs) -> bool:
|
||||
captured["reaction_token"] = kwargs["token"]
|
||||
return True
|
||||
|
|
@ -1196,10 +1033,6 @@ def test_process_github_pr_comment_review_request_without_email_uses_app_token(
|
|||
|
||||
monkeypatch.setattr(webapp, "extract_pr_context", fake_extract_pr_context)
|
||||
monkeypatch.setattr(webapp, "email_for_login", lambda login: asyncio.sleep(0, result=None))
|
||||
monkeypatch.setattr(
|
||||
webapp, "get_github_app_installation_token_with_expiry", fake_get_app_token_with_expiry
|
||||
)
|
||||
monkeypatch.setattr(webapp, "persist_encrypted_github_token", fake_persist_token)
|
||||
monkeypatch.setattr(webapp, "react_to_github_comment", fake_react)
|
||||
monkeypatch.setattr(webapp, "fetch_pr_comments_since_last_tag", fake_fetch_comments)
|
||||
monkeypatch.setattr(webapp, "_trigger_or_queue_run", fake_trigger_or_queue_run)
|
||||
|
|
@ -1214,14 +1047,7 @@ def test_process_github_pr_comment_review_request_without_email_uses_app_token(
|
|||
)
|
||||
)
|
||||
|
||||
assert captured["reaction_token"] == "app-token"
|
||||
assert captured["fetch_token"] == "app-token"
|
||||
assert captured["persisted"] == {
|
||||
"thread_id": "00000000-0000-0000-0000-000000000001",
|
||||
"token": "app-token",
|
||||
"expires_at": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
assert captured["triggered"]
|
||||
assert captured == {}
|
||||
|
||||
|
||||
def test_process_github_issue_uses_resolved_user_token_for_reaction(monkeypatch) -> None:
|
||||
|
|
@ -1394,50 +1220,6 @@ def test_process_github_issue_existing_thread_uses_followup_prompt(monkeypatch)
|
|||
assert "## Repository" not in captured["prompt"]
|
||||
|
||||
|
||||
def test_parse_github_review_command_standalone() -> None:
|
||||
is_review, url = github_comments.parse_github_review_command("@open-swe review")
|
||||
assert is_review is True
|
||||
assert url is None
|
||||
|
||||
|
||||
def test_parse_github_review_command_with_url() -> None:
|
||||
is_review, url = github_comments.parse_github_review_command(
|
||||
"@open-swe review https://github.com/langchain-ai/open-swe/pull/1244"
|
||||
)
|
||||
assert is_review is True
|
||||
assert url == "https://github.com/langchain-ai/open-swe/pull/1244"
|
||||
|
||||
|
||||
def test_parse_github_review_command_case_insensitive_and_aliases() -> None:
|
||||
assert github_comments.parse_github_review_command("@OpenSWE Review") == (True, None)
|
||||
assert github_comments.parse_github_review_command("@openswe review") == (True, None)
|
||||
assert github_comments.parse_github_review_command("@openswe-dev review") == (True, None)
|
||||
|
||||
|
||||
def test_parse_github_review_command_freeform_does_not_match() -> None:
|
||||
assert github_comments.parse_github_review_command("@open-swe review my code") == (False, None)
|
||||
assert github_comments.parse_github_review_command("@open-swe please review") == (False, None)
|
||||
assert github_comments.parse_github_review_command("@open-swe fix this") == (False, None)
|
||||
assert github_comments.parse_github_review_command("") == (False, None)
|
||||
|
||||
|
||||
def test_parse_github_review_command_does_not_swallow_trailing_text() -> None:
|
||||
# Trailing text after `review` (on a new line, with punctuation, or extra
|
||||
# words) must NOT be parsed as a URL — otherwise the comment would be
|
||||
# silently dropped instead of falling through to the regular handler.
|
||||
assert github_comments.parse_github_review_command("@open-swe review\nthanks!") == (
|
||||
False,
|
||||
None,
|
||||
)
|
||||
assert github_comments.parse_github_review_command("@open-swe review please") == (False, None)
|
||||
assert github_comments.parse_github_review_command("@open-swe review now") == (False, None)
|
||||
# Non-http schemes are not URLs we can route to a PR.
|
||||
assert github_comments.parse_github_review_command("@open-swe review ftp://x/y/pull/1") == (
|
||||
False,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def test_github_webhook_routes_pr_comment_review_to_agent(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
|
|
@ -1445,13 +1227,7 @@ def test_github_webhook_routes_pr_comment_review_to_agent(monkeypatch) -> None:
|
|||
captured["payload"] = payload
|
||||
captured["event_type"] = event_type
|
||||
|
||||
async def fake_process_review_command(
|
||||
payload: dict[str, object], event_type: str, pr_url_override: str | None
|
||||
) -> None:
|
||||
raise AssertionError("review commands should route through the main agent")
|
||||
|
||||
monkeypatch.setattr(webapp, "process_github_pr_comment", fake_process_pr_comment)
|
||||
monkeypatch.setattr(webapp, "process_github_pr_review_command", fake_process_review_command)
|
||||
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
|
||||
monkeypatch.setattr(webapp, "ALLOWED_GITHUB_ORGS", frozenset({"langchain-ai"}))
|
||||
|
||||
|
|
@ -1508,105 +1284,3 @@ def test_github_webhook_routes_pr_review_request_comment_to_agent(monkeypatch) -
|
|||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "accepted", "message": "Processing issue_comment event"}
|
||||
assert captured["event_type"] == "issue_comment"
|
||||
|
||||
|
||||
def test_process_github_pr_review_command_uses_payload_pr(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_get_app_token() -> str:
|
||||
return "app-token"
|
||||
|
||||
async def fake_react(*args, **kwargs) -> None:
|
||||
captured["reacted"] = True
|
||||
|
||||
async def fake_trigger(
|
||||
pr_ref: GitHubPrRef,
|
||||
*,
|
||||
source: str,
|
||||
github_login: str = "",
|
||||
github_user_id: int | None = None,
|
||||
slack_channel_id: str = "",
|
||||
slack_thread_ts: str = "",
|
||||
) -> dict[str, object]:
|
||||
captured["pr_ref"] = pr_ref
|
||||
captured["source"] = source
|
||||
captured["github_login"] = github_login
|
||||
captured["github_user_id"] = github_user_id
|
||||
return {"success": True, "thread_id": "tid", "pr_url": pr_ref.url}
|
||||
|
||||
monkeypatch.setattr(webapp, "get_github_app_installation_token", fake_get_app_token)
|
||||
monkeypatch.setattr(webapp, "react_to_github_comment", fake_react)
|
||||
monkeypatch.setattr(webapp, "trigger_pr_review_from_ref", fake_trigger)
|
||||
|
||||
asyncio.run(
|
||||
webapp.process_github_pr_review_command(
|
||||
{
|
||||
"issue": {
|
||||
"number": 1244,
|
||||
"html_url": "https://github.com/langchain-ai/open-swe/pull/1244",
|
||||
"pull_request": {"url": "..."},
|
||||
},
|
||||
"comment": {"id": 9, "body": "@open-swe review"},
|
||||
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
|
||||
"sender": {"login": "octocat", "id": 123},
|
||||
},
|
||||
"issue_comment",
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
pr_ref = captured["pr_ref"]
|
||||
assert isinstance(pr_ref, GitHubPrRef)
|
||||
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"
|
||||
assert captured["source"] == "github"
|
||||
assert captured["github_login"] == "octocat"
|
||||
assert captured["github_user_id"] == 123
|
||||
assert captured.get("reacted") is True
|
||||
|
||||
|
||||
def test_process_github_pr_review_command_uses_url_override(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_get_app_token() -> str:
|
||||
return "app-token"
|
||||
|
||||
async def fake_react(*args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
async def fake_trigger(
|
||||
pr_ref: GitHubPrRef,
|
||||
*,
|
||||
source: str,
|
||||
github_login: str = "",
|
||||
github_user_id: int | None = None,
|
||||
slack_channel_id: str = "",
|
||||
slack_thread_ts: str = "",
|
||||
) -> dict[str, object]:
|
||||
captured["pr_ref"] = pr_ref
|
||||
return {"success": True, "thread_id": "tid", "pr_url": pr_ref.url}
|
||||
|
||||
monkeypatch.setattr(webapp, "get_github_app_installation_token", fake_get_app_token)
|
||||
monkeypatch.setattr(webapp, "react_to_github_comment", fake_react)
|
||||
monkeypatch.setattr(webapp, "trigger_pr_review_from_ref", fake_trigger)
|
||||
|
||||
asyncio.run(
|
||||
webapp.process_github_pr_review_command(
|
||||
{
|
||||
"issue": {"number": 99, "pull_request": {"url": "..."}},
|
||||
"comment": {"id": 1, "body": "@open-swe review https://github.com/x/y/pull/7"},
|
||||
"repository": {"owner": {"login": "x"}, "name": "y"},
|
||||
"sender": {"login": "octocat", "id": 1},
|
||||
},
|
||||
"issue_comment",
|
||||
"https://github.com/x/y/pull/7",
|
||||
)
|
||||
)
|
||||
|
||||
pr_ref = captured["pr_ref"]
|
||||
assert isinstance(pr_ref, GitHubPrRef)
|
||||
assert pr_ref.number == 7
|
||||
assert pr_ref.owner == "x"
|
||||
assert pr_ref.repo == "y"
|
||||
|
|
|
|||
|
|
@ -54,15 +54,9 @@ def test_gate_blocks_non_member_on_public_pr_comment(monkeypatch) -> None:
|
|||
_common_setup(monkeypatch)
|
||||
seen = _install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
async def fake_process_github_pr_review_command(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
async def fake_process_github_pr_comment(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_command", fake_process_github_pr_review_command
|
||||
)
|
||||
monkeypatch.setattr(webapp, "process_github_pr_comment", fake_process_github_pr_comment)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
|
|
@ -254,16 +248,9 @@ def test_gate_blocks_non_member_on_public_issue(monkeypatch) -> None:
|
|||
assert "not a member" in body["reason"]
|
||||
|
||||
|
||||
def test_gate_blocks_non_member_on_public_review_requested(monkeypatch) -> None:
|
||||
def test_review_requested_is_unsupported_before_public_repo_gate(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
_install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
async def fake_process_github_pr_review_request(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_request", fake_process_github_pr_review_request
|
||||
)
|
||||
seen = _install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
|
|
@ -288,9 +275,11 @@ def test_gate_blocks_non_member_on_public_review_requested(monkeypatch) -> None:
|
|||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["status"] == "ignored"
|
||||
assert "not a member" in body["reason"]
|
||||
assert response.json() == {
|
||||
"status": "ignored",
|
||||
"reason": "Unsupported GitHub pull_request action: review_requested",
|
||||
}
|
||||
assert seen["calls"] == []
|
||||
|
||||
|
||||
def test_gate_allows_internal_bot_sender(monkeypatch) -> None:
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ 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,
|
||||
|
|
@ -155,52 +153,6 @@ def test_parse_github_pr_url_slack_formatted_link() -> None:
|
|||
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"}],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue