Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/sentry/seer/autofix/pr_iteration/listeners/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 13 additions & 3 deletions src/sentry/tasks/seer/pr_iteration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions tests/sentry/seer/autofix/test_pr_iteration_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading