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
21 changes: 19 additions & 2 deletions src/sentry/seer/code_review/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from pydantic import ValidationError

from sentry import features
from sentry.constants import ENABLE_PR_REVIEW_TEST_GENERATION_DEFAULT, HIDE_AI_FEATURES_DEFAULT
from sentry.models.organization import Organization
from sentry.utils import metrics

Expand Down Expand Up @@ -91,7 +92,23 @@ def _should_handle_github_check_run_event(organization: Organization, action: st
"""
Determine if the GitHub check_run event should be handled.
"""
if not features.has("organizations:code-review-beta", organization=organization):
if action != GitHubCheckRunAction.REREQUESTED:
return False

return action == GitHubCheckRunAction.REREQUESTED
if not features.has("organizations:gen-ai-features", organization):
return False

hide_ai_features = organization.get_option("sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT)
if hide_ai_features:
return False

pr_review_test_generation_enabled = bool(
organization.get_option(
"sentry:enable_pr_review_test_generation",
ENABLE_PR_REVIEW_TEST_GENERATION_DEFAULT,
)
)
if not pr_review_test_generation_enabled:
return False

return features.has("organizations:code-review-beta", organization)
77 changes: 51 additions & 26 deletions tests/sentry/seer/code_review/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
class CheckRunEventWebhookTest(GitHubWebhookTestCase):
"""Integration tests for GitHub check_run webhook events."""

def _enable_code_review(self) -> None:
"""Enable all required options for code review to work."""
self.organization.update_option("sentry:enable_pr_review_test_generation", True)

def _send_check_run_event(self, event_data: bytes | str) -> HttpResponseBase:
"""Helper to send check_run event with Pydantic validation."""
self.event_dict = (
Expand All @@ -22,7 +26,6 @@ def _send_check_run_event(self, event_data: bytes | str) -> HttpResponseBase:
repo_id = int(self.event_dict["repository"]["id"])

integration = self.create_github_integration()
# Create a repository that matches the fixture's repository ID
self.create_repo(
project=self.project,
provider="integrations:github",
Expand All @@ -34,20 +37,20 @@ def _send_check_run_event(self, event_data: bytes | str) -> HttpResponseBase:
return response

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:code-review-beta"})
@with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"})
def test_base_case(self, mock_task: MagicMock) -> None:
"""Test that rerequested action enqueues task with correct parameters."""
with self.options({"coding_workflows.code_review.github.check_run.rerun.enabled": True}):
self._send_check_run_event(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)

mock_task.delay.assert_called_once()
call_kwargs = mock_task.delay.call_args[1]
assert call_kwargs["original_run_id"] == self.event_dict["check_run"]["external_id"]
assert call_kwargs["organization_id"] == self.organization.id
assert call_kwargs["action"] == "rerequested"
assert call_kwargs["html_url"] == self.event_dict["check_run"]["html_url"]
assert "enqueued_at_str" in call_kwargs
assert isinstance(call_kwargs["enqueued_at_str"], str)
self._enable_code_review()
self._send_check_run_event(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)

mock_task.delay.assert_called_once()
call_kwargs = mock_task.delay.call_args[1]
assert call_kwargs["original_run_id"] == self.event_dict["check_run"]["external_id"]
assert call_kwargs["organization_id"] == self.organization.id
assert call_kwargs["action"] == "rerequested"
assert call_kwargs["html_url"] == self.event_dict["check_run"]["html_url"]
assert "enqueued_at_str" in call_kwargs
assert isinstance(call_kwargs["enqueued_at_str"], str)

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
def test_check_run_skips_when_ai_features_disabled(self, mock_task: MagicMock) -> None:
Expand All @@ -56,57 +59,55 @@ def test_check_run_skips_when_ai_features_disabled(self, mock_task: MagicMock) -
mock_task.delay.assert_not_called()

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:code-review-beta"})
@with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"})
def test_check_run_fails_when_action_missing(self, mock_task: MagicMock) -> None:
"""Test that missing action field is handled gracefully without KeyError."""
self._enable_code_review()
event_without_action = orjson.loads(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)
del event_without_action["action"]

with patch("sentry.seer.code_review.webhooks.logger") as mock_logger:
self._send_check_run_event(orjson.dumps(event_without_action))

mock_task.delay.assert_not_called()
mock_logger.error.assert_called_once_with("github.webhook.check_run.missing-action")

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:code-review-beta"})
@with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"})
def test_check_run_fails_when_external_id_missing(self, mock_task: MagicMock) -> None:
"""Test that missing external_id is handled gracefully."""
self._enable_code_review()
event_without_external_id = orjson.loads(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)
del event_without_external_id["check_run"]["external_id"]

with patch("sentry.seer.code_review.webhooks.logger") as mock_logger:
self._send_check_run_event(orjson.dumps(event_without_external_id))

mock_task.delay.assert_not_called()
# Validation errors are logged with exception
mock_logger.exception.assert_called_once()
assert (
"github.webhook.check_run.invalid-payload" in mock_logger.exception.call_args[0][0]
)

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:code-review-beta"})
@with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"})
def test_check_run_fails_when_external_id_not_numeric(self, mock_task: MagicMock) -> None:
"""Test that non-numeric external_id is handled gracefully."""
self._enable_code_review()
event_with_invalid_external_id = orjson.loads(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)
event_with_invalid_external_id["check_run"]["external_id"] = "not-a-number"

with patch("sentry.seer.code_review.webhooks.logger") as mock_logger:
self._send_check_run_event(orjson.dumps(event_with_invalid_external_id))

mock_task.delay.assert_not_called()
# ValueError raised for non-numeric external_id
mock_logger.exception.assert_called_once()
assert (
"github.webhook.check_run.invalid-payload" in mock_logger.exception.call_args[0][0]
)

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:code-review-beta"})
@with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"})
def test_check_run_enqueues_task_for_processing(self, mock_task: MagicMock) -> None:
"""Test that webhook successfully enqueues task for async processing."""

self._enable_code_review()
self._send_check_run_event(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)

mock_task.delay.assert_called_once()
Expand All @@ -116,8 +117,32 @@ def test_check_run_enqueues_task_for_processing(self, mock_task: MagicMock) -> N

def test_check_run_without_integration_returns_204(self) -> None:
"""Test that check_run events without integration return 204."""
# Don't create an integration, just send the event
response = self.send_github_webhook_event("check_run", CHECK_RUN_COMPLETED_EVENT_EXAMPLE)

# Should still return 204 even without integration
assert response.status_code == 204

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:gen-ai-features"})
def test_check_run_skips_when_code_review_beta_flag_disabled(
self, mock_task: MagicMock
) -> None:
"""Test that task is not enqueued when code-review-beta flag is off."""
self._enable_code_review()
self._send_check_run_event(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)
mock_task.delay.assert_not_called()

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:gen-ai-features"})
def test_check_run_skips_when_pr_review_option_disabled(self, mock_task: MagicMock) -> None:
"""Test that task is not enqueued when pr_review option is off."""
self.organization.update_option("sentry:enable_pr_review_test_generation", False)
self._send_check_run_event(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)
mock_task.delay.assert_not_called()

@patch("sentry.seer.code_review.webhooks.process_github_webhook_event")
@with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"})
def test_check_run_skips_when_hide_ai_features_enabled(self, mock_task: MagicMock) -> None:
"""Test that task is not enqueued when hide_ai_features option is True."""
self._enable_code_review()
self.organization.update_option("sentry:hide_ai_features", True)
self._send_check_run_event(CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE)
mock_task.delay.assert_not_called()
Loading