1414)
1515from sentry .integrations .github .client import GitHubReaction
1616from sentry .integrations .github .webhook_types import GithubWebhookType
17+ from sentry .integrations .models .integration import Integration
18+ from sentry .models .organizationcontributors import OrganizationContributors
1719from sentry .seer .code_review .utils import ClientError
1820from sentry .seer .code_review .webhooks .check_run import GitHubCheckRunAction
1921from sentry .seer .code_review .webhooks .issue_comment import (
2830 process_github_webhook_event ,
2931)
3032from sentry .testutils .cases import TestCase
31- from sentry .testutils .helpers .features import with_feature
33+ from sentry .testutils .helpers .features import Feature
3234from sentry .testutils .helpers .github import GitHubWebhookTestCase
3335
3436CODE_REVIEW_FEATURES = {"organizations:gen-ai-features" , "organizations:code-review-beta" }
3537
38+ DEFAULT_PR_AUTHOR_ID = "12345678"
39+
3640
3741@patch ("sentry.seer.code_review.billing.quotas.backend.check_seer_quota" , return_value = True )
3842class GitHubWebhookHelper (GitHubWebhookTestCase ):
3943 """Base class for GitHub webhook integration tests."""
4044
41- def _enable_code_review (self ) -> None :
42- """Enable all required options for code review to work."""
43- self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
44-
45- def _setup_billing (self , integration_id : int , sender_id : str ) -> None :
46- """Set up billing by creating OrganizationContributors record."""
47- from sentry .models .organizationcontributors import OrganizationContributors
48-
49- OrganizationContributors .objects .get_or_create (
50- organization_id = self .organization .id ,
51- integration_id = integration_id ,
52- external_identifier = sender_id ,
53- )
45+ github_integration : Integration | None = None
5446
5547 def _send_webhook_event (
56- self , github_event : GithubWebhookType , event_data : bytes | str
48+ self ,
49+ github_event : GithubWebhookType ,
50+ event_data : bytes | str ,
51+ enable_code_review : bool = False ,
52+ features : set [str ] | None = None ,
5753 ) -> HttpResponseBase :
58- """Helper to send a GitHub webhook event."""
54+ if enable_code_review :
55+ self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
56+ self .github_integration = self .create_github_integration ()
57+ OrganizationContributors .objects .get_or_create (
58+ organization_id = self .organization .id ,
59+ integration_id = self .github_integration .id ,
60+ external_identifier = DEFAULT_PR_AUTHOR_ID ,
61+ )
62+
5963 self .event_dict = (
6064 orjson .loads (event_data ) if isinstance (event_data , (bytes , str )) else event_data
6165 )
6266 repo_id = int (self .event_dict ["repository" ]["id" ])
6367
64- integration = self .create_github_integration ()
68+ integration = self .github_integration or self .create_github_integration ()
69+
6570 self .create_repo (
6671 project = self .project ,
6772 provider = "integrations:github" ,
6873 external_id = repo_id ,
6974 integration_id = integration .id ,
7075 )
7176
72- # Set up billing using PR author ID (matches _get_pr_author_id in handlers.py)
73- pr_author_id = None
74- # Check issue.user.id first (for issue comments)
75- issue_user_id = self .event_dict .get ("issue" , {}).get ("user" , {}).get ("id" )
76- if issue_user_id is not None :
77- pr_author_id = str (issue_user_id )
78- # Then check pull_request.user.id
79- elif self .event_dict .get ("pull_request" , {}).get ("user" , {}).get ("id" ) is not None :
80- pr_author_id = str (self .event_dict ["pull_request" ]["user" ]["id" ])
81- # Finally check user.id (fallback)
82- elif self .event_dict .get ("user" , {}).get ("id" ) is not None :
83- pr_author_id = str (self .event_dict ["user" ]["id" ])
84-
85- if pr_author_id is not None :
86- self ._setup_billing (integration .id , pr_author_id )
87-
88- response = self .send_github_webhook_event (github_event , event_data )
77+ if enable_code_review :
78+ features_to_enable = features if features is not None else CODE_REVIEW_FEATURES
79+ with Feature (features_to_enable ):
80+ response = self .send_github_webhook_event (github_event , event_data )
81+ else :
82+ response = self .send_github_webhook_event (github_event , event_data )
83+
8984 assert response .status_code == 204
9085 return response
9186
@@ -94,13 +89,12 @@ class CheckRunEventWebhookTest(GitHubWebhookHelper):
9489 """Integration tests for GitHub check_run webhook events."""
9590
9691 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
97- @with_feature (CODE_REVIEW_FEATURES )
9892 def test_base_case (self , mock_task : MagicMock ) -> None :
9993 """Test that rerequested action enqueues task with correct parameters."""
100- self ._enable_code_review ()
10194 self ._send_webhook_event (
10295 GithubWebhookType .CHECK_RUN ,
10396 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
97+ enable_code_review = True ,
10498 )
10599
106100 mock_task .delay .assert_called_once ()
@@ -117,44 +111,42 @@ def test_base_case(self, mock_task: MagicMock) -> None:
117111 assert isinstance (call_kwargs ["enqueued_at_str" ], str )
118112
119113 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
120- @with_feature (CODE_REVIEW_FEATURES )
121- def test_check_run_skips_when_ai_features_disabled (self , mock_task : MagicMock ) -> None :
122- """Test that the handler returns early when AI features are not enabled (even though the option is enabled)."""
114+ def test_check_run_skips_when_code_review_option_disabled (self , mock_task : MagicMock ) -> None :
115+ """Test that the handler skips when preflight requirements are not met."""
123116 self ._send_webhook_event (
124117 GithubWebhookType .CHECK_RUN ,
125118 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
119+ enable_code_review = False ,
126120 )
127121 mock_task .delay .assert_not_called ()
128122
129123 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
130- @with_feature (CODE_REVIEW_FEATURES )
131124 def test_check_run_fails_when_action_missing (self , mock_task : MagicMock ) -> None :
132125 """Test that missing action field is handled gracefully without KeyError."""
133- self ._enable_code_review ()
134126 event_without_action = orjson .loads (CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE )
135127 del event_without_action ["action" ]
136128
137129 with patch ("sentry.seer.code_review.webhooks.check_run.logger" ) as mock_logger :
138130 self ._send_webhook_event (
139131 GithubWebhookType .CHECK_RUN ,
140132 orjson .dumps (event_without_action ),
133+ enable_code_review = True ,
141134 )
142135 mock_task .delay .assert_not_called ()
143136 mock_logger .error .assert_called_once ()
144137 assert "github.webhook.check_run.missing-action" in str (mock_logger .error .call_args )
145138
146139 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
147- @with_feature (CODE_REVIEW_FEATURES )
148140 def test_check_run_fails_when_external_id_missing (self , mock_task : MagicMock ) -> None :
149141 """Test that missing external_id is handled gracefully."""
150- self ._enable_code_review ()
151142 event_without_external_id = orjson .loads (CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE )
152143 del event_without_external_id ["check_run" ]["external_id" ]
153144
154145 with patch ("sentry.seer.code_review.webhooks.check_run.logger" ) as mock_logger :
155146 self ._send_webhook_event (
156147 GithubWebhookType .CHECK_RUN ,
157148 orjson .dumps (event_without_external_id ),
149+ enable_code_review = True ,
158150 )
159151 mock_task .delay .assert_not_called ()
160152 mock_logger .exception .assert_called_once ()
@@ -163,17 +155,16 @@ def test_check_run_fails_when_external_id_missing(self, mock_task: MagicMock) ->
163155 )
164156
165157 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
166- @with_feature (CODE_REVIEW_FEATURES )
167158 def test_check_run_fails_when_external_id_not_numeric (self , mock_task : MagicMock ) -> None :
168159 """Test that non-numeric external_id is handled gracefully."""
169- self ._enable_code_review ()
170160 event_with_invalid_external_id = orjson .loads (CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE )
171161 event_with_invalid_external_id ["check_run" ]["external_id" ] = "not-a-number"
172162
173163 with patch ("sentry.seer.code_review.webhooks.check_run.logger" ) as mock_logger :
174164 self ._send_webhook_event (
175165 GithubWebhookType .CHECK_RUN ,
176166 orjson .dumps (event_with_invalid_external_id ),
167+ enable_code_review = True ,
177168 )
178169 mock_task .delay .assert_not_called ()
179170 mock_logger .exception .assert_called_once ()
@@ -182,13 +173,12 @@ def test_check_run_fails_when_external_id_not_numeric(self, mock_task: MagicMock
182173 )
183174
184175 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
185- @with_feature (CODE_REVIEW_FEATURES )
186176 def test_check_run_enqueues_task_for_processing (self , mock_task : MagicMock ) -> None :
187177 """Test that webhook successfully enqueues task for async processing."""
188- self ._enable_code_review ()
189178 self ._send_webhook_event (
190179 GithubWebhookType .CHECK_RUN ,
191180 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
181+ enable_code_review = True ,
192182 )
193183
194184 mock_task .delay .assert_called_once ()
@@ -207,30 +197,17 @@ def test_check_run_without_integration_returns_204(self) -> None:
207197 )
208198 assert response .status_code == 204
209199
210- @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
211- @with_feature ({"organizations:gen-ai-features" })
212- def test_check_run_runs_when_code_review_beta_flag_disabled_but_pr_review_test_generation_enabled (
213- self , mock_task : MagicMock
214- ) -> None :
215- """Test that task is enqueued when code-review-beta flag is off but pr_review_test_generation is enabled."""
216- self ._enable_code_review ()
217- self ._send_webhook_event (
218- GithubWebhookType .CHECK_RUN ,
219- CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
220- )
221- mock_task .delay .assert_called_once ()
222-
223200 @patch ("sentry.seer.code_review.utils.make_seer_request" )
224- @with_feature (CODE_REVIEW_FEATURES )
225201 def test_check_run_skips_when_hide_ai_features_enabled (
226202 self , mock_make_seer_request : MagicMock
227203 ) -> None :
228204 """Test that task is not enqueued when hide_ai_features option is True."""
229- self . _enable_code_review ()
205+ # Enable hide_ai_features before sending - preflight will fail legal AI consent check
230206 self .organization .update_option ("sentry:hide_ai_features" , True )
231207 self ._send_webhook_event (
232208 GithubWebhookType .CHECK_RUN ,
233209 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
210+ enable_code_review = True ,
234211 )
235212 mock_make_seer_request .assert_not_called ()
236213
@@ -723,8 +700,18 @@ def test_false_cases(self) -> None:
723700class IssueCommentEventWebhookTest (GitHubWebhookHelper ):
724701 """Integration tests for GitHub issue_comment webhook events."""
725702
726- def _send_issue_comment_event (self , event_data : bytes | str ) -> HttpResponseBase :
727- return self ._send_webhook_event (GithubWebhookType .ISSUE_COMMENT , event_data )
703+ def _send_issue_comment_event (
704+ self ,
705+ event_data : bytes | str ,
706+ enable_code_review : bool = False ,
707+ features : set [str ] | None = None ,
708+ ) -> HttpResponseBase :
709+ return self ._send_webhook_event (
710+ GithubWebhookType .ISSUE_COMMENT ,
711+ event_data ,
712+ enable_code_review = enable_code_review ,
713+ features = features ,
714+ )
728715
729716 def _build_issue_comment_event (
730717 self , comment_body : str , comment_id : int | None = 123456789
@@ -757,72 +744,68 @@ def _build_issue_comment_event(
757744
758745 @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
759746 def test_skips_when_code_review_not_enabled (self , mock_schedule : MagicMock ) -> None :
747+ """Test that issue_comment skips when preflight requirements are not met."""
760748 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
761- self ._send_issue_comment_event (event )
749+ self ._send_issue_comment_event (event , enable_code_review = False )
762750 mock_schedule .assert_not_called ()
763751
764752 @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
765- @with_feature ({"organizations:gen-ai-features" , "organizations:code-review-beta" })
766753 def test_skips_when_no_review_command (self , mock_schedule : MagicMock ) -> None :
767- self . _enable_code_review ()
754+ """Test that issue_comment skips when the comment doesn't contain the review command."""
768755 event = self ._build_issue_comment_event ("This is a regular comment without the command" )
769- self ._send_issue_comment_event (event )
756+ self ._send_issue_comment_event (event , enable_code_review = True )
770757 mock_schedule .assert_not_called ()
771758
772759 @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
773- @with_feature ({"organizations:gen-ai-features" })
774760 def test_runs_when_code_review_beta_flag_disabled_but_pr_review_test_generation_enabled (
775761 self , mock_schedule : MagicMock
776762 ) -> None :
777- with self .options (
778- {"organizations:code-review-beta" : False , "github.webhook.issue-comment" : False }
779- ):
780- self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
763+ """Test that code review works via legacy option even without the beta feature flag."""
764+ # Only enable gen-ai-features flag, not code-review-beta
765+ with self .options ({"github.webhook.issue-comment" : False }):
781766 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
782- self ._send_issue_comment_event (event )
767+ self ._send_issue_comment_event (
768+ event ,
769+ enable_code_review = True ,
770+ features = {"organizations:gen-ai-features" },
771+ )
783772 mock_schedule .assert_called_once ()
784773
785774 @patch ("sentry.seer.code_review.webhooks.task.make_seer_request" )
786775 @patch ("sentry.integrations.github.client.GitHubApiClient.create_comment_reaction" )
787- @with_feature ({"organizations:gen-ai-features" , "organizations:code-review-beta" })
788776 def test_adds_reaction_and_forwards_when_valid (
789777 self , mock_create_reaction : MagicMock , mock_seer : MagicMock
790778 ) -> None :
791- self ._enable_code_review ()
792779 with self .options ({"github.webhook.issue-comment" : False }):
793780 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
794781
795782 with self .tasks ():
796- self ._send_issue_comment_event (event )
783+ self ._send_issue_comment_event (event , enable_code_review = True )
797784
798785 mock_create_reaction .assert_called_once ()
799786 mock_seer .assert_called_once ()
800787
801788 @patch ("sentry.seer.code_review.webhooks.issue_comment._add_eyes_reaction_to_comment" )
802789 @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
803- @with_feature ({"organizations:gen-ai-features" , "organizations:code-review-beta" })
804790 def test_skips_reaction_when_no_comment_id (
805791 self , mock_schedule : MagicMock , mock_reaction : MagicMock
806792 ) -> None :
807- self ._enable_code_review ()
808793 with self .options ({"github.webhook.issue-comment" : False }):
809794 event = self ._build_issue_comment_event (SENTRY_REVIEW_COMMAND , comment_id = None )
810- self ._send_issue_comment_event (event )
795+ self ._send_issue_comment_event (event , enable_code_review = True )
811796
812797 mock_reaction .assert_not_called ()
813798 mock_schedule .assert_called_once ()
814799
815800 @patch ("sentry.seer.code_review.webhooks.issue_comment._add_eyes_reaction_to_comment" )
816801 @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
817- @with_feature ({"organizations:gen-ai-features" , "organizations:code-review-beta" })
818802 def test_skips_processing_when_option_is_true (
819803 self , mock_schedule : MagicMock , mock_reaction : MagicMock
820804 ) -> None :
821805 """Test that when github.webhook.issue-comment option is True (default), no processing occurs."""
822- self ._enable_code_review ()
823806 with self .options ({"github.webhook.issue-comment" : True }):
824807 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
825- self ._send_issue_comment_event (event )
808+ self ._send_issue_comment_event (event , enable_code_review = True )
826809
827810 mock_reaction .assert_not_called ()
828811 mock_schedule .assert_not_called ()
0 commit comments