3333 process_github_webhook_event ,
3434)
3535from sentry .testutils .cases import TestCase
36- from sentry .testutils .helpers .features import Feature
36+ from sentry .testutils .helpers .features import with_feature
3737from sentry .testutils .helpers .github import GitHubWebhookTestCase
3838
3939CODE_REVIEW_FEATURES = {"organizations:gen-ai-features" , "organizations:code-review-beta" }
4040
4141DEFAULT_PR_AUTHOR_ID = "12345678"
4242
4343
44- @patch ("sentry.seer.code_review.billing.quotas.backend.check_seer_quota" , return_value = True )
4544class GitHubWebhookHelper (GitHubWebhookTestCase ):
4645 """Base class for GitHub webhook integration tests."""
4746
4847 github_integration : Integration | None = None
4948
49+ @pytest .fixture (autouse = True )
50+ def mock_billing_quota (self ) -> Generator [None ]:
51+ """Mock billing quota check to return True for all tests."""
52+ with patch (
53+ "sentry.seer.code_review.billing.quotas.backend.check_seer_quota" , return_value = True
54+ ):
55+ yield
56+
57+ def _enable_code_review (self ) -> None :
58+ """Enable all required options for code review to work."""
59+ self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
60+
61+ # Setup billing data
62+ self .github_integration = self .create_github_integration ()
63+ OrganizationContributors .objects .get_or_create (
64+ organization_id = self .organization .id ,
65+ integration_id = self .github_integration .id ,
66+ external_identifier = DEFAULT_PR_AUTHOR_ID ,
67+ )
68+
5069 @contextmanager
5170 def code_review_setup (
5271 self , features : Collection [str ] | Mapping [str , Any ] = CODE_REVIEW_FEATURES
5372 ) -> Generator [None ]:
5473 """Helper to set up code review test context."""
5574 self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
75+
76+ # Setup billing data
77+ self .github_integration = self .create_github_integration ()
78+ OrganizationContributors .objects .get_or_create (
79+ organization_id = self .organization .id ,
80+ integration_id = self .github_integration .id ,
81+ external_identifier = DEFAULT_PR_AUTHOR_ID ,
82+ )
83+
5684 with (
5785 self .feature (features ),
5886 self .options ({"github.webhook.issue-comment" : False }),
5987 ):
6088 yield
6189
6290 def _send_webhook_event (
63- self ,
64- github_event : GithubWebhookType ,
65- event_data : bytes | str ,
66- enable_code_review : bool = False ,
67- features : set [str ] | None = None ,
91+ self , github_event : GithubWebhookType , event_data : bytes | str
6892 ) -> HttpResponseBase :
69- if enable_code_review :
70- self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
71- self .github_integration = self .create_github_integration ()
72- OrganizationContributors .objects .get_or_create (
73- organization_id = self .organization .id ,
74- integration_id = self .github_integration .id ,
75- external_identifier = DEFAULT_PR_AUTHOR_ID ,
76- )
77-
93+ """Helper to send a GitHub webhook event."""
7894 self .event_dict = (
7995 orjson .loads (event_data ) if isinstance (event_data , (bytes , str )) else event_data
8096 )
8197 repo_id = int (self .event_dict ["repository" ]["id" ])
8298
8399 integration = self .github_integration or self .create_github_integration ()
84-
85100 self .create_repo (
86101 project = self .project ,
87102 provider = "integrations:github" ,
88103 external_id = repo_id ,
89104 integration_id = integration .id ,
90105 )
91-
92- if enable_code_review :
93- features_to_enable = features if features is not None else CODE_REVIEW_FEATURES
94- with Feature (features_to_enable ):
95- response = self .send_github_webhook_event (github_event , event_data )
96- else :
97- response = self .send_github_webhook_event (github_event , event_data )
98-
106+ response = self .send_github_webhook_event (github_event , event_data )
99107 assert response .status_code == 204
100108 return response
101109
@@ -104,12 +112,13 @@ class CheckRunEventWebhookTest(GitHubWebhookHelper):
104112 """Integration tests for GitHub check_run webhook events."""
105113
106114 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
115+ @with_feature (CODE_REVIEW_FEATURES )
107116 def test_base_case (self , mock_task : MagicMock ) -> None :
108117 """Test that rerequested action enqueues task with correct parameters."""
118+ self ._enable_code_review ()
109119 self ._send_webhook_event (
110120 GithubWebhookType .CHECK_RUN ,
111121 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
112- enable_code_review = True ,
113122 )
114123
115124 mock_task .delay .assert_called_once ()
@@ -126,42 +135,44 @@ def test_base_case(self, mock_task: MagicMock) -> None:
126135 assert isinstance (call_kwargs ["enqueued_at_str" ], str )
127136
128137 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
129- def test_check_run_skips_when_code_review_option_disabled (self , mock_task : MagicMock ) -> None :
130- """Test that the handler skips when preflight requirements are not met."""
138+ @with_feature (CODE_REVIEW_FEATURES )
139+ def test_check_run_skips_when_ai_features_disabled (self , mock_task : MagicMock ) -> None :
140+ """Test that the handler returns early when AI features are not enabled (even though the option is enabled)."""
131141 self ._send_webhook_event (
132142 GithubWebhookType .CHECK_RUN ,
133143 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
134- enable_code_review = False ,
135144 )
136145 mock_task .delay .assert_not_called ()
137146
138147 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
148+ @with_feature (CODE_REVIEW_FEATURES )
139149 def test_check_run_fails_when_action_missing (self , mock_task : MagicMock ) -> None :
140150 """Test that missing action field is handled gracefully without KeyError."""
151+ self ._enable_code_review ()
141152 event_without_action = orjson .loads (CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE )
142153 del event_without_action ["action" ]
143154
144155 with patch ("sentry.seer.code_review.webhooks.check_run.logger" ) as mock_logger :
145156 self ._send_webhook_event (
146157 GithubWebhookType .CHECK_RUN ,
147158 orjson .dumps (event_without_action ),
148- enable_code_review = True ,
149159 )
150160 mock_task .delay .assert_not_called ()
151161 mock_logger .error .assert_called_once ()
152162 assert "github.webhook.check_run.missing-action" in str (mock_logger .error .call_args )
153163
154164 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
165+ @with_feature (CODE_REVIEW_FEATURES )
155166 def test_check_run_fails_when_external_id_missing (self , mock_task : MagicMock ) -> None :
156167 """Test that missing external_id is handled gracefully."""
168+ self ._enable_code_review ()
157169 event_without_external_id = orjson .loads (CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE )
158170 del event_without_external_id ["check_run" ]["external_id" ]
159171
160172 with patch ("sentry.seer.code_review.webhooks.check_run.logger" ) as mock_logger :
161173 self ._send_webhook_event (
162174 GithubWebhookType .CHECK_RUN ,
163175 orjson .dumps (event_without_external_id ),
164- enable_code_review = True ,
165176 )
166177 mock_task .delay .assert_not_called ()
167178 mock_logger .exception .assert_called_once ()
@@ -170,16 +181,17 @@ def test_check_run_fails_when_external_id_missing(self, mock_task: MagicMock) ->
170181 )
171182
172183 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
184+ @with_feature (CODE_REVIEW_FEATURES )
173185 def test_check_run_fails_when_external_id_not_numeric (self , mock_task : MagicMock ) -> None :
174186 """Test that non-numeric external_id is handled gracefully."""
187+ self ._enable_code_review ()
175188 event_with_invalid_external_id = orjson .loads (CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE )
176189 event_with_invalid_external_id ["check_run" ]["external_id" ] = "not-a-number"
177190
178191 with patch ("sentry.seer.code_review.webhooks.check_run.logger" ) as mock_logger :
179192 self ._send_webhook_event (
180193 GithubWebhookType .CHECK_RUN ,
181194 orjson .dumps (event_with_invalid_external_id ),
182- enable_code_review = True ,
183195 )
184196 mock_task .delay .assert_not_called ()
185197 mock_logger .exception .assert_called_once ()
@@ -188,12 +200,13 @@ def test_check_run_fails_when_external_id_not_numeric(self, mock_task: MagicMock
188200 )
189201
190202 @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
203+ @with_feature (CODE_REVIEW_FEATURES )
191204 def test_check_run_enqueues_task_for_processing (self , mock_task : MagicMock ) -> None :
192205 """Test that webhook successfully enqueues task for async processing."""
206+ self ._enable_code_review ()
193207 self ._send_webhook_event (
194208 GithubWebhookType .CHECK_RUN ,
195209 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
196- enable_code_review = True ,
197210 )
198211
199212 mock_task .delay .assert_called_once ()
@@ -212,17 +225,30 @@ def test_check_run_without_integration_returns_204(self) -> None:
212225 )
213226 assert response .status_code == 204
214227
228+ @patch ("sentry.seer.code_review.webhooks.task.process_github_webhook_event" )
229+ @with_feature ({"organizations:gen-ai-features" })
230+ def test_check_run_runs_when_code_review_beta_flag_disabled_but_pr_review_test_generation_enabled (
231+ self , mock_task : MagicMock
232+ ) -> None :
233+ """Test that task is enqueued when code-review-beta flag is off but pr_review_test_generation is enabled."""
234+ self ._enable_code_review ()
235+ self ._send_webhook_event (
236+ GithubWebhookType .CHECK_RUN ,
237+ CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
238+ )
239+ mock_task .delay .assert_called_once ()
240+
215241 @patch ("sentry.seer.code_review.utils.make_seer_request" )
242+ @with_feature (CODE_REVIEW_FEATURES )
216243 def test_check_run_skips_when_hide_ai_features_enabled (
217244 self , mock_make_seer_request : MagicMock
218245 ) -> None :
219246 """Test that task is not enqueued when hide_ai_features option is True."""
220- # Enable hide_ai_features before sending - preflight will fail legal AI consent check
247+ self . _enable_code_review ()
221248 self .organization .update_option ("sentry:hide_ai_features" , True )
222249 self ._send_webhook_event (
223250 GithubWebhookType .CHECK_RUN ,
224251 CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE ,
225- enable_code_review = True ,
226252 )
227253 mock_make_seer_request .assert_not_called ()
228254
@@ -786,7 +812,7 @@ def _build_issue_comment_event(
786812 "number" : 42 ,
787813 "pull_request" : {"url" : "https://api.github.com/repos/owner/repo/pulls/42" },
788814 "user" : {
789- "id" : 12345678 ,
815+ "id" : int ( DEFAULT_PR_AUTHOR_ID ) ,
790816 "login" : "pr-author" ,
791817 },
792818 },
@@ -802,21 +828,6 @@ def _build_issue_comment_event(
802828 }
803829 return orjson .dumps (event )
804830
805- @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
806- def test_skips_when_code_review_not_enabled (self , mock_schedule : MagicMock ) -> None :
807- """Test that issue_comment skips when preflight requirements are not met."""
808- event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
809- self ._send_issue_comment_event (event , enable_code_review = False )
810- mock_schedule .assert_not_called ()
811-
812- @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
813- def test_skips_when_no_review_command (self , mock_schedule : MagicMock ) -> None :
814- """Test that issue_comment skips when the comment doesn't contain the review command."""
815- event = self ._build_issue_comment_event ("This is a regular comment without the command" )
816- self ._send_issue_comment_event (event , enable_code_review = True )
817- mock_schedule .assert_not_called ()
818-
819- @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
820831 def test_skips_when_code_review_features_are_missing (self ) -> None :
821832 """Test that processing is skipped when code review features are missing."""
822833 with self .code_review_setup (features = {}): # Missing on purpose
@@ -828,60 +839,79 @@ def test_skips_when_code_review_features_are_missing(self) -> None:
828839
829840 self .mock_seer .assert_not_called ()
830841
842+ def test_skips_when_no_review_command (self ) -> None :
843+ """Test that processing is skipped when comment doesn't contain review command."""
844+ with self .code_review_setup ():
845+ event = self ._build_issue_comment_event ("This is a regular comment without the command" )
846+
847+ with self .tasks ():
848+ response = self ._send_issue_comment_event (event )
849+ assert response .status_code == 204
850+
851+ self .mock_seer .assert_not_called ()
852+
831853 def test_runs_when_code_review_beta_flag_disabled_but_pr_review_test_generation_enabled (
832854 self ,
833855 ) -> None :
834- """Test that code review works via legacy option even without the beta feature flag."""
835- # Only enable gen-ai-features flag, not code-review-beta
836- with self .options ({"github.webhook.issue-comment" : False }):
837- event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
838- self ._send_issue_comment_event (
839- event ,
840- enable_code_review = True ,
841- features = {"organizations:gen-ai-features" },
842- )
843-
844- @patch ("sentry.seer.code_review.webhooks.task.make_seer_request" )
845- @patch ("sentry.integrations.github.client.GitHubApiClient.create_comment_reaction" )
846- def test_adds_reaction_and_forwards_when_valid (
847- self , mock_create_reaction : MagicMock , mock_seer : MagicMock
848- ) -> None :
849- with self .options ({"github.webhook.issue-comment" : False }):
856+ """Test that processing runs with gen-ai-features flag alone when org option is enabled."""
857+ with self .code_review_setup (features = {"organizations:gen-ai-features" }):
850858 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
851859
852860 with self .tasks ():
853- self ._send_issue_comment_event (event , enable_code_review = True )
854- """Test that processing runs with gen-ai-features flag alone when org option is enabled."""
855- with self .options (
856- {"organizations:code-review-beta" : False , "github.webhook.issue-comment" : False }
857- ):
858- self .organization .update_option ("sentry:enable_pr_review_test_generation" , True )
861+ response = self ._send_issue_comment_event (event )
862+ assert response .status_code == 204
863+
864+ self .mock_seer .assert_called_once ()
865+
866+ def test_adds_reaction_and_forwards_when_valid (self ) -> None :
867+ """Test successful PR review command processing with reaction and Seer request."""
868+ with self .code_review_setup ():
859869 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
860870
861871 with self .tasks ():
862872 response = self ._send_issue_comment_event (event )
863873 assert response .status_code == 204
864874
865- self .mock_seer .assert_called_once ()
875+ self .mock_reaction .assert_called_once_with (
876+ "owner/repo" , "123456789" , GitHubReaction .EYES
877+ )
878+ self .mock_seer .assert_called_once ()
879+
880+ call_args = self .mock_seer .call_args
881+ assert call_args [1 ]["path" ] == "/v1/automation/overwatch-request"
882+ payload = call_args [1 ]["payload" ]
883+ assert payload ["request_type" ] == "pr-review"
884+ assert payload ["data" ]["repo" ]["base_commit_sha" ] == "abc123"
866885
867886 @patch ("sentry.seer.code_review.webhooks.issue_comment._add_eyes_reaction_to_comment" )
868- @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
869- def test_skips_reaction_when_no_comment_id (
870- self , mock_schedule : MagicMock , mock_reaction : MagicMock
871- ) -> None :
872- with self .options ({"github.webhook.issue-comment" : False }):
887+ def test_skips_reaction_when_no_comment_id (self , mock_reaction : MagicMock ) -> None :
888+ """Test that reaction is skipped when comment has no ID, but processing continues."""
889+ with self .code_review_setup ():
873890 event = self ._build_issue_comment_event (SENTRY_REVIEW_COMMAND , comment_id = None )
874- self ._send_issue_comment_event (event , enable_code_review = True )
891+
892+ with self .tasks ():
893+ response = self ._send_issue_comment_event (event )
894+ assert response .status_code == 204
895+
896+ mock_reaction .assert_not_called ()
897+ self .mock_seer .assert_called_once ()
875898
876899 @patch ("sentry.seer.code_review.webhooks.issue_comment._add_eyes_reaction_to_comment" )
877- @patch ("sentry.seer.code_review.webhooks.task.schedule_task" )
878- def test_skips_processing_when_option_is_true (
879- self , mock_schedule : MagicMock , mock_reaction : MagicMock
880- ) -> None :
881- """Test that when github.webhook.issue-comment option is True (default), no processing occurs."""
882- with self .options ({"github.webhook.issue-comment" : True }):
900+ def test_skips_processing_when_option_is_true (self , mock_reaction : MagicMock ) -> None :
901+ """Test that when github.webhook.issue-comment option is True (kill switch), no processing occurs."""
902+ self ._enable_code_review ()
903+ with (
904+ self .feature (CODE_REVIEW_FEATURES ),
905+ self .options ({"github.webhook.issue-comment" : True }),
906+ ):
883907 event = self ._build_issue_comment_event (f"Please { SENTRY_REVIEW_COMMAND } this PR" )
884- self ._send_issue_comment_event (event , enable_code_review = True )
908+
909+ with self .tasks ():
910+ response = self ._send_issue_comment_event (event )
911+ assert response .status_code == 204
912+
913+ mock_reaction .assert_not_called ()
914+ self .mock_seer .assert_not_called ()
885915
886916 def test_validates_seer_request_contains_trigger_metadata (self ) -> None :
887917 """Test that Seer request includes trigger metadata from the comment."""
0 commit comments