diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 25c6aa355cbe..3bd5327635ed 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -289,8 +289,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:seer-wizard", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer issues view manager.add("organizations:seer-issue-view", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable Autofix to use Seer Agent instead of legacy Celery pipeline - manager.add("organizations:autofix-on-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable autofix introspection for early stopping of autofix runs manager.add("organizations:seer-autofix-introspection", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Workflows in Slack (released, kept until overrides are removed) diff --git a/src/sentry/search/snuba/backend.py b/src/sentry/search/snuba/backend.py index 5d54321b08d3..79f0ea594faf 100644 --- a/src/sentry/search/snuba/backend.py +++ b/src/sentry/search/snuba/backend.py @@ -12,7 +12,7 @@ from django.utils import timezone from django.utils.functional import SimpleLazyObject -from sentry import features, quotas +from sentry import quotas from sentry.api.event_search import SearchFilter from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.exceptions import InvalidSearchQuery @@ -595,11 +595,7 @@ def _get_queryset_conditions( "issue.type": QCallbackCondition(lambda types: Q(type__in=types)), "issue.priority": QCallbackCondition(lambda priorities: Q(priority__in=priorities)), "issue.seer_actionability": QCallbackCondition(seer_actionability_filter), - "issue.seer_last_run": ScalarCondition( - "seer_explorer_autofix_last_triggered" - if features.has("organizations:autofix-on-explorer", organization) - else "seer_autofix_last_triggered" - ), + "issue.seer_last_run": ScalarCondition("seer_explorer_autofix_last_triggered"), "issue.id": QCallbackCondition( lambda ids: Q(id__in=[int(v) for v in (ids if isinstance(ids, list) else [ids])]) ), diff --git a/src/sentry/seer/agent/client_utils.py b/src/sentry/seer/agent/client_utils.py index 07b9e3d2eb58..591297a8312a 100644 --- a/src/sentry/seer/agent/client_utils.py +++ b/src/sentry/seer/agent/client_utils.py @@ -208,24 +208,7 @@ def has_seer_agent_access_with_detail( if not has_access: return False, error - feature_names = [ - # Access to seer agent - "organizations:seer-explorer", - # Access to seer agent powered autofix - "organizations:autofix-on-explorer", - ] - - batch_features = features.batch_has( - feature_names, - organization=organization, - actor=actor, - ) - - if batch_features is None: - return False, "Feature flag not enabled" - - org_features = batch_features.get(f"organization:{organization.id}", {}) - if not any(bool(org_features.get(feature_name)) for feature_name in feature_names): + if not features.has("organizations:seer-explorer", organization, actor=actor): return False, "Feature flag not enabled" # Check open team membership (the agent requires this for context) diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index ff6ce342973c..af3e22daff94 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -18,7 +18,7 @@ from sentry.locks import locks from sentry.models.group import Group from sentry.net.http import connection_from_url -from sentry.seer.autofix.autofix import _get_trace_tree_for_event, trigger_legacy_autofix +from sentry.seer.autofix.autofix import _get_trace_tree_for_event from sentry.seer.autofix.autofix_agent import ( AutofixStep, NoSeerQuotaException, @@ -52,7 +52,6 @@ from sentry.taskworker.namespaces import seer_tasks from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser -from sentry.users.services.user.service import user_service from sentry.utils.cache import cache from sentry.utils.locking import UnableToAcquireLock @@ -176,41 +175,17 @@ def _trigger_autofix_task( } ) - user: User | AnonymousUser | RpcUser | None = None - if user_id: - user = user_service.get_user(user_id=user_id) - if user is None: - logger.warning( - "_trigger_autofix_task.user_not_found", - extra={"group_id": group_id, "user_id": user_id}, - ) - user = AnonymousUser() - else: - user = AnonymousUser() - - # Route to agent-based autofix if both feature flags are enabled run_id: int | None = None - if features.has("organizations:autofix-on-explorer", group.organization): - try: - run_id = trigger_autofix_agent( - group=group, - step=AutofixStep.ROOT_CAUSE, - referrer=referrer, - run_id=None, - stopping_point=stopping_point, - ) - except NoSeerQuotaException: - pass - else: - response = trigger_legacy_autofix( + try: + run_id = trigger_autofix_agent( group=group, - event_id=event_id, - user=user, + step=AutofixStep.ROOT_CAUSE, referrer=referrer, - auto_run_source=auto_run_source, + run_id=None, stopping_point=stopping_point, ) - run_id = response.data.get("run_id") + except NoSeerQuotaException: + pass if run_id and SeerAutofixOperator.has_access(organization=group.project.organization): SeerOperatorAutofixCache.migrate(from_group_id=group_id, to_run_id=run_id) diff --git a/src/sentry/seer/entrypoints/operator.py b/src/sentry/seer/entrypoints/operator.py index 2eee7b381a3b..739b1eb90331 100644 --- a/src/sentry/seer/entrypoints/operator.py +++ b/src/sentry/seer/entrypoints/operator.py @@ -1,8 +1,6 @@ import logging from typing import Any -from rest_framework.response import Response - from sentry import features from sentry.constants import DataCategory from sentry.models.activity import Activity @@ -13,20 +11,8 @@ from sentry.seer.agent.client_models import CodingAgentState, SeerRunState from sentry.seer.agent.client_utils import fetch_run_status from sentry.seer.agent.on_completion_hook import AgentOnCompletionHook -from sentry.seer.autofix.autofix import trigger_legacy_autofix, update_legacy_autofix -from sentry.seer.autofix.constants import AutofixReferrer, AutofixStatus -from sentry.seer.autofix.types import ( - AutofixCreatePRPayload, - AutofixSelectRootCausePayload, - AutofixSelectSolutionPayload, -) -from sentry.seer.autofix.utils import ( - AutofixState, - AutofixStoppingPoint, - get_autofix_state, - get_automation_handoff, -) -from sentry.seer.autofix.utils import CodingAgentState as LegacyCodingAgentState +from sentry.seer.autofix.constants import AutofixReferrer +from sentry.seer.autofix.utils import AutofixStoppingPoint, get_automation_handoff from sentry.seer.entrypoints.cache import SeerOperatorAgentCache, SeerOperatorAutofixCache from sentry.seer.entrypoints.metrics import ( SeerOperatorEventLifecycleMetric, @@ -67,7 +53,6 @@ # entrypoint's ability to receive updates from those triggers. So 12 is plenty, even accounting for # incidents, since a run should not take nearly that long to complete. PROCESS_AUTOFIX_TIMEOUT_SECONDS = 60 * 5 # 5 minutes -AUTOFIX_FALLBACK_CAUSE_ID = 0 def has_seer_autofix_entrypoint_access( @@ -149,22 +134,13 @@ def trigger_autofix( instruction: str | None = None, run_id: int | None = None, ) -> None: - if features.has("organizations:autofix-on-explorer", group.organization): - self.trigger_autofix_agent( - group=group, - user=user, - stopping_point=stopping_point, - instruction=instruction, - run_id=run_id, - ) - else: - self.trigger_autofix_legacy( - group=group, - user=user, - stopping_point=stopping_point, - instruction=instruction, - run_id=run_id, - ) + self.trigger_autofix_agent( + group=group, + user=user, + stopping_point=stopping_point, + instruction=instruction, + run_id=run_id, + ) def trigger_autofix_agent( self, @@ -352,17 +328,8 @@ def trigger_handoff( ) try: - coding_agents: list[CodingAgentState] | list[LegacyCodingAgentState] - if features.has("organizations:autofix-on-explorer", group.organization): - agent_state = fetch_run_status(run_id=run_id, organization=group.organization) - coding_agents = list(agent_state.coding_agents.values()) - else: - autofix_state = get_autofix_state( - run_id=run_id, organization_id=group.organization.id - ) - coding_agents = ( - list(autofix_state.coding_agents.values()) if autofix_state else [] - ) + agent_state = fetch_run_status(run_id=run_id, organization=group.organization) + coding_agents: list[CodingAgentState] = list(agent_state.coding_agents.values()) except Exception as e: with SeerOperatorEventLifecycleMetric( interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_HANDOFF_ERROR, @@ -419,162 +386,6 @@ def trigger_handoff( ).capture(): self.entrypoint.on_trigger_handoff_success(run_id=run_id, target=target) - def trigger_autofix_legacy( - self, - *, - group: Group, - user: User | RpcUser, - stopping_point: AutofixStoppingPoint, - instruction: str | None = None, - run_id: int | None = None, - ) -> None: - event_lifecyle = SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.OPERATOR_TRIGGER_AUTOFIX, - entrypoint_key=self.entrypoint.key, - ) - - raw_response: Response | None = None - with event_lifecyle.capture() as lifecycle: - lifecycle.add_extras( - { - "group_id": str(group.id), - "user_id": str(user.id), - "stopping_point": str(stopping_point), - } - ) - try: - existing_state = get_autofix_state( - group_id=group.id, organization_id=group.organization.id - ) - except Exception as e: - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR, - entrypoint_key=self.entrypoint.key, - ).capture(): - self.entrypoint.on_trigger_autofix_error( - error="Encountered an error while talking to Seer" - ) - lifecycle.record_failure(failure_reason=e) - return - if existing_state: - stopping_point_step = get_stopping_point_status(stopping_point, existing_state) - lifecycle.add_extras( - { - "existing_run_id": str(existing_state.run_id), - "existing_run_status": str(existing_state.status), - } - ) - # For now, we don't support re-runs over slack -- it causes a confusing UX without - # reliably being able to edit messages. - if stopping_point_step: - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ALREADY_EXISTS, - entrypoint_key=self.entrypoint.key, - ).capture(): - has_complete_stage = ( - False - if stopping_point_step.get("key") - in {"root_cause_analysis_processing", "solution_processing"} - else stopping_point_step.get("status") == AutofixStatus.COMPLETED - ) - self.entrypoint.on_trigger_autofix_already_exists( - run_id=existing_state.run_id, - has_complete_stage=has_complete_stage, - ) - return - - if not run_id: - raw_response = trigger_legacy_autofix( - group=group, - user=user, - referrer=AutofixReferrer.SLACK, - instruction=instruction, - stopping_point=stopping_point, - ) - else: - payload: ( - AutofixSelectRootCausePayload - | AutofixSelectSolutionPayload - | AutofixCreatePRPayload - | None - ) = None - if stopping_point == AutofixStoppingPoint.SOLUTION: - payload = AutofixSelectRootCausePayload( - type="select_root_cause", - cause_id=get_latest_cause_id(existing_state), - ) - elif stopping_point == AutofixStoppingPoint.CODE_CHANGES: - payload = AutofixSelectSolutionPayload(type="select_solution") - elif stopping_point == AutofixStoppingPoint.OPEN_PR: - payload = AutofixCreatePRPayload(type="create_pr") - else: - lifecycle.record_failure(failure_reason="invalid_stopping_point") - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR, - entrypoint_key=self.entrypoint.key, - ).capture(): - self.entrypoint.on_trigger_autofix_error( - error="Invalid stopping point provided" - ) - return - - raw_response = update_legacy_autofix( - organization_id=group.organization.id, - run_id=run_id, - payload=payload, - ) - - error_message = raw_response.data.get("detail") - - # Let the entrypoint signal to the external service that no run was started :/ - if error_message: - lifecycle.record_failure(failure_reason=error_message) - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR, - entrypoint_key=self.entrypoint.key, - ).capture(): - self.entrypoint.on_trigger_autofix_error(error=error_message) - return - - run_id = raw_response.data.get("run_id") if not run_id else run_id - if not run_id: - lifecycle.record_failure(failure_reason="no_run_id") - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR, - entrypoint_key=self.entrypoint.key, - ).capture(): - self.entrypoint.on_trigger_autofix_error(error="An unknown error has occurred") - return - lifecycle.add_extra("run_id", str(run_id)) - - # Let the entrypoint signal to the external service that the run started - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_SUCCESS, - entrypoint_key=self.entrypoint.key, - ).capture(): - self.entrypoint.on_trigger_autofix_success(run_id=run_id) - - # Create a cache payload that will be picked up for subsequent updates - with SeerOperatorEventLifecycleMetric( - interaction_type=SeerOperatorInteractionType.ENTRYPOINT_CREATE_AUTOFIX_CACHE_PAYLOAD, - entrypoint_key=self.entrypoint.key, - ).capture(): - cache_payload = self.entrypoint.create_autofix_cache_payload() - - if not cache_payload: - return - cache_result = SeerOperatorAutofixCache.populate_post_autofix_cache( - entrypoint_key=str(self.entrypoint.key), - cache_payload=cache_payload, - run_id=run_id, - ) - lifecycle.add_extras( - { - "cache_key": cache_result["key"], - "cache_source": cache_result["source"], - } - ) - def has_seer_agent_entrypoint_access( *, @@ -879,44 +690,6 @@ def process_autofix_updates( ept_lifecycle.record_failure(failure_reason=e) -def get_stopping_point_status( - stopping_point: AutofixStoppingPoint, autofix_state: AutofixState -) -> dict | None: - """ - Gets the most recent matching step state from a given stopping point. - """ - # The most recent of a repeated step is at the end of the list, that's what we want to surface - steps = reversed(autofix_state.steps) - match stopping_point: - case AutofixStoppingPoint.ROOT_CAUSE: - step = next( - ( - step - for step in steps - if step.get("key") in {"root_cause_analysis", "root_cause_analysis_processing"} - ), - None, - ) - case AutofixStoppingPoint.SOLUTION: - step = next( - (step for step in steps if step.get("key") in {"solution", "solution_processing"}), - None, - ) - case AutofixStoppingPoint.CODE_CHANGES: - step = next((step for step in steps if step.get("key") == "changes"), None) - case AutofixStoppingPoint.OPEN_PR: - step = next( - ( - step - for step in steps - if step.get("key") == "changes" - and any(change.get("pull_request") for change in step.get("changes", [])) - ), - None, - ) - return step - - def get_autofix_explorer_status( stopping_point: AutofixStoppingPoint, autofix_state: SeerRunState ) -> bool | None: @@ -975,32 +748,6 @@ def get_autofix_explorer_status( return None -def get_latest_cause_id(autofix_state: AutofixState | None) -> int: - """ - Gets the latest cause_id from a given autofix state. - """ - if not autofix_state: - return AUTOFIX_FALLBACK_CAUSE_ID - root_cause_step = next( - ( - step - # If there are multiple RCA steps, we want the latest, so we reverse the list - for step in reversed(autofix_state.steps) - if step.get("key") == "root_cause_analysis" - ), - None, - ) - if not root_cause_step: - return AUTOFIX_FALLBACK_CAUSE_ID - - root_causes = root_cause_step.get("causes", []) - if not root_causes: - return AUTOFIX_FALLBACK_CAUSE_ID - - # The most recent cause is at the end of the list - return root_causes[-1].get("id", AUTOFIX_FALLBACK_CAUSE_ID) - - class SeerOperatorCompletionHook(AgentOnCompletionHook): """Completion hook that notifies all entrypoints when a Seer Agent run finishes. diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index 7039f5bd0657..9c438eb82325 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -531,7 +531,7 @@ def test_perf_issue(self) -> None: assert response.data[0]["id"] == str(perf_group.id) def test_has_seer_last_run(self) -> None: - """Test filtering issues by whether they have seer_autofix_last_triggered set.""" + """Test filtering issues by whether they have seer_explorer_autofix_last_triggered set.""" event1 = self.store_event( data={ "fingerprint": ["no-seer-group"], @@ -563,29 +563,17 @@ def test_has_seer_last_run(self) -> None: self.login_as(user=self.user) - # Query for issues that have seer_autofix_last_triggered set + # Query for issues that have seer_explorer_autofix_last_triggered set response = self.get_success_response(query="has:issue.seer_last_run") assert len(response.data) == 1 - assert response.data[0]["id"] == str(group_with_legacy_seer.id) + assert response.data[0]["id"] == str(group_with_explorer_seer.id) - # Query for issues that do NOT have seer_autofix_last_triggered set + # Query for issues that do NOT have seer_explorer_autofix_last_triggered set response = self.get_success_response(query="!has:issue.seer_last_run") assert len(response.data) == 2 - assert response.data[0]["id"] == str(group_with_explorer_seer.id) + assert response.data[0]["id"] == str(group_with_legacy_seer.id) assert response.data[1]["id"] == str(group_without_seer.id) - # Query for issues that have seer_explorer_autofix_last_triggered set - with self.feature("organizations:autofix-on-explorer"): - response = self.get_success_response(query="has:issue.seer_last_run") - assert len(response.data) == 1 - assert response.data[0]["id"] == str(group_with_explorer_seer.id) - - # Query for issues that do NOT have seer_explorer_autofix_last_triggered set - response = self.get_success_response(query="!has:issue.seer_last_run") - assert len(response.data) == 2 - assert response.data[0]["id"] == str(group_with_legacy_seer.id) - assert response.data[1]["id"] == str(group_without_seer.id) - def test_lookup_by_event_id(self) -> None: project = self.project project.update_option("sentry:resolve_age", 1) diff --git a/tests/sentry/seer/agent/test_client_utils.py b/tests/sentry/seer/agent/test_client_utils.py index 4b9f2bd7585f..a1f9e9c18307 100644 --- a/tests/sentry/seer/agent/test_client_utils.py +++ b/tests/sentry/seer/agent/test_client_utils.py @@ -32,36 +32,18 @@ def test_hide_ai_features_option_set(self) -> None: result = has_seer_agent_access_with_detail(self.org, self.user) assert result == (False, "AI features are disabled for this organization.") - def test_no_explorer_flags_enabled(self) -> None: + def test_no_explorer_flag_enabled(self) -> None: with self.feature("organizations:gen-ai-features"): result = has_seer_agent_access_with_detail(self.org, self.user) assert result == (False, "Feature flag not enabled") - def test_only_seer_explorer_flag(self) -> None: + def test_seer_explorer_flag_enabled(self) -> None: with self.feature( {"organizations:gen-ai-features": True, "organizations:seer-explorer": True} ): result = has_seer_agent_access_with_detail(self.org, self.user) assert result == (True, None) - def test_only_autofix_on_explorer_flag(self) -> None: - with self.feature( - {"organizations:gen-ai-features": True, "organizations:autofix-on-explorer": True} - ): - result = has_seer_agent_access_with_detail(self.org, self.user) - assert result == (True, None) - - def test_all_explorer_flags_enabled(self) -> None: - with self.feature( - { - "organizations:gen-ai-features": True, - "organizations:seer-explorer": True, - "organizations:autofix-on-explorer": True, - } - ): - result = has_seer_agent_access_with_detail(self.org, self.user) - assert result == (True, None) - def test_allow_joinleave_disabled(self) -> None: self.org.flags.allow_joinleave = False self.org.save() @@ -69,7 +51,6 @@ def test_allow_joinleave_disabled(self) -> None: { "organizations:gen-ai-features": True, "organizations:seer-explorer": True, - "organizations:autofix-on-explorer": True, } ): result = has_seer_agent_access_with_detail(self.org, self.user) diff --git a/tests/sentry/seer/entrypoints/slack/test_tasks.py b/tests/sentry/seer/entrypoints/slack/test_tasks.py index 8f146bf18c10..b8092855c75b 100644 --- a/tests/sentry/seer/entrypoints/slack/test_tasks.py +++ b/tests/sentry/seer/entrypoints/slack/test_tasks.py @@ -41,7 +41,6 @@ _SEER_SLACK_FEATURES = { "organizations:gen-ai-features": True, "organizations:seer-explorer": True, - "organizations:autofix-on-explorer": True, } diff --git a/tests/sentry/seer/entrypoints/test_operator.py b/tests/sentry/seer/entrypoints/test_operator.py index 6cc0fad8f1f1..e96c3148c06f 100644 --- a/tests/sentry/seer/entrypoints/test_operator.py +++ b/tests/sentry/seer/entrypoints/test_operator.py @@ -3,8 +3,6 @@ from typing import Any, TypedDict, cast from unittest.mock import Mock, patch -from rest_framework.response import Response - from fixtures.seer.webhooks import MOCK_RUN_ID from sentry.models.activity import Activity from sentry.models.organization import Organization @@ -16,16 +14,12 @@ RepoPRState, SeerRunState, ) -from sentry.seer.autofix.constants import AutofixReferrer, AutofixStatus +from sentry.seer.autofix.constants import AutofixReferrer from sentry.seer.autofix.utils import ( - AutofixState, AutofixStoppingPoint, CodingAgentProviderType, - CodingAgentStatus, ) -from sentry.seer.autofix.utils import CodingAgentState as LegacyCodingAgentState from sentry.seer.entrypoints.operator import ( - AUTOFIX_FALLBACK_CAUSE_ID, SEER_EVENT_TO_ACTIVITY_TYPE, SeerAgentOperator, SeerAutofixOperator, @@ -113,22 +107,6 @@ def _set_automation_handoff( self.project.update_option("sentry:seer_automation_handoff_target", target.value) self.project.update_option("sentry:seer_automation_handoff_integration_id", 789) - def _build_autofix_state_with_agents( - self, agents: dict[str, LegacyCodingAgentState] - ) -> AutofixState: - return AutofixState( - run_id=MOCK_RUN_ID, - request={ - "organization_id": self.organization.id, - "project_id": self.project.id, - "issue": {"id": self.group.id, "title": "test"}, - "repos": [], - }, - updated_at=datetime.now(), - status=AutofixStatus.PROCESSING, - coding_agents=agents, - ) - @patch("sentry.seer.entrypoints.operator.has_seer_access", return_value=True) def test_has_access_with_seer(self, _mock_has_seer_access): MockNoAccessEntrypoint = Mock(spec=SeerAutofixEntrypoint) @@ -163,234 +141,6 @@ def test_has_access_without_seer(self, _mock_has_seer_access): entrypoint_key=cast(SeerEntrypointKey, entrypoint_key), ) - @patch( - "sentry.seer.entrypoints.operator.update_legacy_autofix", - return_value=Response({"run_id": MOCK_RUN_ID}, status=202), - ) - @patch( - "sentry.seer.entrypoints.operator.trigger_legacy_autofix", - return_value=Response({"run_id": MOCK_RUN_ID}, status=202), - ) - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) - def test_trigger_autofix_pathway( - self, - mock_get_autofix_state, - mock_trigger_autofix_helper, - mock_update_autofix_helper, - ): - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - assert mock_trigger_autofix_helper.call_count == 1 - assert mock_update_autofix_helper.call_count == 0 - mock_trigger_autofix_helper.reset_mock() - - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.SOLUTION, - run_id=MOCK_RUN_ID, - ) - assert mock_trigger_autofix_helper.call_count == 0 - assert mock_update_autofix_helper.call_count == 1 - - @patch( - "sentry.seer.entrypoints.operator.trigger_legacy_autofix", - return_value=Response({"run_id": MOCK_RUN_ID}, status=202), - ) - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) - def test_trigger_autofix_success(self, mock_get_autofix_state, mock_trigger_autofix_helper): - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - assert mock_trigger_autofix_helper.call_count == 1 - assert self.entrypoint.autofix_errors == [] - assert self.entrypoint.autofix_run_ids == [MOCK_RUN_ID] - - @patch("sentry.seer.entrypoints.operator.trigger_legacy_autofix") - @patch("sentry.seer.entrypoints.operator.get_autofix_state") - def test_trigger_autofix_already_exists( - self, mock_get_autofix_state, mock_trigger_autofix_helper - ): - existing_rca_step_state = { - "key": "root_cause_analysis", - "status": AutofixStatus.COMPLETED, - } - existing_state = AutofixState( - run_id=MOCK_RUN_ID, - request={ - "organization_id": self.organization.id, - "project_id": self.project.id, - "issue": {"id": self.group.id, "title": "test"}, - "repos": [], - }, - updated_at=datetime.now(), - status=AutofixStatus.PROCESSING, - steps=[existing_rca_step_state], - ) - mock_get_autofix_state.return_value = existing_state - - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - - mock_trigger_autofix_helper.assert_not_called() - assert self.entrypoint.autofix_already_exists_states == [(existing_state.run_id, True)] - assert self.entrypoint.autofix_run_ids == [] - assert self.entrypoint.autofix_errors == [] - - @patch( - "sentry.seer.entrypoints.operator.trigger_legacy_autofix", - return_value=Response({"run_id": MOCK_RUN_ID}, status=202), - ) - @patch("sentry.seer.entrypoints.operator.get_autofix_state") - def test_trigger_autofix_proceeds_when_completed( - self, mock_get_autofix_state, mock_trigger_autofix_helper - ): - existing_state = AutofixState( - run_id=MOCK_RUN_ID, - request={ - "organization_id": self.organization.id, - "project_id": self.project.id, - "issue": {"id": self.group.id, "title": "test"}, - "repos": [], - }, - updated_at=datetime.now(), - status=AutofixStatus.COMPLETED, - ) - mock_get_autofix_state.return_value = existing_state - - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - - mock_trigger_autofix_helper.assert_called_once() - assert self.entrypoint.autofix_already_exists_states == [] - assert self.entrypoint.autofix_run_ids == [MOCK_RUN_ID] - - @patch("sentry.seer.entrypoints.operator.trigger_legacy_autofix") - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) - def test_trigger_autofix_error(self, mock_get_autofix_state, mock_trigger_autofix_helper): - mock_trigger_autofix_helper.return_value = Response( - {"detail": "Invalid request"}, status=400 - ) - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - mock_trigger_autofix_helper.return_value = Response({"run_id": None}, status=202) - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - assert mock_trigger_autofix_helper.call_count == 2 - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - run_id=MOCK_RUN_ID, - ) - assert self.entrypoint.autofix_errors == [ - "Invalid request", - "An unknown error has occurred", - "Invalid stopping point provided", - ] - assert self.entrypoint.autofix_run_ids == [] - - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) - @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_success(self, mock_trigger_handoff_helper, mock_get_state): - self._set_automation_handoff() - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) - mock_trigger_handoff_helper.assert_called_once() - assert mock_trigger_handoff_helper.call_args.kwargs["referrer"] == AutofixReferrer.SLACK - assert self.entrypoint.handoff_successes == [ - (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT) - ] - assert self.entrypoint.handoff_already_exists == [] - assert self.entrypoint.handoff_errors == [] - - @patch("sentry.seer.entrypoints.operator.get_autofix_state") - @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_already_exists_running( - self, mock_trigger_handoff_helper, mock_get_state - ): - self._set_automation_handoff() - mock_get_state.return_value = self._build_autofix_state_with_agents( - { - "agent-1": LegacyCodingAgentState( - id="agent-1", - status=CodingAgentStatus.RUNNING, - provider=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, - name="Cursor", - started_at=datetime.now(), - ) - } - ) - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) - mock_trigger_handoff_helper.assert_not_called() - assert self.entrypoint.handoff_already_exists == [ - (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, False) - ] - assert self.entrypoint.handoff_successes == [] - - @patch("sentry.seer.entrypoints.operator.get_autofix_state") - @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_already_exists_completed( - self, mock_trigger_handoff_helper, mock_get_state - ): - self._set_automation_handoff() - mock_get_state.return_value = self._build_autofix_state_with_agents( - { - "agent-1": LegacyCodingAgentState( - id="agent-1", - status=CodingAgentStatus.COMPLETED, - provider=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, - name="Cursor", - started_at=datetime.now(), - ) - } - ) - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) - mock_trigger_handoff_helper.assert_not_called() - assert self.entrypoint.handoff_already_exists == [ - (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, True) - ] - - @patch("sentry.seer.entrypoints.operator.get_autofix_state") - @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_proceeds_when_all_agents_failed( - self, mock_trigger_handoff_helper, mock_get_state - ): - self._set_automation_handoff() - mock_get_state.return_value = self._build_autofix_state_with_agents( - { - "agent-1": LegacyCodingAgentState( - id="agent-1", - status=CodingAgentStatus.FAILED, - provider=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, - name="Cursor", - started_at=datetime.now(), - ) - } - ) - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) - mock_trigger_handoff_helper.assert_called_once() - assert self.entrypoint.handoff_successes == [ - (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT) - ] - assert self.entrypoint.handoff_already_exists == [] - @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") def test_trigger_handoff_no_config_is_silent_halt(self, mock_trigger_handoff_helper): self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) @@ -399,29 +149,16 @@ def test_trigger_handoff_no_config_is_silent_halt(self, mock_trigger_handoff_hel assert self.entrypoint.handoff_already_exists == [] assert self.entrypoint.handoff_errors == [] - @patch( - "sentry.seer.entrypoints.operator.get_autofix_state", - side_effect=Exception("seer down"), - ) - @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_state_fetch_error_calls_error_hook( - self, mock_trigger_handoff_helper, mock_get_state - ): - self._set_automation_handoff() - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) - mock_trigger_handoff_helper.assert_not_called() - assert self.entrypoint.handoff_errors == ["Encountered an error while talking to Seer"] - assert self.entrypoint.handoff_successes == [] - - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) + @patch("sentry.seer.entrypoints.operator.fetch_run_status", return_value=None) @patch( "sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff", side_effect=RuntimeError("boom"), ) def test_trigger_handoff_launch_error_calls_error_hook( - self, mock_trigger_handoff_helper, mock_get_state + self, mock_trigger_handoff_helper, mock_fetch_status ): self._set_automation_handoff() + mock_fetch_status.return_value = self._build_explorer_state_with_agents({}) self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) assert self.entrypoint.handoff_errors == [ "Encountered an error while launching the coding agent" @@ -441,11 +178,10 @@ def _build_explorer_state_with_agents( @patch("sentry.seer.entrypoints.operator.fetch_run_status") @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_explorer_success(self, mock_trigger_handoff_helper, mock_fetch_status): + def test_trigger_handoff_success(self, mock_trigger_handoff_helper, mock_fetch_status): self._set_automation_handoff() mock_fetch_status.return_value = self._build_explorer_state_with_agents({}) - with self.feature("organizations:autofix-on-explorer"): - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) + self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) mock_trigger_handoff_helper.assert_called_once() assert mock_trigger_handoff_helper.call_args.kwargs["referrer"] == AutofixReferrer.SLACK assert self.entrypoint.handoff_successes == [ @@ -456,7 +192,7 @@ def test_trigger_handoff_explorer_success(self, mock_trigger_handoff_helper, moc @patch("sentry.seer.entrypoints.operator.fetch_run_status") @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_explorer_already_exists_running( + def test_trigger_handoff_already_exists_running( self, mock_trigger_handoff_helper, mock_fetch_status ): self._set_automation_handoff() @@ -471,8 +207,7 @@ def test_trigger_handoff_explorer_already_exists_running( ) } ) - with self.feature("organizations:autofix-on-explorer"): - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) + self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) mock_trigger_handoff_helper.assert_not_called() assert self.entrypoint.handoff_already_exists == [ (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, False) @@ -480,7 +215,7 @@ def test_trigger_handoff_explorer_already_exists_running( @patch("sentry.seer.entrypoints.operator.fetch_run_status") @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_explorer_already_exists_completed( + def test_trigger_handoff_already_exists_completed( self, mock_trigger_handoff_helper, mock_fetch_status ): self._set_automation_handoff() @@ -495,8 +230,7 @@ def test_trigger_handoff_explorer_already_exists_completed( ) } ) - with self.feature("organizations:autofix-on-explorer"): - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) + self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) mock_trigger_handoff_helper.assert_not_called() assert self.entrypoint.handoff_already_exists == [ (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, True) @@ -504,7 +238,7 @@ def test_trigger_handoff_explorer_already_exists_completed( @patch("sentry.seer.entrypoints.operator.fetch_run_status") @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_explorer_proceeds_when_all_agents_failed( + def test_trigger_handoff_proceeds_when_all_agents_failed( self, mock_trigger_handoff_helper, mock_fetch_status ): self._set_automation_handoff() @@ -519,8 +253,7 @@ def test_trigger_handoff_explorer_proceeds_when_all_agents_failed( ) } ) - with self.feature("organizations:autofix-on-explorer"): - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) + self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) mock_trigger_handoff_helper.assert_called_once() assert self.entrypoint.handoff_successes == [ (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT) @@ -532,39 +265,15 @@ def test_trigger_handoff_explorer_proceeds_when_all_agents_failed( side_effect=Exception("seer down"), ) @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff") - def test_trigger_handoff_explorer_state_fetch_error_calls_error_hook( + def test_trigger_handoff_state_fetch_error_calls_error_hook( self, mock_trigger_handoff_helper, mock_fetch_status ): self._set_automation_handoff() - with self.feature("organizations:autofix-on-explorer"): - self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) + self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID) mock_trigger_handoff_helper.assert_not_called() assert self.entrypoint.handoff_errors == ["Encountered an error while talking to Seer"] assert self.entrypoint.handoff_successes == [] - @patch( - "sentry.seer.entrypoints.operator.trigger_legacy_autofix", - return_value=Response({"run_id": MOCK_RUN_ID}, status=202), - ) - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) - @patch("sentry.seer.entrypoints.cache.SeerOperatorAutofixCache.populate_post_autofix_cache") - def test_trigger_autofix_creates_cache_payload( - self, - mock_populate_post_autofix_cache, - mock_get_autofix_state, - mock_trigger_autofix_helper, - ): - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.ROOT_CAUSE, - ) - mock_populate_post_autofix_cache.assert_called_with( - entrypoint_key=MockAutofixEntrypoint.key, - run_id=MOCK_RUN_ID, - cache_payload=self.entrypoint.create_autofix_cache_payload(), - ) - @patch.object(SeerAutofixOperator, "has_access", return_value=True) @patch.dict( "sentry.seer.entrypoints.operator.autofix_entrypoint_registry.registrations", @@ -693,66 +402,6 @@ def test_process_autofix_updates_skips_entrypoint_without_access( cache_payload=cache_payload, ) - @patch("sentry.seer.entrypoints.operator.update_legacy_autofix") - @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None) - def test_solution_stopping_point_sends_select_root_cause( - self, _mock_get_autofix_state, mock_update_autofix - ): - mock_update_autofix.return_value = Response({"run_id": MOCK_RUN_ID}, status=202) - - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.SOLUTION, - run_id=MOCK_RUN_ID, - ) - - mock_update_autofix.assert_called_once() - call_kwargs = mock_update_autofix.call_args.kwargs - assert call_kwargs["organization_id"] == self.group.organization.id - payload = call_kwargs["payload"] - assert payload["type"] == "select_root_cause" - assert payload["cause_id"] == AUTOFIX_FALLBACK_CAUSE_ID - - @patch("sentry.seer.entrypoints.operator.update_legacy_autofix") - @patch("sentry.seer.entrypoints.operator.get_autofix_state") - def test_solution_stopping_point_uses_cause_id_from_state( - self, mock_get_autofix_state, mock_update_autofix - ): - mock_update_autofix.return_value = Response({"run_id": MOCK_RUN_ID}, status=202) - existing_state = AutofixState( - run_id=MOCK_RUN_ID, - request={ - "organization_id": self.organization.id, - "project_id": self.project.id, - "issue": {"id": self.group.id, "title": "test"}, - "repos": [], - }, - updated_at=datetime.now(), - status=AutofixStatus.PROCESSING, - steps=[ - { - "key": "root_cause_analysis", - "status": AutofixStatus.COMPLETED, - "causes": [{"id": 12}, {"id": 34}], - }, - ], - ) - mock_get_autofix_state.return_value = existing_state - - self.operator.trigger_autofix( - group=self.group, - user=self.user, - stopping_point=AutofixStoppingPoint.SOLUTION, - run_id=MOCK_RUN_ID, - ) - - mock_update_autofix.assert_called_once() - call_kwargs = mock_update_autofix.call_args.kwargs - payload = call_kwargs["payload"] - assert payload["type"] == "select_root_cause" - assert payload["cause_id"] == 34 - def test_can_trigger_autofix_returns_false_without_seer_access(self) -> None: assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False