From aeaac9c618b9deed43c7ca4700d5e322bf9f1532 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Wed, 9 Sep 2026 13:11:28 -0400 Subject: [PATCH] fix(pr-iter): ignore bot reviews with no inline comments Bots that approve a PR with only a summary body ("this pr looks good to me!") were treated as review feedback and drove an Autofix iteration. Drop those reviews before the body fetch. Human summary-only reviews are still real feedback and keep iterating. Claude-Session: https://claude.ai/code/session_018aDv6h3DLDkVVfgTi7A1RJ --- .../autofix/pr_iteration/listeners/review.py | 3 +- src/sentry/tasks/seer/pr_iteration.py | 16 ++++++-- .../seer/autofix/test_pr_iteration_review.py | 41 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/sentry/seer/autofix/pr_iteration/listeners/review.py b/src/sentry/seer/autofix/pr_iteration/listeners/review.py index 558a978897aa..ada79fc1c88a 100644 --- a/src/sentry/seer/autofix/pr_iteration/listeners/review.py +++ b/src/sentry/seer/autofix/pr_iteration/listeners/review.py @@ -19,7 +19,8 @@ which fetches the review's inline comments and summary body and dispatches an Autofix PR iteration. The task gates human review authors on repo write access, so a human review only drives an iteration when its author could push the change -themselves; bot reviews are instead bounded by the automated-iteration cap. +themselves. Bot reviews are instead dropped when they have no inline comments, and +bounded by the automated-iteration cap. """ from __future__ import annotations diff --git a/src/sentry/tasks/seer/pr_iteration.py b/src/sentry/tasks/seer/pr_iteration.py index 3bc5b5936faa..73060315a824 100644 --- a/src/sentry/tasks/seer/pr_iteration.py +++ b/src/sentry/tasks/seer/pr_iteration.py @@ -1470,9 +1470,9 @@ def trigger_pr_iteration_from_review( review author must have repo write/admin access, so an untrusted reviewer can't spend Autofix quota or inject feedback that rewrites the PR. - ``author_is_bot`` reviews (test-coverage bots and the like) count toward the - automated-iteration streak cap and are dropped once it's reached; human - reviews always drive an iteration and reset that streak. + ``author_is_bot`` reviews (test-coverage bots and the like) are dropped when + they have no inline comments, and count toward the automated-iteration streak + cap. Human reviews always drive an iteration and reset that streak. """ log_extra = { "organization_id": organization_id, @@ -1578,6 +1578,16 @@ def trigger_pr_iteration_from_review( ) inline_comments = _fetch_all_review_comments(scm, pr_number=pr_number, review_id=review_id) + + # A bot review with no inline comments has nothing to act on; a human summary does. + if author_is_bot and not inline_comments: + metrics.incr("autofix.pr_iteration.review_trigger.bot_review_no_inline_comments") + logger.info( + "autofix.pr_iteration.review_trigger.bot_review_no_inline_comments", + extra=log_extra, + ) + return None + review = _fetch_review_body(scm, pr_number=pr_number, review_id=review_id) review_body = (review.get("body") or "").strip() if review else None review_html_url = review.get("html_url") if review else None diff --git a/tests/sentry/seer/autofix/test_pr_iteration_review.py b/tests/sentry/seer/autofix/test_pr_iteration_review.py index ffee8fb75492..3eb34ee20d91 100644 --- a/tests/sentry/seer/autofix/test_pr_iteration_review.py +++ b/tests/sentry/seer/autofix/test_pr_iteration_review.py @@ -607,6 +607,47 @@ def test_looks_good_review_is_not_skipped(self) -> None: assert isinstance(source, GithubPrReviewBodyFeedbackSource) assert source.body == "looks good" + def test_bot_review_without_inline_comments_is_skipped(self) -> None: + # A bot approval with only a summary body carries nothing to act on. + self._run(author_is_bot=True) + + self.mock_enqueue.assert_not_called() + self.mock_consume.assert_not_called() + # The bail happens before the body fetch. + self.mock_actions.get_pull_request_review.assert_not_called() + + def test_bot_review_with_inline_comments_still_iterates(self) -> None: + self.mock_actions.get_review_comments.return_value = self._paginated( + [self._review_comment(comment_id="1", body="fix this")] + ) + self.mock_actions.get_pull_request_review.return_value = self._review_result( + {"id": "500", "html_url": "https://x/500", "body": "one nit below"} + ) + + self._run(author_is_bot=True) + + # The inline comment and the summary body each become a feedback source. + assert self.mock_enqueue.call_count == 2 + sources = [c.kwargs["feedback"].source for c in self.mock_enqueue.call_args_list] + assert len([s for s in sources if isinstance(s, GithubPrReviewCommentFeedbackSource)]) == 1 + assert len([s for s in sources if isinstance(s, GithubPrReviewBodyFeedbackSource)]) == 1 + self.mock_consume.assert_called_once() + self.mock_actions.create_review_comment_reaction.assert_called_once() + assert self.mock_actions.create_review_comment_reaction.call_args.args[3] == "eyes" + + def test_human_review_without_inline_comments_still_iterates(self) -> None: + # The bot guard must not touch a human summary-only review. + self.mock_actions.get_pull_request_review.return_value = self._review_result( + {"id": "500", "html_url": "https://x/500", "body": "this pr looks good to me!"} + ) + + self._run(author_is_bot=False) + + self.mock_enqueue.assert_called_once() + source = self.mock_enqueue.call_args.kwargs["feedback"].source + assert isinstance(source, GithubPrReviewBodyFeedbackSource) + self.mock_consume.assert_called_once() + def test_review_not_found_still_processes_inline_comments(self) -> None: # If the review is gone (deleted/dismissed between webhook and task) the # direct fetch 404s; we treat it as no body but still act on the inline