From 277c112c048a2716337aef66cdba818c383e9976 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Jul 2026 18:59:27 -0400 Subject: [PATCH 1/2] ref(autofix): Use local run mirror for automation eligibility --- src/sentry/seer/autofix/autofix_agent.py | 2 +- src/sentry/seer/autofix/issue_summary.py | 5 +-- .../sentry/seer/autofix/test_issue_summary.py | 39 +++++-------------- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/src/sentry/seer/autofix/autofix_agent.py b/src/sentry/seer/autofix/autofix_agent.py index b31d639a7386..6c90124ca2ab 100644 --- a/src/sentry/seer/autofix/autofix_agent.py +++ b/src/sentry/seer/autofix/autofix_agent.py @@ -575,7 +575,7 @@ def trigger_autofix_agent( return run -def get_autofix_agent_state(organization: Organization, group_id: int): +def get_autofix_agent_state(organization: Organization, group_id: int) -> SeerRunState | None: """ Get the current state of an agent-based autofix run for a group. diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index fcc720d78489..b253cd13a7a6 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -26,7 +26,6 @@ from sentry.seer.autofix.autofix_agent import ( AutofixStep, NoSeerQuotaException, - get_autofix_agent_state, trigger_autofix_agent, ) from sentry.seer.autofix.constants import ( @@ -52,6 +51,7 @@ make_signed_seer_api_request, make_summarize_issue_request, ) +from sentry.seer.utils import runs_for_group from sentry.services import eventstore from sentry.services.eventstore.models import Event, GroupEvent from sentry.tasks.base import instrumented_task @@ -438,8 +438,7 @@ def is_group_eligible_for_automation(group: Group) -> bool: if not has_seer_access(group.organization): return False - autofix_state = get_autofix_agent_state(group.organization, group.id) - if autofix_state: + if runs_for_group(group.id, "autofix").exists(): return False fixability_score = get_and_update_group_fixability_score(group) diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 35413e9d8dd5..a52a85789522 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -508,7 +508,6 @@ def test_get_event_provided( mock_serialize.assert_called_once() @patch("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay") - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state") @patch("sentry.seer.autofix.issue_summary._generate_fixability_score") @patch("sentry.quotas.backend.record_seer_run") @patch("sentry.seer.autofix.issue_summary.get_trace_tree_for_event") @@ -521,10 +520,8 @@ def test_get_issue_summary_with_web_vitals_issue( mock_get_trace_tree, mock_record_seer_run, mock_generate_fixability_score, - mock_get_autofix_agent_state, mock_trigger_autofix_task, ): - mock_get_autofix_agent_state.return_value = None mock_fixability_response = SummarizeIssueResponse( group_id=str(self.group.id), headline="some headline", @@ -895,11 +892,10 @@ def setUp(self) -> None: "sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited_and_increment", return_value=False, ) - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) @patch("sentry.seer.autofix.issue_summary._generate_fixability_score") def test_high_fixability_code_changes( - self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger, mock_seat_based_tier + self, mock_gen, mock_budget, mock_rate, mock_trigger, mock_seat_based_tier ): self.project.update_option("sentry:autofix_automation_tuning", "always") mock_gen.return_value = SummarizeIssueResponse( @@ -919,11 +915,10 @@ def test_high_fixability_code_changes( "sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited_and_increment", return_value=False, ) - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) @patch("sentry.seer.autofix.issue_summary._generate_fixability_score") def test_medium_fixability_solution( - self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger, mock_seat_based_tier + self, mock_gen, mock_budget, mock_rate, mock_trigger, mock_seat_based_tier ): self.project.update_option("sentry:autofix_automation_tuning", "always") mock_gen.return_value = SummarizeIssueResponse( @@ -944,10 +939,8 @@ def test_medium_fixability_solution( return_value=False, ) @patch("sentry.seer.autofix.issue_summary.is_group_eligible_for_automation", return_value=True) - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) def test_without_seat_based_tier( self, - mock_state, mock_triggering, mock_rate, mock_trigger, @@ -1048,14 +1041,12 @@ def setUp(self) -> None: "sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited_and_increment", return_value=False, ) - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) @patch("sentry.seer.autofix.issue_summary._generate_fixability_score") def test_user_preference_limits_high_fixability( self, mock_gen, mock_budget, - mock_state, mock_rate, mock_trigger, mock_seat_based_tier, @@ -1083,14 +1074,12 @@ def test_user_preference_limits_high_fixability( "sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited_and_increment", return_value=False, ) - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) @patch("sentry.seer.autofix.issue_summary._generate_fixability_score") def test_fixability_limits_permissive_user_preference( self, mock_gen, mock_budget, - mock_state, mock_rate, mock_trigger, mock_seat_based_tier, @@ -1264,11 +1253,8 @@ def setUp(self) -> None: @patch("sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited") @patch("sentry.quotas.backend.check_seer_quota") - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") - def test_returns_true_when_all_checks_pass( - self, mock_fixability, mock_state, mock_quota, mock_rate_limit - ): + def test_returns_true_when_all_checks_pass(self, mock_fixability, mock_quota, mock_rate_limit): mock_fixability.return_value = 0.80 mock_quota.return_value = True mock_rate_limit.return_value = False @@ -1285,18 +1271,15 @@ def test_returns_false_without_seer_access(self, mock_fixability): mock_fixability.assert_not_called() @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") - @patch( - "sentry.seer.autofix.issue_summary.get_autofix_agent_state", - return_value={"status": "in_progress"}, - ) - def test_returns_false_when_autofix_in_progress(self, mock_state, mock_fixability): + def test_returns_false_when_autofix_run_exists(self, mock_fixability): + run = self.create_seer_run(organization=self.organization) + self.create_seer_agent_run(run, source="autofix", group=self.group) assert is_group_eligible_for_automation(self.group) is False mock_fixability.assert_not_called() - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") - def test_returns_false_when_not_fixable(self, mock_fixability, mock_state): + def test_returns_false_when_not_fixable(self, mock_fixability): mock_fixability.return_value = 0.20 self.group.times_seen = 10 self.group.times_seen_pending = 0 @@ -1306,9 +1289,8 @@ def test_returns_false_when_not_fixable(self, mock_fixability, mock_state): assert is_group_eligible_for_automation(self.group) is False @patch("sentry.quotas.backend.check_seer_quota") - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") - def test_returns_false_when_no_budget(self, mock_fixability, mock_state, mock_quota): + def test_returns_false_when_no_budget(self, mock_fixability, mock_quota): mock_fixability.return_value = 0.80 mock_quota.return_value = False self.group.times_seen = 10 @@ -1318,11 +1300,8 @@ def test_returns_false_when_no_budget(self, mock_fixability, mock_state, mock_qu @patch("sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited") @patch("sentry.quotas.backend.check_seer_quota") - @patch("sentry.seer.autofix.issue_summary.get_autofix_agent_state", return_value=None) @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") - def test_returns_false_when_rate_limited( - self, mock_fixability, mock_state, mock_quota, mock_rate_limit - ): + def test_returns_false_when_rate_limited(self, mock_fixability, mock_quota, mock_rate_limit): mock_fixability.return_value = 0.80 mock_quota.return_value = True mock_rate_limit.return_value = True From d668caf0b35620a993eeb45c5776ee7b817524b8 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 31 Jul 2026 19:21:02 -0400 Subject: [PATCH 2/2] fix(autofix): Ignore failed run mirrors --- src/sentry/seer/autofix/issue_summary.py | 11 ++++++-- .../sentry/seer/autofix/test_issue_summary.py | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index b253cd13a7a6..5d695bb209ac 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -43,7 +43,7 @@ from sentry.seer.entrypoints.cache import SeerOperatorAutofixCache from sentry.seer.entrypoints.operator import SeerAutofixOperator from sentry.seer.models import SummarizeIssueResponse -from sentry.seer.models.run import SeerRun +from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus from sentry.seer.seer_setup import has_seer_access from sentry.seer.signed_seer_api import ( SeerViewerContext, @@ -438,7 +438,14 @@ def is_group_eligible_for_automation(group: Group) -> bool: if not has_seer_access(group.organization): return False - if runs_for_group(group.id, "autofix").exists(): + if ( + runs_for_group(group.id, "autofix") + .filter( + run__mirror_status=SeerRunMirrorStatus.LIVE, + run__seer_run_state_id__isnull=False, + ) + .exists() + ): return False fixability_score = get_and_update_group_fixability_score(group) diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index a52a85789522..1e9c4adba664 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -1271,13 +1271,35 @@ def test_returns_false_without_seer_access(self, mock_fixability): mock_fixability.assert_not_called() @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") - def test_returns_false_when_autofix_run_exists(self, mock_fixability): - run = self.create_seer_run(organization=self.organization) + def test_returns_false_when_live_autofix_run_exists(self, mock_fixability): + run = self.create_seer_run( + organization=self.organization, + mirror_status="live", + seer_run_state_id=1, + ) self.create_seer_agent_run(run, source="autofix", group=self.group) assert is_group_eligible_for_automation(self.group) is False mock_fixability.assert_not_called() + @patch("sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited") + @patch("sentry.quotas.backend.check_seer_quota") + @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") + def test_returns_true_when_only_failed_autofix_run_exists( + self, mock_fixability, mock_quota, mock_rate_limit + ): + run = self.create_seer_run( + organization=self.organization, + mirror_status="failed", + seer_run_state_id=2, + ) + self.create_seer_agent_run(run, source="autofix", group=self.group) + mock_fixability.return_value = 0.80 + mock_quota.return_value = True + mock_rate_limit.return_value = False + + assert is_group_eligible_for_automation(self.group) is True + @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") def test_returns_false_when_not_fixable(self, mock_fixability): mock_fixability.return_value = 0.20