mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 19:32:12 +00:00
fix(reviewer): enforce blocking review checks
This commit is contained in:
parent
39c768aa3f
commit
4fea5f21c5
5 changed files with 113 additions and 31 deletions
|
|
@ -415,10 +415,6 @@ async def _publish_review_async(
|
||||||
),
|
),
|
||||||
"verdict_authorization": verdict_authorization,
|
"verdict_authorization": verdict_authorization,
|
||||||
}
|
}
|
||||||
if verdict_ignored_reason:
|
|
||||||
skip_result["verdict_submitted"] = False
|
|
||||||
skip_result["verdict_ignored"] = True
|
|
||||||
skip_result["verdict_ignored_reason"] = verdict_ignored_reason
|
|
||||||
conclusion, check_title, check_summary = review_check_conclusion(skip_result)
|
conclusion, check_title, check_summary = review_check_conclusion(skip_result)
|
||||||
await settle_review_check_run(
|
await settle_review_check_run(
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,28 @@ def review_check_conclusion(
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
ignored_reason = publish_outcome.get("verdict_ignored_reason")
|
ignored_reason = publish_outcome.get("verdict_ignored_reason")
|
||||||
|
verdict_authorization = publish_outcome.get("verdict_authorization")
|
||||||
|
if (
|
||||||
|
blocking_count is not None
|
||||||
|
and blocking_count > 0
|
||||||
|
and verdict_authorization
|
||||||
|
in {
|
||||||
|
"requested",
|
||||||
|
"consistent",
|
||||||
|
}
|
||||||
|
and ignored_reason
|
||||||
|
in {
|
||||||
|
None,
|
||||||
|
"approve_with_open_findings",
|
||||||
|
"self_review",
|
||||||
|
}
|
||||||
|
):
|
||||||
|
issue_word = "issue" if blocking_count == 1 else "issues"
|
||||||
|
return (
|
||||||
|
"failure",
|
||||||
|
f"Found {blocking_count} blocking {issue_word}",
|
||||||
|
f"Open SWE found {blocking_count} blocking {issue_word} on this pull request.",
|
||||||
|
)
|
||||||
if ignored_reason and ignored_reason != "self_review":
|
if ignored_reason and ignored_reason != "self_review":
|
||||||
return (
|
return (
|
||||||
"neutral",
|
"neutral",
|
||||||
|
|
@ -193,23 +215,6 @@ def review_check_conclusion(
|
||||||
"Open SWE reviewed this pull request and found no blocking issues.",
|
"Open SWE reviewed this pull request and found no blocking issues.",
|
||||||
)
|
)
|
||||||
|
|
||||||
verdict_authorization = publish_outcome.get("verdict_authorization")
|
|
||||||
if (
|
|
||||||
blocking_count is not None
|
|
||||||
and blocking_count > 0
|
|
||||||
and verdict_authorization
|
|
||||||
in {
|
|
||||||
"requested",
|
|
||||||
"consistent",
|
|
||||||
}
|
|
||||||
):
|
|
||||||
issue_word = "issue" if blocking_count == 1 else "issues"
|
|
||||||
return (
|
|
||||||
"failure",
|
|
||||||
f"Found {blocking_count} blocking {issue_word}",
|
|
||||||
f"Open SWE found {blocking_count} blocking {issue_word} on this pull request.",
|
|
||||||
)
|
|
||||||
|
|
||||||
surfaced_count_raw = publish_outcome.get("surfaced_count")
|
surfaced_count_raw = publish_outcome.get("surfaced_count")
|
||||||
surfaced_count = (
|
surfaced_count = (
|
||||||
surfaced_count_raw
|
surfaced_count_raw
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,15 @@ async def test_post_autofix_status_check_completes_neutral(
|
||||||
"failure",
|
"failure",
|
||||||
"Found 2 blocking issues",
|
"Found 2 blocking issues",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"blocking_finding_count": 2,
|
||||||
|
"verdict_authorization": "consistent",
|
||||||
|
"verdict_ignored_reason": "approve_with_open_findings",
|
||||||
|
},
|
||||||
|
"failure",
|
||||||
|
"Found 2 blocking issues",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"blocking_finding_count": 1,
|
"blocking_finding_count": 1,
|
||||||
|
|
@ -208,6 +217,24 @@ async def test_post_autofix_status_check_completes_neutral(
|
||||||
"neutral",
|
"neutral",
|
||||||
"Verdict withheld",
|
"Verdict withheld",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"blocking_finding_count": 1,
|
||||||
|
"verdict_authorization": "consistent",
|
||||||
|
"verdict_ignored_reason": "author_unknown",
|
||||||
|
},
|
||||||
|
"neutral",
|
||||||
|
"Verdict withheld",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"blocking_finding_count": 1,
|
||||||
|
"verdict_authorization": "consistent",
|
||||||
|
"verdict_ignored_reason": "head_check_failed",
|
||||||
|
},
|
||||||
|
"neutral",
|
||||||
|
"Verdict withheld",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"blocking_finding_count": 3,
|
"blocking_finding_count": 3,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from agent.api import app as api_app
|
from agent.api import app as api_app
|
||||||
|
|
@ -1155,8 +1156,16 @@ def test_trigger_pr_review_from_ref_creates_reviewer_run(monkeypatch) -> None:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_trigger_pr_review_from_ref_reuses_open_check_on_same_head(monkeypatch) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"tracked_head",
|
||||||
|
["head-sha", "old-head-sha"],
|
||||||
|
ids=["same-head", "stale-check-different-head"],
|
||||||
|
)
|
||||||
|
def test_trigger_pr_review_from_ref_tracks_check_for_current_head(
|
||||||
|
monkeypatch, tracked_head: str
|
||||||
|
) -> None:
|
||||||
created_check = AsyncMock(return_value=88)
|
created_check = AsyncMock(return_value=88)
|
||||||
|
set_metadata = AsyncMock()
|
||||||
|
|
||||||
async def fake_token() -> tuple[str | None, str | None]:
|
async def fake_token() -> tuple[str | None, str | None]:
|
||||||
return "app-token", None
|
return "app-token", None
|
||||||
|
|
@ -1185,13 +1194,13 @@ def test_trigger_pr_review_from_ref_reuses_open_check_on_same_head(monkeypatch)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
webhook_common,
|
webhook_common,
|
||||||
"_get_thread_metadata_safe",
|
"_get_thread_metadata_safe",
|
||||||
AsyncMock(return_value={"review_check_run_id": 77, "head_sha": "head-sha"}),
|
AsyncMock(return_value={"review_check_run_id": 77, "head_sha": tracked_head}),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
webhook_common, "_resolve_verdict_authorization", AsyncMock(return_value=True)
|
webhook_common, "_resolve_verdict_authorization", AsyncMock(return_value=True)
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(webhook_common, "cache_github_token_for_thread", lambda *a, **k: None)
|
monkeypatch.setattr(webhook_common, "cache_github_token_for_thread", lambda *a, **k: None)
|
||||||
monkeypatch.setattr(webhook_common, "set_reviewer_thread_metadata", AsyncMock())
|
monkeypatch.setattr(webhook_common, "set_reviewer_thread_metadata", set_metadata)
|
||||||
monkeypatch.setattr(webhook_common, "create_review_check_run", created_check)
|
monkeypatch.setattr(webhook_common, "create_review_check_run", created_check)
|
||||||
monkeypatch.setattr(webhook_common, "post_review_started_comment", AsyncMock(return_value=1))
|
monkeypatch.setattr(webhook_common, "post_review_started_comment", AsyncMock(return_value=1))
|
||||||
monkeypatch.setattr(webhook_common, "_store_current_reviewer_run_id", AsyncMock())
|
monkeypatch.setattr(webhook_common, "_store_current_reviewer_run_id", AsyncMock())
|
||||||
|
|
@ -1211,7 +1220,15 @@ def test_trigger_pr_review_from_ref_reuses_open_check_on_same_head(monkeypatch)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
created_check.assert_not_awaited()
|
if tracked_head == "head-sha":
|
||||||
|
created_check.assert_not_awaited()
|
||||||
|
else:
|
||||||
|
created_check.assert_awaited_once()
|
||||||
|
assert created_check.await_args.kwargs["head_sha"] == "head-sha"
|
||||||
|
assert any(
|
||||||
|
call.kwargs.get("extra") == {"review_check_run_id": 88}
|
||||||
|
for call in set_metadata.await_args_list
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_trigger_pr_review_from_ref_threads_verdict_request(monkeypatch) -> None:
|
def test_trigger_pr_review_from_ref_threads_verdict_request(monkeypatch) -> None:
|
||||||
|
|
|
||||||
|
|
@ -2628,8 +2628,13 @@ async def test_publish_async_consistent_approve_clean_posts_approve() -> None:
|
||||||
from agent.tools.publish_review import _publish_review_async
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
post_review = AsyncMock(return_value={"id": 2001, "state": "APPROVED"})
|
post_review = AsyncMock(return_value={"id": 2001, "state": "APPROVED"})
|
||||||
|
settle_check = AsyncMock()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for p in _verdict_publish_patches(findings=[], post_review=post_review):
|
for p in _verdict_publish_patches(
|
||||||
|
findings=[],
|
||||||
|
post_review=post_review,
|
||||||
|
settle_check=settle_check,
|
||||||
|
):
|
||||||
stack.enter_context(p)
|
stack.enter_context(p)
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -2652,6 +2657,7 @@ async def test_publish_async_consistent_approve_clean_posts_approve() -> None:
|
||||||
body = post_review.await_args.kwargs["body"]
|
body = post_review.await_args.kwargs["body"]
|
||||||
assert "Automatic verdict evaluation (`approve`) pending" in body
|
assert "Automatic verdict evaluation (`approve`) pending" in body
|
||||||
assert "requester" not in body
|
assert "requester" not in body
|
||||||
|
assert settle_check.await_args.kwargs["conclusion"] == "success"
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_async_consistent_approve_with_open_findings_downgrades() -> None:
|
async def test_publish_async_consistent_approve_with_open_findings_downgrades() -> None:
|
||||||
|
|
@ -2661,8 +2667,13 @@ async def test_publish_async_consistent_approve_with_open_findings_downgrades()
|
||||||
|
|
||||||
findings = [_f(id="f_1", severity="high", file="a.py", start_line=1, end_line=1)]
|
findings = [_f(id="f_1", severity="high", file="a.py", start_line=1, end_line=1)]
|
||||||
post_review = AsyncMock(return_value={"id": 2002, "state": "COMMENTED"})
|
post_review = AsyncMock(return_value={"id": 2002, "state": "COMMENTED"})
|
||||||
|
settle_check = AsyncMock()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for p in _verdict_publish_patches(findings=findings, post_review=post_review):
|
for p in _verdict_publish_patches(
|
||||||
|
findings=findings,
|
||||||
|
post_review=post_review,
|
||||||
|
settle_check=settle_check,
|
||||||
|
):
|
||||||
stack.enter_context(p)
|
stack.enter_context(p)
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -2683,6 +2694,7 @@ async def test_publish_async_consistent_approve_with_open_findings_downgrades()
|
||||||
assert result["verdict_ignored"] is True
|
assert result["verdict_ignored"] is True
|
||||||
assert result["verdict_ignored_reason"] == "approve_with_open_findings"
|
assert result["verdict_ignored_reason"] == "approve_with_open_findings"
|
||||||
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
||||||
|
assert settle_check.await_args.kwargs["conclusion"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_async_requested_approve_ignores_open_findings_gate() -> None:
|
async def test_publish_async_requested_approve_ignores_open_findings_gate() -> None:
|
||||||
|
|
@ -2722,8 +2734,13 @@ async def test_publish_async_request_changes_maps_event() -> None:
|
||||||
|
|
||||||
findings = [_f(id="f_1", severity="high", file="a.py", start_line=1, end_line=1)]
|
findings = [_f(id="f_1", severity="high", file="a.py", start_line=1, end_line=1)]
|
||||||
post_review = AsyncMock(return_value={"id": 1000, "state": "CHANGES_REQUESTED"})
|
post_review = AsyncMock(return_value={"id": 1000, "state": "CHANGES_REQUESTED"})
|
||||||
|
settle_check = AsyncMock()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for p in _verdict_publish_patches(findings=findings, post_review=post_review):
|
for p in _verdict_publish_patches(
|
||||||
|
findings=findings,
|
||||||
|
post_review=post_review,
|
||||||
|
settle_check=settle_check,
|
||||||
|
):
|
||||||
stack.enter_context(p)
|
stack.enter_context(p)
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -2741,6 +2758,7 @@ async def test_publish_async_request_changes_maps_event() -> None:
|
||||||
assert result["verdict_submitted"] is True
|
assert result["verdict_submitted"] is True
|
||||||
assert result["verdict_event"] == "REQUEST_CHANGES"
|
assert result["verdict_event"] == "REQUEST_CHANGES"
|
||||||
assert post_review.await_args.kwargs["event"] == "REQUEST_CHANGES"
|
assert post_review.await_args.kwargs["event"] == "REQUEST_CHANGES"
|
||||||
|
assert settle_check.await_args.kwargs["conclusion"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_async_self_review_downgrades_verdict_to_comment() -> None:
|
async def test_publish_async_self_review_downgrades_verdict_to_comment() -> None:
|
||||||
|
|
@ -2918,9 +2936,15 @@ async def test_publish_async_verdict_fails_closed_on_unknown_author() -> None:
|
||||||
from agent.tools.publish_review import _publish_review_async
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
post_review = AsyncMock(return_value={"id": 2002, "state": "COMMENTED"})
|
post_review = AsyncMock(return_value={"id": 2002, "state": "COMMENTED"})
|
||||||
|
settle_check = AsyncMock()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
# No pr author in metadata → cannot confirm it isn't a bot self-review.
|
# No pr author in metadata → cannot confirm it isn't a bot self-review.
|
||||||
for p in _verdict_publish_patches(findings=[], post_review=post_review, thread_metadata={}):
|
for p in _verdict_publish_patches(
|
||||||
|
findings=[],
|
||||||
|
post_review=post_review,
|
||||||
|
thread_metadata={},
|
||||||
|
settle_check=settle_check,
|
||||||
|
):
|
||||||
stack.enter_context(p)
|
stack.enter_context(p)
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -2938,6 +2962,7 @@ async def test_publish_async_verdict_fails_closed_on_unknown_author() -> None:
|
||||||
assert result["verdict_ignored"] is True
|
assert result["verdict_ignored"] is True
|
||||||
assert result["verdict_ignored_reason"] == "author_unknown"
|
assert result["verdict_ignored_reason"] == "author_unknown"
|
||||||
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
||||||
|
assert settle_check.await_args.kwargs["conclusion"] == "neutral"
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_async_self_review_guard_is_case_insensitive() -> None:
|
async def test_publish_async_self_review_guard_is_case_insensitive() -> None:
|
||||||
|
|
@ -3192,8 +3217,13 @@ async def test_publish_async_unauthorized_verdict_posts_withheld_empty_re_review
|
||||||
from agent.tools.publish_review import _publish_review_async
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
post_review = AsyncMock(return_value={"id": 4003, "state": "COMMENTED"})
|
post_review = AsyncMock(return_value={"id": 4003, "state": "COMMENTED"})
|
||||||
|
settle_check = AsyncMock()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for patcher in _verdict_publish_patches(findings=[], post_review=post_review):
|
for patcher in _verdict_publish_patches(
|
||||||
|
findings=[],
|
||||||
|
post_review=post_review,
|
||||||
|
settle_check=settle_check,
|
||||||
|
):
|
||||||
stack.enter_context(patcher)
|
stack.enter_context(patcher)
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -3212,6 +3242,7 @@ async def test_publish_async_unauthorized_verdict_posts_withheld_empty_re_review
|
||||||
assert result["verdict_ignored_reason"] == "verdict_not_requested"
|
assert result["verdict_ignored_reason"] == "verdict_not_requested"
|
||||||
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
||||||
assert "Verdict withheld (verdict not requested)" in post_review.await_args.kwargs["body"]
|
assert "Verdict withheld (verdict not requested)" in post_review.await_args.kwargs["body"]
|
||||||
|
assert settle_check.await_args.kwargs["conclusion"] == "neutral"
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_async_failed_live_head_check_downgrades_verdict() -> None:
|
async def test_publish_async_failed_live_head_check_downgrades_verdict() -> None:
|
||||||
|
|
@ -3221,8 +3252,13 @@ async def test_publish_async_failed_live_head_check_downgrades_verdict() -> None
|
||||||
|
|
||||||
post_review = AsyncMock(return_value={"id": 4004, "state": "COMMENTED"})
|
post_review = AsyncMock(return_value={"id": 4004, "state": "COMMENTED"})
|
||||||
update_body = AsyncMock(return_value=False)
|
update_body = AsyncMock(return_value=False)
|
||||||
|
settle_check = AsyncMock()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for patcher in _verdict_publish_patches(findings=[], post_review=post_review):
|
for patcher in _verdict_publish_patches(
|
||||||
|
findings=[],
|
||||||
|
post_review=post_review,
|
||||||
|
settle_check=settle_check,
|
||||||
|
):
|
||||||
stack.enter_context(patcher)
|
stack.enter_context(patcher)
|
||||||
stack.enter_context(
|
stack.enter_context(
|
||||||
patch(
|
patch(
|
||||||
|
|
@ -3252,6 +3288,7 @@ async def test_publish_async_failed_live_head_check_downgrades_verdict() -> None
|
||||||
assert "Verdict withheld (head check failed)" in post_review.await_args.kwargs["body"]
|
assert "Verdict withheld (head check failed)" in post_review.await_args.kwargs["body"]
|
||||||
update_body.assert_not_awaited()
|
update_body.assert_not_awaited()
|
||||||
assert "body_update_failed" not in result
|
assert "body_update_failed" not in result
|
||||||
|
assert settle_check.await_args.kwargs["conclusion"] == "neutral"
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_async_rechecks_live_head_immediately_before_verdict_post() -> None:
|
async def test_publish_async_rechecks_live_head_immediately_before_verdict_post() -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue