feat(robomp): added incoming PR review feature with classify and submit

- Added `review_pr` task that checks out PR head in a detached worktree, classifies rank/type/area, and posts a batched GitHub review as `event=COMMENT`.
- Added four new host tools: `fetch_pr`, `classify_pr`, `pr_review_comment`, and `submit_pr_review`; review tools self-gate on `review_mode`, push/open-PR tools refuse when `review_mode` is set.
- Added sqlite staging table `pr_review_comments` with `stage_review_comment`, `list_staged_review_comments`, and `clear_staged_review_comments` DAOs.
- Routed `pull_request.opened/reopened/ready_for_review` to `review_pr` and extended `pull_request.closed` cleanup to any tracked PR regardless of author.
This commit is contained in:
can1357
2026-06-02 08:23:04 +02:00
parent 04cc014c16
commit b503f7d86b
30 changed files with 2392 additions and 112 deletions
+111
View File
@@ -109,6 +109,117 @@ def test_get_pull_request_parses_head_repo_and_author() -> None:
assert pr.author == "robomp-bot"
def test_get_pull_request_parses_title_and_body() -> None:
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/repos/octo/widget/pulls/9"
return httpx.Response(
200,
json={
"number": 9,
"html_url": "https://github.com/octo/widget/pull/9",
"title": "Fix crash",
"body": "Fixes #1",
"head": {"ref": "fix", "repo": {"full_name": "fork/widget"}},
"base": {"ref": "main"},
"state": "open",
"user": {"login": "alice"},
},
)
client = GitHubClient("tok", transport=httpx.MockTransport(handler))
pr = _run_async(client.get_pull_request("octo/widget", 9))
assert pr.title == "Fix crash"
assert pr.body == "Fixes #1"
def test_list_pr_files_parses_changed_file_summary() -> None:
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/repos/octo/widget/pulls/9/files"
assert request.url.params.get("per_page") == "100"
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 len(files) == 1
assert files[0].path == "src/app.py"
assert files[0].additions == 5
assert files[0].deletions == 2
def test_list_pr_files_paginates_past_first_page() -> None:
seen_pages: list[str | None] = []
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/repos/octo/widget/pulls/9/files"
page = request.url.params.get("page")
seen_pages.append(page)
if page == "1":
return httpx.Response(
200,
json=[
{
"filename": f"src/file-{idx}.py",
"status": "modified",
"additions": 1,
"deletions": 0,
}
for idx in range(100)
],
)
assert page == "2"
return httpx.Response(
200,
json=[{"filename": "src/final.py", "status": "added", "additions": 2, "deletions": 0}],
)
client = GitHubClient("tok", transport=httpx.MockTransport(handler))
files = _run_async(client.list_pr_files("octo/widget", 9))
assert seen_pages == ["1", "2"]
assert len(files) == 101
assert files[-1].path == "src/final.py"
def test_submit_pr_review_posts_comment_event_and_inline_comments() -> None:
captured: dict[str, object] = {}
def handler(request: httpx.Request) -> httpx.Response:
import json
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))
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"}],
)
)
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", "line": 12, "side": "RIGHT", "body": "finding"}],
}
def test_204_no_content_returns_none() -> None:
transport = httpx.MockTransport(lambda r: httpx.Response(204))
client = GitHubClient("tok", transport=transport)