fix(robomp): validate PR review comment anchors against the diff before submitting

A single comment on a line outside a diff hunk 422s the whole review on
GitHub, which the 422/500 fallback (ported here from the Forgejo work,
including payload shaping and commit_id) then degraded into plain issue
comments. Now we fetch /pulls/{n}/files, compute per-hunk anchorable line
sets, drop non-anchorable comments, fold them into the review summary, and
only fall back to issue comments for genuine 422/500s that survive
filtering. Files-fetch failure fails open (submit unfiltered).
This commit is contained in:
djdembeck
2026-08-18 03:54:00 -05:00
parent cc00ab161b
commit 71d608aae6
4 changed files with 630 additions and 15 deletions
+66 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import json
import httpx
import pytest
@@ -166,7 +167,15 @@ def test_list_pr_files_parses_changed_file_summary() -> None:
assert request.url.params.get("per_page") == "100"
return httpx.Response(
200,
json=[{"filename": "src/app.py", "status": "modified", "additions": 5, "deletions": 2}],
json=[
{
"filename": "src/app.py",
"status": "modified",
"additions": 5,
"deletions": 2,
"patch": "@@ -8,3 +8,5 @@\n ctx\n+added\n ctx2",
}
],
)
client = GitHubClient("tok", transport=httpx.MockTransport(handler))
@@ -175,6 +184,20 @@ def test_list_pr_files_parses_changed_file_summary() -> None:
assert files[0].path == "src/app.py"
assert files[0].additions == 5
assert files[0].deletions == 2
assert files[0].patch.startswith("@@ -8,3 +8,5")
def test_list_pr_files_defaults_missing_patch_to_empty() -> None:
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/repos/octo/widget/pulls/9/files"
return httpx.Response(
200,
json=[{"filename": "src/app.py", "status": "modified", "additions": 5, "deletions": 2}],
)
client = GitHubClient("tok", transport=httpx.MockTransport(handler))
files = _run_async(client.list_pr_files("octo/widget", 9))
assert files[0].patch == ""
def test_list_pr_files_paginates_past_first_page() -> None:
@@ -248,6 +271,48 @@ def test_submit_pr_review_posts_comment_event_and_inline_comments() -> None:
}
def test_submit_pr_review_forgejo_uses_new_position_payload() -> None:
captured: dict[str, object] = {}
def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["body"] = json.loads(request.content)
return httpx.Response(
200,
json={
"id": 44,
"user": {"login": "robomp-bot"},
"body": "summary",
"state": "COMMENTED",
"submitted_at": "t",
},
)
client = GitHubClient("tok", transport=httpx.MockTransport(handler), platform="forgejo")
review = _run_async(
client.submit_pr_review(
repo="octo/widget",
pr_number=9,
body="summary",
event="COMMENT",
comments=[
{"path": "src/app.py", "line": 12, "side": "RIGHT", "body": "finding"},
{"path": "src/old.py", "line": 5, "side": "LEFT", "body": "removed-line finding"},
],
)
)
assert review.id == 44
assert captured["path"] == "/repos/octo/widget/pulls/9/reviews"
assert captured["body"] == {
"body": "summary",
"event": "COMMENT",
"comments": [
{"path": "src/app.py", "body": "finding", "new_position": 12},
{"path": "src/old.py", "body": "removed-line finding", "old_position": 5},
],
}
def test_204_no_content_returns_none() -> None:
transport = httpx.MockTransport(lambda r: httpx.Response(204))
client = GitHubClient("tok", transport=transport)