@@ -641,11 +641,16 @@ def test_get_issue_summary_continues_when_automation_fails(
641641 )
642642 mock_call_seer .return_value = mock_summary
643643
644+ # Set fixability score so _run_automation will be called
645+ self .group .update (seer_fixability_score = 0.75 )
646+
644647 # Make _run_automation raise an exception
645648 mock_run_automation .side_effect = Exception ("Automation failed" )
646649
647- # Call get_issue_summary and verify it still returns successfully
648- summary_data , status_code = get_issue_summary (self .group , self .user )
650+ # Call get_issue_summary with a source that triggers automation
651+ summary_data , status_code = get_issue_summary (
652+ self .group , self .user , source = SeerAutomationSource .POST_PROCESS
653+ )
649654
650655 assert status_code == 200
651656 expected_response = mock_summary .dict ()
@@ -750,6 +755,105 @@ def test_get_issue_summary_with_should_run_automation_false(
750755 cached_summary = cache .get (f"ai-group-summary-v2:{ self .group .id } " )
751756 assert cached_summary == expected_response_summary
752757
758+ @patch ("sentry.seer.autofix.issue_summary.get_seer_org_acknowledgement" )
759+ @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
760+ @patch ("sentry.seer.autofix.issue_summary._get_trace_tree_for_event" )
761+ @patch ("sentry.seer.autofix.issue_summary._call_seer" )
762+ @patch ("sentry.seer.autofix.issue_summary._get_event" )
763+ def test_generate_summary_fixability_generation (
764+ self ,
765+ mock_get_event ,
766+ mock_call_seer ,
767+ mock_get_trace_tree ,
768+ mock_generate_fixability ,
769+ mock_get_acknowledgement ,
770+ ):
771+ """Test fixability generation: creates when missing, skips when exists."""
772+ mock_get_acknowledgement .return_value = True
773+ event = Mock (event_id = "test_event_id" , datetime = datetime .datetime .now ())
774+ serialized_event = {"event_id" : "test_event_id" , "data" : "test_event_data" }
775+ mock_get_event .return_value = [serialized_event , event ]
776+ mock_summary = SummarizeIssueResponse (
777+ group_id = str (self .group .id ),
778+ headline = "Test headline" ,
779+ whats_wrong = "Test whats wrong" ,
780+ trace = "Test trace" ,
781+ possible_cause = "Test possible cause" ,
782+ )
783+ mock_call_seer .return_value = mock_summary
784+ mock_get_trace_tree .return_value = None
785+ mock_generate_fixability .return_value = SummarizeIssueResponse (
786+ group_id = str (self .group .id ),
787+ headline = "h" ,
788+ whats_wrong = "w" ,
789+ trace = "t" ,
790+ possible_cause = "c" ,
791+ scores = SummarizeIssueScores (fixability_score = 0.75 ),
792+ )
793+
794+ # Test 1: Generates fixability when missing
795+ assert self .group .seer_fixability_score is None
796+ get_issue_summary (
797+ self .group ,
798+ self .user ,
799+ source = SeerAutomationSource .POST_PROCESS ,
800+ should_run_automation = False ,
801+ )
802+ mock_generate_fixability .assert_called_once_with (self .group )
803+ self .group .refresh_from_db ()
804+ assert self .group .seer_fixability_score == 0.75
805+
806+ # Test 2: Skips fixability when already exists
807+ mock_generate_fixability .reset_mock ()
808+ cache .delete (f"ai-group-summary-v2:{ self .group .id } " )
809+ get_issue_summary (
810+ self .group ,
811+ self .user ,
812+ source = SeerAutomationSource .POST_PROCESS ,
813+ should_run_automation = False ,
814+ )
815+ mock_generate_fixability .assert_not_called ()
816+
817+ @patch ("sentry.seer.autofix.issue_summary.get_seer_org_acknowledgement" )
818+ @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
819+ @patch ("sentry.seer.autofix.issue_summary._get_trace_tree_for_event" )
820+ @patch ("sentry.seer.autofix.issue_summary._call_seer" )
821+ @patch ("sentry.seer.autofix.issue_summary._get_event" )
822+ def test_generate_summary_continues_when_fixability_fails (
823+ self ,
824+ mock_get_event ,
825+ mock_call_seer ,
826+ mock_get_trace_tree ,
827+ mock_generate_fixability ,
828+ mock_get_acknowledgement ,
829+ ):
830+ """Test that summary is still cached when fixability generation fails."""
831+ mock_get_acknowledgement .return_value = True
832+ event = Mock (event_id = "test_event_id" , datetime = datetime .datetime .now ())
833+ serialized_event = {"event_id" : "test_event_id" , "data" : "test_event_data" }
834+ mock_get_event .return_value = [serialized_event , event ]
835+ mock_summary = SummarizeIssueResponse (
836+ group_id = str (self .group .id ),
837+ headline = "Test headline" ,
838+ whats_wrong = "Test whats wrong" ,
839+ trace = "Test trace" ,
840+ possible_cause = "Test possible cause" ,
841+ )
842+ mock_call_seer .return_value = mock_summary
843+ mock_get_trace_tree .return_value = None
844+ mock_generate_fixability .side_effect = Exception ("Fixability service down" )
845+
846+ summary_data , status_code = get_issue_summary (
847+ self .group ,
848+ self .user ,
849+ source = SeerAutomationSource .POST_PROCESS ,
850+ should_run_automation = False ,
851+ )
852+
853+ assert status_code == 200
854+ assert summary_data ["headline" ] == "Test headline"
855+ mock_generate_fixability .assert_called_once ()
856+
753857
754858class TestGetStoppingPointFromFixability :
755859 @pytest .mark .parametrize (
@@ -785,22 +889,12 @@ def setUp(self) -> None:
785889 )
786890 @patch ("sentry.seer.autofix.issue_summary.get_autofix_state" , return_value = None )
787891 @patch ("sentry.quotas.backend.has_available_reserved_budget" , return_value = True )
788- @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
789- def test_high_fixability_code_changes (
790- self , mock_gen , mock_budget , mock_state , mock_rate , mock_trigger
791- ):
892+ def test_high_fixability_code_changes (self , mock_budget , mock_state , mock_rate , mock_trigger ):
792893 self .project .update_option ("sentry:autofix_automation_tuning" , "always" )
793- mock_gen .return_value = SummarizeIssueResponse (
794- group_id = str (self .group .id ),
795- headline = "h" ,
796- whats_wrong = "w" ,
797- trace = "t" ,
798- possible_cause = "c" ,
799- scores = SummarizeIssueScores (fixability_score = 0.70 ),
800- )
894+ self .group .update (seer_fixability_score = 0.80 )
801895 _run_automation (self .group , self .user , self .event , SeerAutomationSource .ALERT )
802896 mock_trigger .assert_called_once ()
803- assert mock_trigger .call_args [1 ]["stopping_point" ] == AutofixStoppingPoint .CODE_CHANGES
897+ assert mock_trigger .call_args [1 ]["stopping_point" ] == AutofixStoppingPoint .OPEN_PR
804898
805899 @patch ("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay" )
806900 @patch (
@@ -809,19 +903,9 @@ def test_high_fixability_code_changes(
809903 )
810904 @patch ("sentry.seer.autofix.issue_summary.get_autofix_state" , return_value = None )
811905 @patch ("sentry.quotas.backend.has_available_reserved_budget" , return_value = True )
812- @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
813- def test_medium_fixability_solution (
814- self , mock_gen , mock_budget , mock_state , mock_rate , mock_trigger
815- ):
906+ def test_medium_fixability_solution (self , mock_budget , mock_state , mock_rate , mock_trigger ):
816907 self .project .update_option ("sentry:autofix_automation_tuning" , "always" )
817- mock_gen .return_value = SummarizeIssueResponse (
818- group_id = str (self .group .id ),
819- headline = "h" ,
820- whats_wrong = "w" ,
821- trace = "t" ,
822- possible_cause = "c" ,
823- scores = SummarizeIssueScores (fixability_score = 0.50 ),
824- )
908+ self .group .update (seer_fixability_score = 0.50 )
825909 _run_automation (self .group , self .user , self .event , SeerAutomationSource .ALERT )
826910 mock_trigger .assert_called_once ()
827911 assert mock_trigger .call_args [1 ]["stopping_point" ] == AutofixStoppingPoint .SOLUTION
@@ -833,17 +917,9 @@ def test_medium_fixability_solution(
833917 )
834918 @patch ("sentry.seer.autofix.issue_summary.get_autofix_state" , return_value = None )
835919 @patch ("sentry.quotas.backend.has_available_reserved_budget" , return_value = True )
836- @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
837- def test_without_feature_flag (self , mock_gen , mock_budget , mock_state , mock_rate , mock_trigger ):
920+ def test_without_feature_flag (self , mock_budget , mock_state , mock_rate , mock_trigger ):
838921 self .project .update_option ("sentry:autofix_automation_tuning" , "always" )
839- mock_gen .return_value = SummarizeIssueResponse (
840- group_id = str (self .group .id ),
841- headline = "h" ,
842- whats_wrong = "w" ,
843- trace = "t" ,
844- possible_cause = "c" ,
845- scores = SummarizeIssueScores (fixability_score = 0.80 ),
846- )
922+ self .group .update (seer_fixability_score = 0.80 )
847923
848924 with self .feature (
849925 {"organizations:gen-ai-features" : True , "projects:triage-signals-v0" : False }
@@ -853,6 +929,13 @@ def test_without_feature_flag(self, mock_gen, mock_budget, mock_state, mock_rate
853929 mock_trigger .assert_called_once ()
854930 assert mock_trigger .call_args [1 ]["stopping_point" ] is None
855931
932+ @patch ("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay" )
933+ def test_missing_fixability_score_returns_early (self , mock_trigger ):
934+ """Test that _run_automation returns early when fixability score is None."""
935+ assert self .group .seer_fixability_score is None
936+ _run_automation (self .group , self .user , self .event , SeerAutomationSource .ALERT )
937+ mock_trigger .assert_not_called ()
938+
856939
857940class TestFetchUserPreference :
858941 @patch ("sentry.seer.autofix.issue_summary.sign_with_seer_secret" , return_value = {})
@@ -985,20 +1068,12 @@ def setUp(self) -> None:
9851068 )
9861069 @patch ("sentry.seer.autofix.issue_summary.get_autofix_state" , return_value = None )
9871070 @patch ("sentry.quotas.backend.has_available_reserved_budget" , return_value = True )
988- @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
9891071 def test_user_preference_limits_high_fixability (
990- self , mock_gen , mock_budget , mock_state , mock_rate , mock_fetch , mock_trigger
1072+ self , mock_budget , mock_state , mock_rate , mock_fetch , mock_trigger
9911073 ):
9921074 """High fixability (OPEN_PR) limited by user preference (SOLUTION)"""
9931075 self .project .update_option ("sentry:autofix_automation_tuning" , "always" )
994- mock_gen .return_value = SummarizeIssueResponse (
995- group_id = str (self .group .id ),
996- headline = "h" ,
997- whats_wrong = "w" ,
998- trace = "t" ,
999- possible_cause = "c" ,
1000- scores = SummarizeIssueScores (fixability_score = 0.80 ), # High = OPEN_PR
1001- )
1076+ self .group .update (seer_fixability_score = 0.80 )
10021077 mock_fetch .return_value = "solution"
10031078
10041079 _run_automation (self .group , self .user , self .event , SeerAutomationSource .ALERT )
@@ -1015,20 +1090,12 @@ def test_user_preference_limits_high_fixability(
10151090 )
10161091 @patch ("sentry.seer.autofix.issue_summary.get_autofix_state" , return_value = None )
10171092 @patch ("sentry.quotas.backend.has_available_reserved_budget" , return_value = True )
1018- @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
10191093 def test_fixability_limits_permissive_user_preference (
1020- self , mock_gen , mock_budget , mock_state , mock_rate , mock_fetch , mock_trigger
1094+ self , mock_budget , mock_state , mock_rate , mock_fetch , mock_trigger
10211095 ):
10221096 """Medium fixability (SOLUTION) used despite user allowing OPEN_PR"""
10231097 self .project .update_option ("sentry:autofix_automation_tuning" , "always" )
1024- mock_gen .return_value = SummarizeIssueResponse (
1025- group_id = str (self .group .id ),
1026- headline = "h" ,
1027- whats_wrong = "w" ,
1028- trace = "t" ,
1029- possible_cause = "c" ,
1030- scores = SummarizeIssueScores (fixability_score = 0.50 ), # Medium = SOLUTION
1031- )
1098+ self .group .update (seer_fixability_score = 0.50 )
10321099 mock_fetch .return_value = "open_pr"
10331100
10341101 _run_automation (self .group , self .user , self .event , SeerAutomationSource .ALERT )
@@ -1045,20 +1112,12 @@ def test_fixability_limits_permissive_user_preference(
10451112 )
10461113 @patch ("sentry.seer.autofix.issue_summary.get_autofix_state" , return_value = None )
10471114 @patch ("sentry.quotas.backend.has_available_reserved_budget" , return_value = True )
1048- @patch ("sentry.seer.autofix.issue_summary._generate_fixability_score" )
10491115 def test_no_user_preference_uses_fixability_only (
1050- self , mock_gen , mock_budget , mock_state , mock_rate , mock_fetch , mock_trigger
1116+ self , mock_budget , mock_state , mock_rate , mock_fetch , mock_trigger
10511117 ):
10521118 """When user has no preference, use fixability score alone"""
10531119 self .project .update_option ("sentry:autofix_automation_tuning" , "always" )
1054- mock_gen .return_value = SummarizeIssueResponse (
1055- group_id = str (self .group .id ),
1056- headline = "h" ,
1057- whats_wrong = "w" ,
1058- trace = "t" ,
1059- possible_cause = "c" ,
1060- scores = SummarizeIssueScores (fixability_score = 0.80 ), # High = OPEN_PR
1061- )
1120+ self .group .update (seer_fixability_score = 0.80 )
10621121 mock_fetch .return_value = None
10631122
10641123 _run_automation (self .group , self .user , self .event , SeerAutomationSource .ALERT )
0 commit comments