diff --git a/agent/reviewer_publish.py b/agent/reviewer_publish.py index ee491002..6dac0c11 100644 --- a/agent/reviewer_publish.py +++ b/agent/reviewer_publish.py @@ -284,9 +284,16 @@ def render_review_body( trace_url: str | None = None, ui_url: str | None = None, out_of_diff_findings: list[Finding] | None = None, + additional_findings_count: int = 0, ) -> str: - """Compose the top-level review body.""" + """Compose the top-level review body. + + When ``surfaced_count`` is 0 but ``additional_findings_count`` is > 0, + the headline says "No issues found" and a second line directs the reader + to the web app for the remaining sub-threshold findings. + """ out_of_diff_findings = out_of_diff_findings or [] + has_additional = additional_findings_count > 0 if surfaced_count == 0 and not out_of_diff_findings: headline = ( "## ✅ Open SWE Review: No issues found\n\n" @@ -299,6 +306,9 @@ def render_review_body( headline = f"**Open SWE Review** found {surfaced_count} potential {issue_word}." parts = [headline] + if has_additional: + noun = "finding" if additional_findings_count == 1 else "findings" + parts.append(f"{additional_findings_count} additional {noun} can be viewed in the web app.") if out_of_diff_findings: parts.append(render_out_of_diff_section(out_of_diff_findings)) links = [] diff --git a/agent/tools/publish_review.py b/agent/tools/publish_review.py index c15b90ec..2e27095b 100644 --- a/agent/tools/publish_review.py +++ b/agent/tools/publish_review.py @@ -10,6 +10,7 @@ from langgraph.config import get_config from ..dashboard.team_settings import get_team_review_trace_links_enabled from ..reviewer_diff import compute_diff_line_set, fetch_pr_diff, is_range_in_diff from ..reviewer_findings import ( + SEVERITY_ORDER, Finding, ReviewerThreadMissingError, Severity, @@ -72,9 +73,10 @@ def publish_review( GitHub Review but still resolves fixed threads and updates reviewer state. Args: - severity_threshold: Lowest severity to surface to GitHub (default - ``medium``). Lower-severity findings stay in state and surface in - the future UI but not on the PR. + severity_threshold: Lowest severity to surface as inline GitHub comments + (default ``medium``). Lower-severity findings stay in state and are + mentioned in the review summary with a link to the web app, but are + not posted as inline PR comments. cap: Maximum number of inline comments to publish (default 4). Returns: @@ -268,6 +270,16 @@ async def _publish_review_async( in_diff_unpublished, severity_threshold=severity_threshold, cap=cap ) + severity_rank = SEVERITY_ORDER[severity_threshold] + eligible_ids = {f.get("id") for f in eligible} + additional_findings_count = sum( + 1 + for f in in_diff_unpublished + if f.get("id") not in eligible_ids + and f.get("status", "open") == "open" + and SEVERITY_ORDER.get(f.get("severity", "low"), 0) < severity_rank + ) + inline_comments: list[dict[str, Any]] = [] eligible_with_payload: list[tuple[dict[str, Any], dict[str, Any]]] = [] for finding in eligible: @@ -328,6 +340,7 @@ async def _publish_review_async( surfaced_count=len(inline_comments), trace_url=review_trace_url, ui_url=review_ui_url, + additional_findings_count=additional_findings_count, ) review_response = await post_pull_request_review( @@ -362,6 +375,7 @@ async def _publish_review_async( surfaced_count=len(retry_inline), trace_url=review_trace_url, ui_url=review_ui_url, + additional_findings_count=additional_findings_count, ) retry_response = await post_pull_request_review( owner=owner, diff --git a/tests/test_reviewer_publish.py b/tests/test_reviewer_publish.py index 045f576e..12529d40 100644 --- a/tests/test_reviewer_publish.py +++ b/tests/test_reviewer_publish.py @@ -212,6 +212,40 @@ def test_render_review_body_no_findings_message() -> None: body = render_review_body(pr_number=99, surfaced_count=0) assert "## ✅ Open SWE Review: No issues found" in body assert "Open SWE reviewed this PR and found no potential bugs to report." in body + assert "additional" not in body + + +def test_render_review_body_with_additional_findings_and_ui_link() -> None: + body = render_review_body( + pr_number=99, + surfaced_count=0, + additional_findings_count=2, + ui_url="https://dash.example/agents/reviews/o/r/99", + ) + assert "## ✅ Open SWE Review: No issues found" in body + assert "2 additional findings can be viewed in the web app." in body + assert "[Open in Web](https://dash.example/agents/reviews/o/r/99)" in body + + +def test_render_review_body_with_single_additional_finding_uses_singular() -> None: + body = render_review_body(pr_number=99, surfaced_count=0, additional_findings_count=1) + assert "1 additional finding can be viewed in the web app." in body + + +def test_render_review_body_with_surfaced_and_additional_findings() -> None: + body = render_review_body( + pr_number=99, + surfaced_count=3, + additional_findings_count=2, + ui_url="https://dash.example/agents/reviews/o/r/99", + ) + assert "found 3 potential issues." in body + assert "2 additional findings can be viewed in the web app." in body + + +def test_render_review_body_additional_findings_zero_omits_line() -> None: + body = render_review_body(pr_number=99, surfaced_count=0, additional_findings_count=0) + assert "additional" not in body def test_render_status_comment_reviewing_includes_ui_link(monkeypatch: Any) -> None: @@ -387,6 +421,51 @@ def test_publish_review_eval_mode_does_not_call_github() -> None: set_meta.assert_awaited_once_with("tid", last_reviewed_sha="sha") +@pytest.mark.asyncio +async def test_publish_review_surfaces_additional_findings_count_in_body() -> None: + """When all surfaced findings are above threshold but sub-threshold findings + exist, the review body must mention how many additional findings are in the + web app.""" + from agent.tools.publish_review import _publish_review_async + + findings = [ + _f(id="f_low_1", severity="low", file="a.py", start_line=1, end_line=1), + _f(id="f_low_2", severity="low", file="b.py", start_line=2, end_line=2), + ] + post_review = AsyncMock(return_value={"id": 555}) + fetch_comments = AsyncMock(return_value=[]) + + with ( + patch("agent.tools.publish_review.get_thread_id_from_runtime", return_value="tid"), + patch("agent.tools.publish_review.list_findings_async", AsyncMock(return_value=findings)), + patch("agent.tools.publish_review.post_pull_request_review", post_review), + patch("agent.tools.publish_review.fetch_review_comments", fetch_comments), + patch( + "agent.tools.publish_review._resolve_threads_for_resolved_findings", + new_callable=AsyncMock, + return_value=0, + ), + patch("agent.tools.publish_review.set_reviewer_thread_metadata", AsyncMock()), + patch("agent.tools.publish_review._maybe_post_slack_completion_reply", AsyncMock()), + ): + result = await _publish_review_async( + owner="o", + repo="r", + pr_number=7, + head_sha="sha", + token="t", + severity_threshold="medium", + cap=15, + is_re_review=False, + ) + + assert result["success"] is True + assert result["surfaced_count"] == 0 + posted_body = post_review.await_args.kwargs["body"] + assert "No issues found" in posted_body + assert "2 additional findings can be viewed in the web app." in posted_body + + def test_publish_review_forwards_trace_link_config_override() -> None: from agent.tools.publish_review import publish_review