From 1e0cb3de1c2ee22ec16ca48f6fdd2acc7f7db85f Mon Sep 17 00:00:00 2001 From: Mihir Mavalankar Date: Thu, 13 Nov 2025 15:13:43 -0800 Subject: [PATCH 1/4] feat(autofix): Organize seer automation checks --- src/sentry/tasks/post_process.py | 73 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index cc64c249c51892..f40e4bf594ccb1 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1620,17 +1620,10 @@ def check_if_flags_sent(job: PostProcessJob) -> None: set_project_flag_and_signal(project, "has_flags", first_flag_received) -def kick_off_seer_automation(job: PostProcessJob) -> None: - from sentry.seer.autofix.issue_summary import get_issue_summary_lock_key +def seer_automation_permission_and_type_check(group: Group) -> bool: + from sentry import quotas + from sentry.constants import DataCategory from sentry.seer.seer_setup import get_seer_org_acknowledgement - from sentry.tasks.autofix import start_seer_automation - - event = job["event"] - group = event.group - - # Only run on issues with no existing scan - TODO: Update condition for triage signals V0 - if group.seer_fixability_score is not None: - return # check currently supported issue categories for Seer if group.issue_category not in [ @@ -1644,56 +1637,64 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: GroupCategory.REPLAY, GroupCategory.FEEDBACK, ]: - return + return False if not features.has("organizations:gen-ai-features", group.organization): - return + return False gen_ai_allowed = not group.organization.get_option("sentry:hide_ai_features") if not gen_ai_allowed: - return + return False project = group.project if ( not project.get_option("sentry:seer_scanner_automation") and not group.issue_type.always_trigger_seer_automation ): - return - - # Check if automation has already been queued or completed for this group - # seer_autofix_last_triggered is set when trigger_autofix is successfully started. - # Use cache with short TTL to hold lock for a short since it takes a few minutes to set seer_autofix_last_triggeredes - cache_key = f"seer_automation_queued:{group.id}" - if cache.get(cache_key) or group.seer_autofix_last_triggered is not None: - return - - # Don't run if there's already a task in progress for this issue - lock_key, lock_name = get_issue_summary_lock_key(group.id) - lock = locks.get(lock_key, duration=1, name=lock_name) - if lock.locked(): - return + return False seer_enabled = get_seer_org_acknowledgement(group.organization) if not seer_enabled: - return - - from sentry import quotas - from sentry.constants import DataCategory + return False has_budget: bool = quotas.backend.has_available_reserved_budget( org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER ) if not has_budget: - return + return False + + return True + +def seer_automation_rate_limit_check(group: Group) -> bool: from sentry.seer.autofix.utils import is_seer_scanner_rate_limited - if is_seer_scanner_rate_limited(project, group.organization): + if is_seer_scanner_rate_limited(group.project, group.organization): + return False + return True + + +def kick_off_seer_automation(job: PostProcessJob) -> None: + from sentry.seer.autofix.issue_summary import get_issue_summary_lock_key + from sentry.tasks.autofix import start_seer_automation + + event = job["event"] + group = event.group + + # Only run on issues with no existing scan - TODO: Update condition for triage signals V0 + if group.seer_fixability_score is not None: + return + + if seer_automation_permission_and_type_check(group) is False: + return + + # Don't run if there's already a task in progress for this issue + lock_key, lock_name = get_issue_summary_lock_key(group.id) + lock = locks.get(lock_key, duration=1, name=lock_name) + if lock.locked(): return - # cache.add uses Redis SETNX which atomically sets the key only if it doesn't exist - # Returns False if another process already set the key, ensuring only one process proceeds - if not cache.add(cache_key, True, timeout=600): # 10 minute + if seer_automation_rate_limit_check(group) is False: return start_seer_automation.delay(group.id) From 6f0e34511911ec6ada420eb3111023b73eee8b8b Mon Sep 17 00:00:00 2001 From: Mihir Mavalankar Date: Thu, 13 Nov 2025 15:21:37 -0800 Subject: [PATCH 2/4] updated tests --- tests/sentry/tasks/test_post_process.py | 177 +++++++++--------------- 1 file changed, 64 insertions(+), 113 deletions(-) diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 357424102c6e68..40ff4e6b3e5288 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -7,7 +7,7 @@ from hashlib import md5 from typing import Any from unittest import mock -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from django.db import router @@ -3053,136 +3053,86 @@ def test_kick_off_seer_automation_with_hide_ai_features_enabled( mock_start_seer_automation.assert_not_called() - @patch( - "sentry.seer.seer_setup.get_seer_org_acknowledgement", - return_value=True, - ) - @patch("sentry.tasks.autofix.start_seer_automation.delay") - @with_feature("organizations:gen-ai-features") - def test_kick_off_seer_automation_skips_when_seer_autofix_last_triggered_set( - self, mock_start_seer_automation, mock_get_seer_org_acknowledgement - ): - """Test that automation is skipped when group.seer_autofix_last_triggered is already set""" - self.project.update_option("sentry:seer_scanner_automation", True) - event = self.create_event( - data={"message": "testing"}, - project_id=self.project.id, - ) - - # Set seer_autofix_last_triggered on the group to simulate autofix already triggered - group = event.group - group.seer_autofix_last_triggered = timezone.now() - group.save() - - self.call_post_process_group( - is_new=True, - is_regression=False, - is_new_group_environment=True, - event=event, - ) - mock_start_seer_automation.assert_not_called() +class SeerAutomationHelperFunctionsTestMixin(BasePostProgressGroupMixin): + """Unit tests for seer_automation_permission_and_type_check and seer_automation_rate_limit_check.""" - @patch( - "sentry.seer.seer_setup.get_seer_org_acknowledgement", - return_value=True, - ) - @patch("sentry.tasks.autofix.start_seer_automation.delay") - @with_feature("organizations:gen-ai-features") - def test_kick_off_seer_automation_skips_when_cache_key_exists( - self, mock_start_seer_automation, mock_get_seer_org_acknowledgement + @patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True) + @patch("sentry.seer.seer_setup.get_seer_org_acknowledgement", return_value=True) + @patch("sentry.features.has", return_value=True) + def test_seer_automation_permission_and_type_check( + self, mock_features_has, mock_get_seer_org_acknowledgement, mock_has_budget ): - """Test that automation is skipped when cache key indicates it's already queued""" - self.project.update_option("sentry:seer_scanner_automation", True) - event = self.create_event( - data={"message": "testing"}, - project_id=self.project.id, - ) + """Test permission check with various failure conditions.""" + from sentry.constants import DataCategory + from sentry.issues.grouptype import GroupCategory + from sentry.tasks.post_process import seer_automation_permission_and_type_check - # Set cache key to simulate automation already queued - cache_key = f"seer_automation_queued:{event.group.id}" - cache.set(cache_key, True, timeout=600) - - self.call_post_process_group( - is_new=True, - is_regression=False, - is_new_group_environment=True, - event=event, - ) + self.project.update_option("sentry:seer_scanner_automation", True) + event = self.create_event(data={"message": "testing"}, project_id=self.project.id) + group = event.group - mock_start_seer_automation.assert_not_called() + # All conditions pass + assert seer_automation_permission_and_type_check(group) is True - # Cleanup - cache.delete(cache_key) + # Unsupported categories (using PropertyMock to mock the property) + with patch( + "sentry.models.group.Group.issue_category", new_callable=PropertyMock + ) as mock_category: + mock_category.return_value = GroupCategory.REPLAY + assert seer_automation_permission_and_type_check(group) is False - @patch( - "sentry.seer.seer_setup.get_seer_org_acknowledgement", - return_value=True, - ) - @patch("sentry.tasks.autofix.start_seer_automation.delay") - @with_feature("organizations:gen-ai-features") - def test_kick_off_seer_automation_uses_atomic_cache_add( - self, mock_start_seer_automation, mock_get_seer_org_acknowledgement - ): - """Test that cache.add atomic operation prevents race conditions""" - self.project.update_option("sentry:seer_scanner_automation", True) - event = self.create_event( - data={"message": "testing"}, - project_id=self.project.id, - ) + mock_category.return_value = GroupCategory.FEEDBACK + assert seer_automation_permission_and_type_check(group) is False - cache_key = f"seer_automation_queued:{event.group.id}" + # Missing feature flag + mock_features_has.return_value = False + assert seer_automation_permission_and_type_check(group) is False - with patch("sentry.tasks.post_process.cache") as mock_cache: - # Simulate cache.get returning None (not in cache) - # but cache.add returning False (another process set it) - mock_cache.get.return_value = None - mock_cache.add.return_value = False + # Hide AI features enabled + mock_features_has.return_value = True + self.organization.update_option("sentry:hide_ai_features", True) + assert seer_automation_permission_and_type_check(group) is False + self.organization.update_option("sentry:hide_ai_features", False) - self.call_post_process_group( - is_new=True, - is_regression=False, - is_new_group_environment=True, - event=event, - ) + # Scanner disabled without always_trigger + self.project.update_option("sentry:seer_scanner_automation", False) + with patch.object(group.issue_type, "always_trigger_seer_automation", False): + assert seer_automation_permission_and_type_check(group) is False - # Should check cache but not call automation due to cache.add returning False - mock_cache.get.assert_called() - mock_cache.add.assert_called_once_with(cache_key, True, timeout=600) - mock_start_seer_automation.assert_not_called() + # Scanner disabled but always_trigger enabled + with patch.object(group.issue_type, "always_trigger_seer_automation", True): + assert seer_automation_permission_and_type_check(group) is True - @patch( - "sentry.seer.seer_setup.get_seer_org_acknowledgement", - return_value=True, - ) - @patch("sentry.tasks.autofix.start_seer_automation.delay") - @with_feature("organizations:gen-ai-features") - def test_kick_off_seer_automation_proceeds_when_cache_add_succeeds( - self, mock_start_seer_automation, mock_get_seer_org_acknowledgement - ): - """Test that automation proceeds when cache.add succeeds (no race condition)""" + # Seer not acknowledged self.project.update_option("sentry:seer_scanner_automation", True) - event = self.create_event( - data={"message": "testing"}, - project_id=self.project.id, + mock_get_seer_org_acknowledgement.return_value = False + assert seer_automation_permission_and_type_check(group) is False + + # No budget + mock_get_seer_org_acknowledgement.return_value = True + mock_has_budget.return_value = False + assert seer_automation_permission_and_type_check(group) is False + mock_has_budget.assert_called_with( + org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER ) - # Ensure seer_autofix_last_triggered is not set - assert event.group.seer_autofix_last_triggered is None + @patch("sentry.seer.autofix.utils.is_seer_scanner_rate_limited") + def test_seer_automation_rate_limit_check(self, mock_is_rate_limited): + """Test rate limit check returns correct value based on rate limiting status.""" + from sentry.tasks.post_process import seer_automation_rate_limit_check - self.call_post_process_group( - is_new=True, - is_regression=False, - is_new_group_environment=True, - event=event, - ) + event = self.create_event(data={"message": "testing"}, project_id=self.project.id) + group = event.group - # Should successfully queue automation - mock_start_seer_automation.assert_called_once_with(event.group.id) + mock_is_rate_limited.return_value = False + assert seer_automation_rate_limit_check(group) is True + + mock_is_rate_limited.return_value = True + assert seer_automation_rate_limit_check(group) is False - # Cleanup - cache_key = f"seer_automation_queued:{event.group.id}" - cache.delete(cache_key) + assert mock_is_rate_limited.call_count == 2 + mock_is_rate_limited.assert_called_with(group.project, group.organization) class PostProcessGroupErrorTest( @@ -3194,6 +3144,7 @@ class PostProcessGroupErrorTest( InboxTestMixin, ResourceChangeBoundsTestMixin, KickOffSeerAutomationTestMixin, + SeerAutomationHelperFunctionsTestMixin, RuleProcessorTestMixin, ServiceHooksTestMixin, SnoozeTestMixin, From b0893cec5e5abf5bc60a41f72073796b18cf1228 Mon Sep 17 00:00:00 2001 From: Mihir Mavalankar Date: Thu, 13 Nov 2025 15:49:28 -0800 Subject: [PATCH 3/4] move functions to utils file --- src/sentry/seer/autofix/utils.py | 55 +++++++++++++++++++++++ src/sentry/tasks/post_process.py | 58 ++----------------------- tests/sentry/tasks/test_post_process.py | 4 +- 3 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index deb848980c1926..89f6751d12f726 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -286,6 +286,61 @@ def is_seer_scanner_rate_limited(project: Project, organization: Organization) - return is_rate_limited +def seer_automation_permission_and_type_check(group) -> bool: + """Check if Seer automation is allowed for a given group based on permissions and issue type.""" + from sentry import quotas + from sentry.issues.grouptype import GroupCategory + + # check currently supported issue categories for Seer + if group.issue_category not in [ + GroupCategory.ERROR, + GroupCategory.PERFORMANCE, + GroupCategory.MOBILE, + GroupCategory.FRONTEND, + GroupCategory.DB_QUERY, + GroupCategory.HTTP_CLIENT, + ] or group.issue_category in [ + GroupCategory.REPLAY, + GroupCategory.FEEDBACK, + ]: + return False + + if not features.has("organizations:gen-ai-features", group.organization): + return False + + gen_ai_allowed = not group.organization.get_option("sentry:hide_ai_features") + if not gen_ai_allowed: + return False + + project = group.project + if ( + not project.get_option("sentry:seer_scanner_automation") + and not group.issue_type.always_trigger_seer_automation + ): + return False + + from sentry.seer.seer_setup import get_seer_org_acknowledgement + + seer_enabled = get_seer_org_acknowledgement(group.organization) + if not seer_enabled: + return False + + has_budget: bool = quotas.backend.has_available_reserved_budget( + org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER + ) + if not has_budget: + return False + + return True + + +def seer_automation_rate_limit_check(group) -> bool: + """Check if Seer automation is rate limited for a given group.""" + if is_seer_scanner_rate_limited(group.project, group.organization): + return False + return True + + AUTOFIX_AUTOTRIGGED_RATE_LIMIT_OPTION_MULTIPLIERS = { AutofixAutomationTuningSettings.OFF: 5, AutofixAutomationTuningSettings.SUPER_LOW: 5, diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index f40e4bf594ccb1..1f27b7bc42f9b3 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1620,62 +1620,12 @@ def check_if_flags_sent(job: PostProcessJob) -> None: set_project_flag_and_signal(project, "has_flags", first_flag_received) -def seer_automation_permission_and_type_check(group: Group) -> bool: - from sentry import quotas - from sentry.constants import DataCategory - from sentry.seer.seer_setup import get_seer_org_acknowledgement - - # check currently supported issue categories for Seer - if group.issue_category not in [ - GroupCategory.ERROR, - GroupCategory.PERFORMANCE, - GroupCategory.MOBILE, - GroupCategory.FRONTEND, - GroupCategory.DB_QUERY, - GroupCategory.HTTP_CLIENT, - ] or group.issue_category in [ - GroupCategory.REPLAY, - GroupCategory.FEEDBACK, - ]: - return False - - if not features.has("organizations:gen-ai-features", group.organization): - return False - - gen_ai_allowed = not group.organization.get_option("sentry:hide_ai_features") - if not gen_ai_allowed: - return False - - project = group.project - if ( - not project.get_option("sentry:seer_scanner_automation") - and not group.issue_type.always_trigger_seer_automation - ): - return False - - seer_enabled = get_seer_org_acknowledgement(group.organization) - if not seer_enabled: - return False - - has_budget: bool = quotas.backend.has_available_reserved_budget( - org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER - ) - if not has_budget: - return False - - return True - - -def seer_automation_rate_limit_check(group: Group) -> bool: - from sentry.seer.autofix.utils import is_seer_scanner_rate_limited - - if is_seer_scanner_rate_limited(group.project, group.organization): - return False - return True - - def kick_off_seer_automation(job: PostProcessJob) -> None: from sentry.seer.autofix.issue_summary import get_issue_summary_lock_key + from sentry.seer.autofix.utils import ( + seer_automation_permission_and_type_check, + seer_automation_rate_limit_check, + ) from sentry.tasks.autofix import start_seer_automation event = job["event"] diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 40ff4e6b3e5288..9ecc3b1404889a 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3066,7 +3066,7 @@ def test_seer_automation_permission_and_type_check( """Test permission check with various failure conditions.""" from sentry.constants import DataCategory from sentry.issues.grouptype import GroupCategory - from sentry.tasks.post_process import seer_automation_permission_and_type_check + from sentry.seer.autofix.utils import seer_automation_permission_and_type_check self.project.update_option("sentry:seer_scanner_automation", True) event = self.create_event(data={"message": "testing"}, project_id=self.project.id) @@ -3120,7 +3120,7 @@ def test_seer_automation_permission_and_type_check( @patch("sentry.seer.autofix.utils.is_seer_scanner_rate_limited") def test_seer_automation_rate_limit_check(self, mock_is_rate_limited): """Test rate limit check returns correct value based on rate limiting status.""" - from sentry.tasks.post_process import seer_automation_rate_limit_check + from sentry.seer.autofix.utils import seer_automation_rate_limit_check event = self.create_event(data={"message": "testing"}, project_id=self.project.id) group = event.group From 227212b2dcba5578547bc71912ed017bf6e898fa Mon Sep 17 00:00:00 2001 From: Mihir Mavalankar Date: Thu, 13 Nov 2025 16:21:47 -0800 Subject: [PATCH 4/4] pr feedback fixes --- src/sentry/seer/autofix/utils.py | 10 ++---- src/sentry/tasks/post_process.py | 8 ++--- tests/sentry/tasks/test_post_process.py | 41 ++++++++----------------- 3 files changed, 18 insertions(+), 41 deletions(-) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index 89f6751d12f726..31e0da5e47c6b5 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -11,6 +11,7 @@ from sentry import features, options, ratelimits from sentry.constants import DataCategory from sentry.issues.auto_source_code_config.code_mapping import get_sorted_code_mapping_configs +from sentry.models.group import Group from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.repository import Repository @@ -286,7 +287,7 @@ def is_seer_scanner_rate_limited(project: Project, organization: Organization) - return is_rate_limited -def seer_automation_permission_and_type_check(group) -> bool: +def is_issue_eligible_for_seer_automation(group: Group) -> bool: """Check if Seer automation is allowed for a given group based on permissions and issue type.""" from sentry import quotas from sentry.issues.grouptype import GroupCategory @@ -334,13 +335,6 @@ def seer_automation_permission_and_type_check(group) -> bool: return True -def seer_automation_rate_limit_check(group) -> bool: - """Check if Seer automation is rate limited for a given group.""" - if is_seer_scanner_rate_limited(group.project, group.organization): - return False - return True - - AUTOFIX_AUTOTRIGGED_RATE_LIMIT_OPTION_MULTIPLIERS = { AutofixAutomationTuningSettings.OFF: 5, AutofixAutomationTuningSettings.SUPER_LOW: 5, diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 1f27b7bc42f9b3..e4cabf90c055db 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1623,8 +1623,8 @@ def check_if_flags_sent(job: PostProcessJob) -> None: def kick_off_seer_automation(job: PostProcessJob) -> None: from sentry.seer.autofix.issue_summary import get_issue_summary_lock_key from sentry.seer.autofix.utils import ( - seer_automation_permission_and_type_check, - seer_automation_rate_limit_check, + is_issue_eligible_for_seer_automation, + is_seer_scanner_rate_limited, ) from sentry.tasks.autofix import start_seer_automation @@ -1635,7 +1635,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: if group.seer_fixability_score is not None: return - if seer_automation_permission_and_type_check(group) is False: + if is_issue_eligible_for_seer_automation(group) is False: return # Don't run if there's already a task in progress for this issue @@ -1644,7 +1644,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: if lock.locked(): return - if seer_automation_rate_limit_check(group) is False: + if is_seer_scanner_rate_limited(group.project, group.organization): return start_seer_automation.delay(group.id) diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 9ecc3b1404889a..36f90a502ac84d 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3055,85 +3055,68 @@ def test_kick_off_seer_automation_with_hide_ai_features_enabled( class SeerAutomationHelperFunctionsTestMixin(BasePostProgressGroupMixin): - """Unit tests for seer_automation_permission_and_type_check and seer_automation_rate_limit_check.""" + """Unit tests for is_issue_eligible_for_seer_automation.""" @patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True) @patch("sentry.seer.seer_setup.get_seer_org_acknowledgement", return_value=True) @patch("sentry.features.has", return_value=True) - def test_seer_automation_permission_and_type_check( + def test_is_issue_eligible_for_seer_automation( self, mock_features_has, mock_get_seer_org_acknowledgement, mock_has_budget ): """Test permission check with various failure conditions.""" from sentry.constants import DataCategory from sentry.issues.grouptype import GroupCategory - from sentry.seer.autofix.utils import seer_automation_permission_and_type_check + from sentry.seer.autofix.utils import is_issue_eligible_for_seer_automation self.project.update_option("sentry:seer_scanner_automation", True) event = self.create_event(data={"message": "testing"}, project_id=self.project.id) group = event.group # All conditions pass - assert seer_automation_permission_and_type_check(group) is True + assert is_issue_eligible_for_seer_automation(group) is True # Unsupported categories (using PropertyMock to mock the property) with patch( "sentry.models.group.Group.issue_category", new_callable=PropertyMock ) as mock_category: mock_category.return_value = GroupCategory.REPLAY - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False mock_category.return_value = GroupCategory.FEEDBACK - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False # Missing feature flag mock_features_has.return_value = False - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False # Hide AI features enabled mock_features_has.return_value = True self.organization.update_option("sentry:hide_ai_features", True) - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False self.organization.update_option("sentry:hide_ai_features", False) # Scanner disabled without always_trigger self.project.update_option("sentry:seer_scanner_automation", False) with patch.object(group.issue_type, "always_trigger_seer_automation", False): - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False # Scanner disabled but always_trigger enabled with patch.object(group.issue_type, "always_trigger_seer_automation", True): - assert seer_automation_permission_and_type_check(group) is True + assert is_issue_eligible_for_seer_automation(group) is True # Seer not acknowledged self.project.update_option("sentry:seer_scanner_automation", True) mock_get_seer_org_acknowledgement.return_value = False - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False # No budget mock_get_seer_org_acknowledgement.return_value = True mock_has_budget.return_value = False - assert seer_automation_permission_and_type_check(group) is False + assert is_issue_eligible_for_seer_automation(group) is False mock_has_budget.assert_called_with( org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER ) - @patch("sentry.seer.autofix.utils.is_seer_scanner_rate_limited") - def test_seer_automation_rate_limit_check(self, mock_is_rate_limited): - """Test rate limit check returns correct value based on rate limiting status.""" - from sentry.seer.autofix.utils import seer_automation_rate_limit_check - - event = self.create_event(data={"message": "testing"}, project_id=self.project.id) - group = event.group - - mock_is_rate_limited.return_value = False - assert seer_automation_rate_limit_check(group) is True - - mock_is_rate_limited.return_value = True - assert seer_automation_rate_limit_check(group) is False - - assert mock_is_rate_limited.call_count == 2 - mock_is_rate_limited.assert_called_with(group.project, group.organization) - class PostProcessGroupErrorTest( TestCase,