From dba25984840e2c0c5091479a4d70d42ecd6e9a6f Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 28 Sep 2026 14:16:28 -0700 Subject: [PATCH] ref(seer): Remove organizations:gen-ai-features feature flag The flag was on for every SaaS and single-tenant cell and off only on self-hosted, where no flagpole config exists. With has_seer_access checking the self-hosted setting directly, the flag no longer carries information. Remove the registration, the remaining two-step checks, the bulk task feature lists, and the test fixtures that enabled it. The frontend must stop reading the flag before this deploys, and the flagpole entry must be deleted only after this is deployed everywhere, since deleting it first would turn Seer off for all SaaS orgs. --- .../organization_dashboard_generate.py | 2 +- src/sentry/features/temporary.py | 2 - .../organization_feedback_categories.py | 2 +- .../organization_feedback_summary.py | 2 +- .../integrations/utils/external_issues.py | 2 +- .../endpoints/project_replay_summary.py | 2 +- src/sentry/seer/agent/client.py | 2 +- src/sentry/seer/agent/client_utils.py | 3 +- src/sentry/seer/autofix/issue_summary.py | 5 +- src/sentry/seer/autofix/trigger.py | 5 +- .../endpoints/organization_seer_agent_chat.py | 4 +- .../seer/endpoints/search_agent_start.py | 2 +- .../seer/endpoints/search_agent_state.py | 2 +- .../seer/endpoints/trace_explorer_ai_query.py | 6 +- .../seer/endpoints/trace_explorer_ai_setup.py | 6 +- .../trace_explorer_ai_translate_agentic.py | 2 +- src/sentry/seer/seer_setup.py | 19 +--- src/sentry/tasks/seer/agentic_triage/cron.py | 7 +- src/sentry/tasks/seer/explorer_index.py | 40 ++------- src/sentry/testutils/helpers/github.py | 2 +- ...ganization_uptime_assertion_suggestions.py | 3 +- .../test_organization_dashboard_generate.py | 2 - .../cursor_origin/test_code_review.py | 1 - .../slack/test_message_builder.py | 4 - .../slack/webhooks/events/__init__.py | 1 - tests/sentry/integrations/test_issues.py | 10 +-- .../utils/test_external_issues.py | 15 ++-- tests/sentry/pr_metrics/test_emit.py | 7 +- tests/sentry/pr_metrics/test_webhooks.py | 52 +++++------ tests/sentry/seer/agent/test_agent_client.py | 2 +- tests/sentry/seer/agent/test_client_utils.py | 22 ++--- .../sentry/seer/autofix/test_autofix_agent.py | 5 +- .../sentry/seer/autofix/test_autofix_utils.py | 86 +++++++++---------- .../sentry/seer/autofix/test_issue_summary.py | 16 ++-- .../sentry/seer/code_review/test_preflight.py | 36 ++++---- .../webhooks/test_merge_request.py | 63 +------------- .../code_review/webhooks/test_pull_request.py | 14 +-- .../webhooks/test_review_request.py | 2 +- .../seer/endpoints/test_group_ai_autofix.py | 3 +- .../seer/endpoints/test_group_ai_summary.py | 5 +- .../endpoints/test_group_autofix_repos.py | 2 - .../test_group_autofix_setup_check.py | 7 +- .../test_organization_agent_token.py | 8 +- .../test_organization_seer_agent_chat.py | 10 +-- .../test_organization_seer_agent_update.py | 7 +- .../endpoints/test_organization_seer_runs.py | 1 - .../test_organization_seer_workflows.py | 33 +++---- .../seer/endpoints/test_search_agent_start.py | 3 +- .../seer/endpoints/test_search_agent_state.py | 4 +- .../endpoints/test_trace_explorer_ai_query.py | 18 +++- .../endpoints/test_trace_explorer_ai_setup.py | 20 +++-- ...est_trace_explorer_ai_translate_agentic.py | 3 +- .../seer/entrypoints/slack/test_tasks.py | 5 +- .../sentry/seer/entrypoints/test_operator.py | 27 ++---- tests/sentry/seer/test_seer_setup.py | 9 -- tests/sentry/seer/workflows/test_runs.py | 17 ++-- .../sentry/tasks/seer/test_agentic_triage.py | 55 +++++------- .../sentry/tasks/seer/test_explorer_index.py | 33 +++---- .../sentry/tasks/test_llm_issue_detection.py | 24 +++--- tests/sentry/tasks/test_post_process.py | 33 +++---- .../tasks/test_web_vitals_issue_detection.py | 10 --- 61 files changed, 309 insertions(+), 486 deletions(-) diff --git a/src/sentry/dashboards/endpoints/organization_dashboard_generate.py b/src/sentry/dashboards/endpoints/organization_dashboard_generate.py index eef2f9cc9e50..c9432cc8c239 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboard_generate.py +++ b/src/sentry/dashboards/endpoints/organization_dashboard_generate.py @@ -158,7 +158,7 @@ class OrganizationDashboardGenerateEndpoint(OrganizationEndpoint): permission_classes = (OrganizationDashboardGeneratePermission,) def post(self, request: Request, organization: Organization) -> Response: - has_access, error = has_seer_access_with_detail(organization, request.user) + has_access, error = has_seer_access_with_detail(organization) if not has_access: raise PermissionDenied(error) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 09c380a1b4ca..d76239dcbf9f 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -116,8 +116,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:explore-errors", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable returning the migrated discover queries in explore saved queries manager.add("organizations:expose-migrated-discover-queries", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable GenAI features such as Autofix and Issue Summary - manager.add("organizations:gen-ai-features", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable organization investigation notebooks. manager.add("organizations:investigations", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the 'translate' functionality for GenAI on the explore > traces page diff --git a/src/sentry/feedback/endpoints/organization_feedback_categories.py b/src/sentry/feedback/endpoints/organization_feedback_categories.py index 1bb71c8270ea..13cab166f826 100644 --- a/src/sentry/feedback/endpoints/organization_feedback_categories.py +++ b/src/sentry/feedback/endpoints/organization_feedback_categories.py @@ -103,7 +103,7 @@ def get(self, request: Request, organization: Organization) -> Response: :auth: required """ - if not has_seer_access(organization, actor=request.user): + if not has_seer_access(organization): return Response( {"detail": "AI categorization is not available for this organization."}, status=403 ) diff --git a/src/sentry/feedback/endpoints/organization_feedback_summary.py b/src/sentry/feedback/endpoints/organization_feedback_summary.py index 4f10d724582c..ad5c8096ee69 100644 --- a/src/sentry/feedback/endpoints/organization_feedback_summary.py +++ b/src/sentry/feedback/endpoints/organization_feedback_summary.py @@ -90,7 +90,7 @@ def get(self, request: Request, organization: Organization) -> Response: :auth: required """ - if not has_seer_access(organization, actor=request.user): + if not has_seer_access(organization): return Response( {"detail": "AI summaries are not available for this organization."}, status=403 ) diff --git a/src/sentry/integrations/utils/external_issues.py b/src/sentry/integrations/utils/external_issues.py index 1f0c5b40df57..a32569d252b6 100644 --- a/src/sentry/integrations/utils/external_issues.py +++ b/src/sentry/integrations/utils/external_issues.py @@ -124,7 +124,7 @@ def maybe_generate_external_issue_details( ) -> GeneratedExternalIssueDetails: organization = group.organization empty_result = GeneratedExternalIssueDetails(title=None, description=None) - if not has_seer_access(organization, actor=user): + if not has_seer_access(organization): return empty_result if not features.has("organizations:external-issues-ai-generate", organization, actor=user): return empty_result diff --git a/src/sentry/replays/endpoints/project_replay_summary.py b/src/sentry/replays/endpoints/project_replay_summary.py index 10feec9b7856..512f65827835 100644 --- a/src/sentry/replays/endpoints/project_replay_summary.py +++ b/src/sentry/replays/endpoints/project_replay_summary.py @@ -152,7 +152,7 @@ def has_replay_summary_access(self, project: Project, request: Request) -> bool: project.organization, actor=request.user, ) - and has_seer_access(project.organization, actor=request.user) + and has_seer_access(project.organization) ) def get(self, request: Request, project: Project, replay_id: str) -> Response: diff --git a/src/sentry/seer/agent/client.py b/src/sentry/seer/agent/client.py index 6a32326287a3..b2f6d9e8a23e 100644 --- a/src/sentry/seer/agent/client.py +++ b/src/sentry/seer/agent/client.py @@ -375,7 +375,7 @@ def __init__( raise ValueError("category_key and category_value must be provided together") # Validate base Seer access on init (agent-specific flag checks are done at the endpoint level) - has_access, error = has_seer_access_with_detail(organization, user) + has_access, error = has_seer_access_with_detail(organization) if not has_access: raise SeerPermissionError(error or "Access denied") diff --git a/src/sentry/seer/agent/client_utils.py b/src/sentry/seer/agent/client_utils.py index 18dff49de44e..d426a224a95b 100644 --- a/src/sentry/seer/agent/client_utils.py +++ b/src/sentry/seer/agent/client_utils.py @@ -399,8 +399,7 @@ def has_seer_agent_access_with_detail( Returns: tuple[bool, str | None]: (has_access, error_message) """ - # Check base Seer access (gen-ai-features, hide_ai_features, acknowledgement) - has_access, error = has_seer_access_with_detail(organization, actor) + has_access, error = has_seer_access_with_detail(organization) if not has_access: return False, error diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index 5bfffdcdb5f7..eed0108114b5 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -58,6 +58,7 @@ from sentry.users.services.user.model import RpcUser from sentry.utils.cache import cache from sentry.utils.locking import UnableToAcquireLock +from sentry.utils.settings import is_self_hosted from sentry.utils.tracing import start_span logger = logging.getLogger(__name__) @@ -585,8 +586,8 @@ def get_issue_summary( """ if user is None: user = AnonymousUser() - if not features.has("organizations:gen-ai-features", group.organization, actor=user): - return {"detail": "Feature flag not enabled"}, 400 + if is_self_hosted(): + return {"detail": "Seer is not available on this installation."}, 400 if group.organization.get_option("sentry:hide_ai_features"): return {"detail": "AI features are disabled for this organization."}, 403 diff --git a/src/sentry/seer/autofix/trigger.py b/src/sentry/seer/autofix/trigger.py index 7b73d1ec0d99..1c19574b5d6c 100644 --- a/src/sentry/seer/autofix/trigger.py +++ b/src/sentry/seer/autofix/trigger.py @@ -28,14 +28,15 @@ def get_seer_automation_ineligibility_reason( group: Group, ) -> SeerAutomationIneligibilityReason | None: """Return the reason an issue is ineligible for Seer automation, or None if eligible.""" - from sentry import features, quotas + from sentry import quotas from sentry.constants import DataCategory from sentry.seer.autofix.utils import is_issue_category_eligible + from sentry.utils.settings import is_self_hosted if not is_issue_category_eligible(group): return "not_eligible.issue_category_ineligible" - if not features.has("organizations:gen-ai-features", group.organization): + if is_self_hosted(): return "not_eligible.gen_ai_feature_disabled" gen_ai_allowed = not group.organization.get_option("sentry:hide_ai_features") diff --git a/src/sentry/seer/endpoints/organization_seer_agent_chat.py b/src/sentry/seer/endpoints/organization_seer_agent_chat.py index 8157ae5fb343..3cc129a5cf61 100644 --- a/src/sentry/seer/endpoints/organization_seer_agent_chat.py +++ b/src/sentry/seer/endpoints/organization_seer_agent_chat.py @@ -213,7 +213,7 @@ def get( """ has_access, error = has_seer_agent_access_with_detail(organization, request.user) - has_seer_access, _ = has_seer_access_with_detail(organization, request.user) + has_seer_access, _ = has_seer_access_with_detail(organization) if not has_access and not has_seer_access: raise PermissionDenied(error) @@ -263,7 +263,7 @@ def post( """ has_access, error = has_seer_agent_access_with_detail(organization, request.user) - has_seer_access, _ = has_seer_access_with_detail(organization, request.user) + has_seer_access, _ = has_seer_access_with_detail(organization) # Orgs with Seer access can continue existing dashboard generate runs, but cannot start new runs from this endpoint. can_continue_dashboards_generate_run = has_seer_access and run_id is not None diff --git a/src/sentry/seer/endpoints/search_agent_start.py b/src/sentry/seer/endpoints/search_agent_start.py index 590253241de0..ee06a56a863a 100644 --- a/src/sentry/seer/endpoints/search_agent_start.py +++ b/src/sentry/seer/endpoints/search_agent_start.py @@ -159,7 +159,7 @@ def post(self, request: Request, organization: Organization) -> Response: status=status.HTTP_403_FORBIDDEN, ) - has_seer_access, detail = has_seer_access_with_detail(organization, actor=request.user) + has_seer_access, detail = has_seer_access_with_detail(organization) if not has_seer_access: return Response( {"detail": detail}, diff --git a/src/sentry/seer/endpoints/search_agent_state.py b/src/sentry/seer/endpoints/search_agent_state.py index 6fd186069eea..553b32caf1ea 100644 --- a/src/sentry/seer/endpoints/search_agent_state.py +++ b/src/sentry/seer/endpoints/search_agent_state.py @@ -91,7 +91,7 @@ def get(self, request: Request, organization: Organization, run_id: str) -> Resp status=status.HTTP_403_FORBIDDEN, ) - has_seer_access, detail = has_seer_access_with_detail(organization, actor=request.user) + has_seer_access, detail = has_seer_access_with_detail(organization) if not has_seer_access: return Response( {"detail": detail}, diff --git a/src/sentry/seer/endpoints/trace_explorer_ai_query.py b/src/sentry/seer/endpoints/trace_explorer_ai_query.py index 12146d67bf06..8d2ba2703c4f 100644 --- a/src/sentry/seer/endpoints/trace_explorer_ai_query.py +++ b/src/sentry/seer/endpoints/trace_explorer_ai_query.py @@ -8,7 +8,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -21,6 +20,7 @@ TranslateQueryRequest, make_translate_query_request, ) +from sentry.utils.settings import is_self_hosted logger = logging.getLogger(__name__) @@ -97,9 +97,7 @@ def post(self, request: Request, organization: Organization) -> Response: status=status.HTTP_403_FORBIDDEN, ) - if not features.has( - "organizations:gen-ai-features", organization=organization, actor=request.user - ): + if is_self_hosted(): return Response( {"detail": "Organization does not have access to this feature"}, status=status.HTTP_403_FORBIDDEN, diff --git a/src/sentry/seer/endpoints/trace_explorer_ai_setup.py b/src/sentry/seer/endpoints/trace_explorer_ai_setup.py index 6c34880a50f8..ded20fa91ec1 100644 --- a/src/sentry/seer/endpoints/trace_explorer_ai_setup.py +++ b/src/sentry/seer/endpoints/trace_explorer_ai_setup.py @@ -7,7 +7,6 @@ from rest_framework.exceptions import ParseError from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -20,6 +19,7 @@ SeerViewerContext, make_create_cache_request, ) +from sentry.utils.settings import is_self_hosted logger = logging.getLogger(__name__) @@ -84,9 +84,7 @@ def post(self, request: Request, organization: Organization) -> Response: status=status.HTTP_403_FORBIDDEN, ) - if not features.has( - "organizations:gen-ai-features", organization=organization, actor=request.user - ): + if is_self_hosted(): return Response( {"detail": "Organization does not have access to this feature"}, status=status.HTTP_403_FORBIDDEN, diff --git a/src/sentry/seer/endpoints/trace_explorer_ai_translate_agentic.py b/src/sentry/seer/endpoints/trace_explorer_ai_translate_agentic.py index bb29f39223e7..0c4cb80a3d6e 100644 --- a/src/sentry/seer/endpoints/trace_explorer_ai_translate_agentic.py +++ b/src/sentry/seer/endpoints/trace_explorer_ai_translate_agentic.py @@ -139,7 +139,7 @@ def post(self, request: Request, organization: Organization) -> Response: status=status.HTTP_403_FORBIDDEN, ) - has_seer_access, detail = has_seer_access_with_detail(organization, actor=request.user) + has_seer_access, detail = has_seer_access_with_detail(organization) if not has_seer_access: return Response( {"detail": detail}, diff --git a/src/sentry/seer/seer_setup.py b/src/sentry/seer/seer_setup.py index e77315892bcf..8a1991f5f359 100644 --- a/src/sentry/seer/seer_setup.py +++ b/src/sentry/seer/seer_setup.py @@ -1,5 +1,3 @@ -from django.contrib.auth.models import AnonymousUser - from sentry import features from sentry.models.organization import Organization from sentry.organizations.services.organization.model import RpcOrganization @@ -8,8 +6,6 @@ SEER_GITLAB_SCM_PROVIDERS, SEER_SUPPORTED_SCM_PROVIDERS, ) -from sentry.users.models.user import User -from sentry.users.services.user.model import RpcUser from sentry.utils.settings import is_self_hosted @@ -24,27 +20,16 @@ def get_supported_scm_providers(organization: Organization | None = None) -> lis return providers -def has_seer_access( - organization: Organization | RpcOrganization, - actor: User | AnonymousUser | RpcUser | None = None, -) -> bool: - return ( - not is_self_hosted() - and features.has("organizations:gen-ai-features", organization, actor=actor) - and not bool(organization.get_option("sentry:hide_ai_features")) - ) +def has_seer_access(organization: Organization | RpcOrganization) -> bool: + return not is_self_hosted() and not bool(organization.get_option("sentry:hide_ai_features")) def has_seer_access_with_detail( organization: Organization | RpcOrganization, - actor: User | AnonymousUser | RpcUser | None = None, ) -> tuple[bool, str | None]: if is_self_hosted(): return False, "Seer is not available on this installation." - if not features.has("organizations:gen-ai-features", organization, actor=actor): - return False, "Feature flag not enabled" - if organization.get_option("sentry:hide_ai_features"): return False, "AI features are disabled for this organization." diff --git a/src/sentry/tasks/seer/agentic_triage/cron.py b/src/sentry/tasks/seer/agentic_triage/cron.py index 832d44f83c87..889b2afa57a9 100644 --- a/src/sentry/tasks/seer/agentic_triage/cron.py +++ b/src/sentry/tasks/seer/agentic_triage/cron.py @@ -72,6 +72,7 @@ from sentry.utils.hashlib import md5_text from sentry.utils.iterators import chunked from sentry.utils.query import RangeQuerySetWrapper +from sentry.utils.settings import is_self_hosted logger = logging.getLogger("sentry.tasks.seer.agentic_triage") @@ -79,7 +80,6 @@ BATCH_FEATURE_NAMES = [ "organizations:seer-night-shift", - "organizations:gen-ai-features", ] PER_ORG_FEATURE_NAMES = [ # INTERNAL handlers aren't routed through batch_has_for_organizations, @@ -611,6 +611,9 @@ def _get_eligible_orgs_from_batch( Check feature flags for a batch of orgs. Returns orgs that have all required feature flags enabled. """ + if is_self_hosted(): + return [] + # enable_seer_coding off => agentic triage can't open a PR for the org. enable_coding = OrganizationOption.objects.get_value_bulk( orgs, "sentry:enable_seer_coding", ENABLE_SEER_CODING_DEFAULT @@ -639,7 +642,7 @@ def _get_eligible_orgs_from_batch( for org in eligible: if all(features.has(f, org) for f in PER_ORG_FEATURE_NAMES): paid_eligible.append(org) - elif features.has("organizations:gen-ai-features", org) and is_free_cohort_org(org): + elif is_free_cohort_org(org): free_cohort_eligible.append(org) return paid_eligible + free_cohort_eligible diff --git a/src/sentry/tasks/seer/explorer_index.py b/src/sentry/tasks/seer/explorer_index.py index 2d5f6e7ade43..1f8a3f006119 100644 --- a/src/sentry/tasks/seer/explorer_index.py +++ b/src/sentry/tasks/seer/explorer_index.py @@ -21,6 +21,7 @@ from sentry.tasks.utils import compute_delay from sentry.taskworker.namespaces import seer_tasks from sentry.utils.query import RangeQuerySetWrapper +from sentry.utils.settings import is_self_hosted from sentry.utils.tracing import start_span from sentry.viewer_context import ActorType, ViewerContext, viewer_context_scope @@ -34,7 +35,6 @@ EXPLORER_INDEX_DISPATCH_STEP = timedelta(seconds=37) FEATURE_NAMES = [ - "organizations:gen-ai-features", "organizations:seer-explorer-index", "organizations:seat-based-seer-enabled", "organizations:seer-added", @@ -48,6 +48,9 @@ def get_seer_explorer_enabled_projects() -> Generator[tuple[int, int]]: Yields: Tuple of (project_id, organization_id) """ + if is_self_hosted(): + return + projects = Project.objects.filter(status=ObjectStatus.ACTIVE).select_related("organization") current_hour = django_timezone.now().hour @@ -69,43 +72,18 @@ def get_seer_explorer_enabled_projects() -> Generator[tuple[int, int]]: if bool(project.organization.get_option("sentry:hide_ai_features")): continue - is_eligible = False with start_span( op="seer_explorer_index.has_feature", name="seer_explorer_index.has_feature" ): batch_result = features.batch_has(FEATURE_NAMES, organization=project.organization) - if batch_result: - org_key = f"organization:{project.organization.id}" - org_features = batch_result.get(org_key, {}) - has_gen_ai = org_features.get("organizations:gen-ai-features", False) - has_explorer_index = org_features.get("organizations:seer-explorer-index", False) - - if has_explorer_index and has_gen_ai: - is_eligible = True - - has_seer_plan = org_features.get( - "organizations:seat-based-seer-enabled", False - ) or org_features.get("organizations:seer-added", False) - - if has_seer_plan and has_gen_ai: - is_eligible = True - + org_features = batch_result.get(f"organization:{project.organization.id}", {}) else: - has_gen_ai = features.has("organizations:gen-ai-features", project.organization) - has_explorer_index = features.has( - "organizations:seer-explorer-index", project.organization - ) - - if has_explorer_index and has_gen_ai: - is_eligible = True - - has_seer_plan = features.has( - "organizations:seat-based-seer-enabled", project.organization - ) or features.has("organizations:seer-added", project.organization) + org_features = { + name: features.has(name, project.organization) for name in FEATURE_NAMES + } - if has_seer_plan and has_gen_ai: - is_eligible = True + is_eligible = any(org_features.get(name, False) for name in FEATURE_NAMES) if not is_eligible: continue diff --git a/src/sentry/testutils/helpers/github.py b/src/sentry/testutils/helpers/github.py index 4ad4c8046938..b6bc753fe04e 100644 --- a/src/sentry/testutils/helpers/github.py +++ b/src/sentry/testutils/helpers/github.py @@ -144,7 +144,7 @@ def send_github_webhook_event( @override_settings(SENTRY_SELF_HOSTED=False) class GitHubWebhookCodeReviewTestCase(GitHubWebhookTestCase): # Code review features are org features as set in options automator - CODE_REVIEW_FEATURES = {"organizations:gen-ai-features", "organizations:code-review-beta"} + CODE_REVIEW_FEATURES = {"organizations:code-review-beta"} # Options to set are regional options as set in options automator OPTIONS_TO_SET: dict[str, Any] = {} # Org options are org options as set via OrganizationOption.objects.set_value diff --git a/src/sentry/uptime/endpoints/organization_uptime_assertion_suggestions.py b/src/sentry/uptime/endpoints/organization_uptime_assertion_suggestions.py index 855aa428ac54..4f4a5d91945b 100644 --- a/src/sentry/uptime/endpoints/organization_uptime_assertion_suggestions.py +++ b/src/sentry/uptime/endpoints/organization_uptime_assertion_suggestions.py @@ -84,8 +84,7 @@ def post( request: Request, organization: Organization, ) -> Response: - # Check if AI features are enabled (includes gen-ai-features flag + hide_ai_features opt-out) - if not has_seer_access(organization, actor=request.user): + if not has_seer_access(organization): return self.respond( {"detail": "AI features are not enabled for this organization"}, status=403, diff --git a/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py b/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py index bdca7ac3e484..cd6e418742a4 100644 --- a/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py +++ b/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py @@ -9,11 +9,9 @@ from sentry.dashboards.on_completion_hook import DashboardOnCompletionHook from sentry.seer.models import SeerPermissionError from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.features import with_feature @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class OrganizationDashboardGenerateEndpointTest(APITestCase): endpoint = "sentry-api-0-organization-dashboards-generate" diff --git a/tests/sentry/integrations/cursor_origin/test_code_review.py b/tests/sentry/integrations/cursor_origin/test_code_review.py index d65ddacf35c5..81c149da6ed3 100644 --- a/tests/sentry/integrations/cursor_origin/test_code_review.py +++ b/tests/sentry/integrations/cursor_origin/test_code_review.py @@ -24,7 +24,6 @@ REPO_EXTERNAL_ID = "r_01example" USER_ID = "user_01example" FEATURES = { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-cursor-origin-support", } diff --git a/tests/sentry/integrations/slack/test_message_builder.py b/tests/sentry/integrations/slack/test_message_builder.py index db77174a8167..e4dea6adbd88 100644 --- a/tests/sentry/integrations/slack/test_message_builder.py +++ b/tests/sentry/integrations/slack/test_message_builder.py @@ -42,7 +42,6 @@ from sentry.silo.base import SiloMode from sentry.testutils.cases import PerformanceIssueTestCase, TestCase from sentry.testutils.factories import EventType -from sentry.testutils.helpers import with_feature from sentry.testutils.helpers.datetime import before_now, freeze_time from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode @@ -1000,7 +999,6 @@ def _has_autofix_button(self, blocks: dict[str, Any]) -> bool: @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) - @with_feature({"organizations:gen-ai-features": True}) def test_autofix_button_shown_when_all_conditions_met(self, mock_quota: MagicMock) -> None: group = self.create_group(project=self.project) blocks = SlackIssuesMessageBuilder(group).build() @@ -1008,7 +1006,6 @@ def test_autofix_button_shown_when_all_conditions_met(self, mock_quota: MagicMoc @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) - @with_feature({"organizations:gen-ai-features": True}) def test_autofix_button_hidden_on_unfurl(self, mock_quota: MagicMock) -> None: group = self.create_group(project=self.project) blocks = SlackIssuesMessageBuilder(group, is_unfurl=True).build() @@ -1016,7 +1013,6 @@ def test_autofix_button_hidden_on_unfurl(self, mock_quota: MagicMock) -> None: @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) - @with_feature({"organizations:gen-ai-features": True}) def test_autofix_button_hidden_when_no_other_actions(self, mock_quota: MagicMock) -> None: group = self.create_group(project=self.project) blocks = SlackIssuesMessageBuilder(group, issue_details=True).build() diff --git a/tests/sentry/integrations/slack/webhooks/events/__init__.py b/tests/sentry/integrations/slack/webhooks/events/__init__.py index 7baa2039fc9f..acf49f3e5962 100644 --- a/tests/sentry/integrations/slack/webhooks/events/__init__.py +++ b/tests/sentry/integrations/slack/webhooks/events/__init__.py @@ -13,7 +13,6 @@ UNSET = object() SEER_EXPLORER_FEATURES = { - "organizations:gen-ai-features": True, "organizations:seer-explorer": True, } diff --git a/tests/sentry/integrations/test_issues.py b/tests/sentry/integrations/test_issues.py index 83e842e38b2c..c1f9a01bdc76 100644 --- a/tests/sentry/integrations/test_issues.py +++ b/tests/sentry/integrations/test_issues.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch import pytest +from django.test import override_settings from django.utils import timezone from sentry.analytics.events.issue_resolved import IssueResolvedEvent @@ -635,6 +636,7 @@ def test_status_sync_inbound_unresolve_webhook_and_sends_to_sentry_app( assert data["installation"]["uuid"] == str(self.sentry_app_installation.uuid) +@override_settings(SENTRY_SELF_HOSTED=False) class IssueDefaultTest(TestCase): def setUp(self) -> None: event = self.store_event( @@ -823,9 +825,7 @@ def test_feature_flag_disabled_skips_ai(self, mock_request: MagicMock) -> None: def test_hide_ai_features_skips_ai(self, mock_request: MagicMock) -> None: self.group.organization.update_option("sentry:hide_ai_features", True) - with self.feature( - ["organizations:gen-ai-features", "organizations:external-issues-ai-generate"] - ): + with self.feature(["organizations:external-issues-ai-generate"]): config = self.installation.get_create_issue_config(self.group, self.user) title_field = next(f for f in config if f["name"] == "title") @@ -836,9 +836,7 @@ def test_hide_ai_features_skips_ai(self, mock_request: MagicMock) -> None: def test_ai_exception_falls_back(self, mock_request: MagicMock) -> None: mock_request.side_effect = Exception("Connection error") - with self.feature( - ["organizations:gen-ai-features", "organizations:external-issues-ai-generate"] - ): + with self.feature(["organizations:external-issues-ai-generate"]): config = self.installation.get_create_issue_config(self.group, self.user) title_field = next(f for f in config if f["name"] == "title") diff --git a/tests/sentry/integrations/utils/test_external_issues.py b/tests/sentry/integrations/utils/test_external_issues.py index 357adac5eedd..6922d16f8630 100644 --- a/tests/sentry/integrations/utils/test_external_issues.py +++ b/tests/sentry/integrations/utils/test_external_issues.py @@ -117,16 +117,15 @@ def test_feature_flag_disabled_returns_empty(self, mock_request: MagicMock) -> N def test_hide_ai_features_returns_empty(self, mock_request: MagicMock) -> None: self.group.organization.update_option("sentry:hide_ai_features", True) - with self.feature( - ["organizations:gen-ai-features", "organizations:external-issues-ai-generate"] - ): + with self.feature(["organizations:external-issues-ai-generate"]): result = maybe_generate_external_issue_details(group=self.group, user=self.user) assert result == GeneratedExternalIssueDetails(title=None, description=None) mock_request.assert_not_called() + @override_settings(SENTRY_SELF_HOSTED=True) @patch("sentry.integrations.utils.external_issues.make_llm_generate_request") - def test_gen_ai_features_disabled_returns_empty(self, mock_request: MagicMock) -> None: + def test_self_hosted_returns_empty(self, mock_request: MagicMock) -> None: with self.feature("organizations:external-issues-ai-generate"): result = maybe_generate_external_issue_details(group=self.group, user=self.user) @@ -137,9 +136,7 @@ def test_gen_ai_features_disabled_returns_empty(self, mock_request: MagicMock) - def test_exception_returns_empty(self, mock_request: MagicMock) -> None: mock_request.side_effect = Exception("Connection error") - with self.feature( - ["organizations:gen-ai-features", "organizations:external-issues-ai-generate"] - ): + with self.feature(["organizations:external-issues-ai-generate"]): result = maybe_generate_external_issue_details(group=self.group, user=self.user) assert result == GeneratedExternalIssueDetails(title=None, description=None) @@ -152,9 +149,7 @@ def test_successful_returns_details(self, mock_request: MagicMock) -> None: } mock_request.return_value = mock_response - with self.feature( - ["organizations:gen-ai-features", "organizations:external-issues-ai-generate"] - ): + with self.feature(["organizations:external-issues-ai-generate"]): result = maybe_generate_external_issue_details(group=self.group, user=self.user) assert result == GeneratedExternalIssueDetails( diff --git a/tests/sentry/pr_metrics/test_emit.py b/tests/sentry/pr_metrics/test_emit.py index 9f3cb3cf2fdb..75007a60b782 100644 --- a/tests/sentry/pr_metrics/test_emit.py +++ b/tests/sentry/pr_metrics/test_emit.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from django.test import override_settings from sentry.analytics.events.pr_metrics_events import PrCloseMetricsEvent from sentry.models.grouplink import GroupLink @@ -175,9 +176,9 @@ def _doc_suite( @with_feature( [ "organizations:pr-metrics", - "organizations:gen-ai-features", ] ) +@override_settings(SENTRY_SELF_HOSTED=False) class PrMetricsEmissionTest(TestCase): def setUp(self) -> None: self.repo = self.create_repo( @@ -1697,9 +1698,9 @@ def test_untracked_pr_does_not_enqueue_cleanup( @with_feature( [ "organizations:pr-metrics", - "organizations:gen-ai-features", ] ) +@override_settings(SENTRY_SELF_HOSTED=False) class MultiOrgEmissionDedupeTest(TestCase): """A provider PR shared across orgs fans out to one tracked row per org; only the canonical (run's-org) row should emit.""" @@ -1826,9 +1827,9 @@ def test_tracked_sibling_emits_when_lower_id_row_untracked(self, mock_record: An @with_feature( [ "organizations:pr-metrics", - "organizations:gen-ai-features", ] ) +@override_settings(SENTRY_SELF_HOSTED=False) class DeduplicationKeyTest(TestCase): """The same provider PR, fanned out to one row per org, must build the same opaque deduplication_key so a consumer can collapse them.""" diff --git a/tests/sentry/pr_metrics/test_webhooks.py b/tests/sentry/pr_metrics/test_webhooks.py index 10885137b1ac..7db79315cdc5 100644 --- a/tests/sentry/pr_metrics/test_webhooks.py +++ b/tests/sentry/pr_metrics/test_webhooks.py @@ -392,8 +392,9 @@ def test_missing_pr_logs_unresolved_and_does_not_raise(self) -> None: CLOSED_AT = datetime(2020, 6, 4, 10, 0, 0, tzinfo=timezone.utc) -@with_feature(["organizations:pr-metrics", "organizations:gen-ai-features"]) +@with_feature(["organizations:pr-metrics"]) @cell_silo_test +@override_settings(SENTRY_SELF_HOSTED=False) class HandleWebhookForPrMetricsEmissionTest(TestCase): def setUp(self) -> None: self.project = self.create_project(organization=self.organization) @@ -565,12 +566,12 @@ def test_does_nothing_when_flag_off(self, mock_record: MagicMock) -> None: ).exists() @patch("sentry.analytics.record") + @override_settings(SENTRY_SELF_HOSTED=True) def test_emits_without_seer_access(self, mock_record: MagicMock) -> None: # Seer access is no longer required for activity tracking, so the # commits-after-open signal is present regardless — a clean merge can # still resolve to merged_unchanged without Seer access. - with self.feature({"organizations:gen-ai-features": False}): - self._call(merged=True) + self._call(merged=True) assert get_event_count(mock_record, PrCloseMetricsEvent) == 1 assert ( PullRequestMetrics.objects.get(pull_request=self.pull_request).verdict @@ -648,8 +649,9 @@ def test_missing_pr_logs_unresolved_and_does_not_emit( assert get_event_count(mock_record, PrCloseMetricsEvent) == 0 -@with_feature(["organizations:pr-metrics", "organizations:gen-ai-features"]) +@with_feature(["organizations:pr-metrics"]) @cell_silo_test +@override_settings(SENTRY_SELF_HOSTED=False) class HandleWebhookForPrMetricsCooldownTest(TestCase): """The webhook-side scheduling of deferred emission and its cooldown claim.""" @@ -1307,9 +1309,9 @@ def test_unhandled_actions_do_not_write_activity(self) -> None: assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists() + @override_settings(SENTRY_SELF_HOSTED=True) def test_activity_written_without_seer_access(self) -> None: - with self.feature({"organizations:gen-ai-features": False}): - self._call(action="opened") + self._call(action="opened") assert PullRequestActivity.objects.filter(pull_request=self.pr).exists() @@ -1462,9 +1464,9 @@ def test_regular_comment_has_is_review_false(self) -> None: activity = PullRequestActivity.objects.get(pull_request=self.pr) assert activity.payload["is_review"] is False + @override_settings(SENTRY_SELF_HOSTED=True) def test_comment_written_without_seer_access(self) -> None: - with self.feature({"organizations:gen-ai-features": False}): - self._call() + self._call() assert PullRequestActivity.objects.filter(pull_request=self.pr).exists() @@ -1643,9 +1645,9 @@ def test_unknown_pr_number_logs_unresolved_and_does_not_raise(self) -> None: ) assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists() + @override_settings(SENTRY_SELF_HOSTED=True) def test_review_written_without_seer_access(self) -> None: - with self.feature({"organizations:gen-ai-features": False}): - self._call() + self._call() assert PullRequestActivity.objects.filter(pull_request=self.pr).exists() @@ -1756,9 +1758,9 @@ def test_unknown_pr_number_logs_unresolved_and_does_not_raise(self) -> None: ) assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists() + @override_settings(SENTRY_SELF_HOSTED=True) def test_review_comment_written_without_seer_access(self) -> None: - with self.feature({"organizations:gen-ai-features": False}): - self._call() + self._call() assert PullRequestActivity.objects.filter(pull_request=self.pr).exists() @@ -1864,9 +1866,9 @@ def test_unknown_pr_number_logs_unresolved_and_does_not_raise(self) -> None: ) assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists() + @override_settings(SENTRY_SELF_HOSTED=True) def test_thread_event_written_without_seer_access(self) -> None: - with self.feature({"organizations:gen-ai-features": False}): - self._call() + self._call() assert PullRequestActivity.objects.filter(pull_request=self.pr).exists() @@ -2059,9 +2061,9 @@ def test_check_suite_flag_off_skips(self) -> None: assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists() + @override_settings(SENTRY_SELF_HOSTED=True) def test_check_suite_written_without_seer_access(self) -> None: - with self.feature({"organizations:gen-ai-features": False}): - self._call_suite() + self._call_suite() assert PullRequestActivity.objects.filter(pull_request=self.pr).exists() @@ -2173,9 +2175,9 @@ def test_check_suite_judge_in_progress_skips(self) -> None: assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists() -@override_settings(SENTRY_SELF_HOSTED=False) -@with_feature(["organizations:pr-metrics", "organizations:gen-ai-features"]) +@with_feature(["organizations:pr-metrics"]) @cell_silo_test +@override_settings(SENTRY_SELF_HOSTED=False) class HandleWebhookForPrMetricsJudgeForwardTest(TestCase): """The needs-judge branch: claim the sentinel and forward.""" @@ -2310,12 +2312,12 @@ def test_untracked_pr_is_not_forwarded( @patch("sentry.pr_metrics.tasks.forward_pr_to_seer_task.delay") @patch("sentry.analytics.record") + @override_settings(SENTRY_SELF_HOSTED=True) def test_no_seer_access_skips_judge( self, mock_record: MagicMock, mock_delay: MagicMock ) -> None: # Without Seer access the judge path is not eligible regardless of attribution. - with self.feature({"organizations:gen-ai-features": False}): - self._call() + self._call() assert mock_delay.call_count == 0 assert PullRequestMetrics.objects.get(pull_request=self.pull_request).verdict is None @@ -2340,17 +2342,17 @@ def test_ineligible_attribution_emits_merged_with_iteration( @patch(f"{MODULE}.forward_pr_to_seer_task.delay") @patch("sentry.analytics.record") + @override_settings(SENTRY_SELF_HOSTED=True) def test_ineligible_attribution_emits_without_seer_access( self, mock_record: MagicMock, mock_delay: MagicMock ) -> None: # The fallback never talks to Seer, so an org's Seer-access consent gate - # (gen-ai-features / hide_ai_features) must not block it — only the actual - # forward-to-Seer branch, reached for judge-eligible attribution, needs it. + # must not block it — only the actual forward-to-Seer branch, reached for + # judge-eligible attribution, needs it. PullRequestAttribution.objects.filter(pull_request=self.pull_request).update( signal_type=PullRequestAttributionSignalType.MCP ) - with self.feature({"organizations:gen-ai-features": False}): - self._call() + self._call() assert mock_delay.call_count == 0 assert PullRequestMetrics.objects.get(pull_request=self.pull_request).verdict == ( "merged_with_iteration" @@ -2398,10 +2400,10 @@ def test_ineligible_attribution_stays_unemitted_when_indeterminate( @with_feature( [ "organizations:pr-metrics", - "organizations:gen-ai-features", ] ) @cell_silo_test +@override_settings(SENTRY_SELF_HOSTED=False) class HandleDelegatedAgentDetectionTest(TestCase): def setUp(self) -> None: self.project = self.create_project(organization=self.organization) diff --git a/tests/sentry/seer/agent/test_agent_client.py b/tests/sentry/seer/agent/test_agent_client.py index 82851adfd0c7..0694e8845ced 100644 --- a/tests/sentry/seer/agent/test_agent_client.py +++ b/tests/sentry/seer/agent/test_agent_client.py @@ -1629,7 +1629,7 @@ def test_flush_true_dispatch_failure_marks_failed_and_raises( assert run.seer_run_state_id is None def test_access_gate_blocks_dispatch(self) -> None: - # No gen-ai-features -> client construction raises before any run is created. + # No Seer access -> client construction raises before any run is created. with pytest.raises(SeerPermissionError): SeerAgentClient(self.organization, self.user) assert not SeerRun.objects.filter(organization=self.organization).exists() diff --git a/tests/sentry/seer/agent/test_client_utils.py b/tests/sentry/seer/agent/test_client_utils.py index 2244609057ca..4eeec61f9eeb 100644 --- a/tests/sentry/seer/agent/test_client_utils.py +++ b/tests/sentry/seer/agent/test_client_utils.py @@ -36,37 +36,29 @@ def setUp(self) -> None: self.org.flags.allow_joinleave = True self.org.save() - def test_gen_ai_features_disabled(self) -> None: + @override_settings(SENTRY_SELF_HOSTED=True) + def test_denied_on_self_hosted(self) -> None: result = has_seer_agent_access_with_detail(self.org, self.user) - assert result == (False, "Feature flag not enabled") + assert result == (False, "Seer is not available on this installation.") def test_hide_ai_features_option_set(self) -> None: self.org.update_option("sentry:hide_ai_features", True) - with self.feature("organizations:gen-ai-features"): - result = has_seer_agent_access_with_detail(self.org, self.user) + 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_flag_enabled(self) -> None: - with self.feature("organizations:gen-ai-features"): - result = has_seer_agent_access_with_detail(self.org, self.user) + result = has_seer_agent_access_with_detail(self.org, self.user) assert result == (False, "Feature flag not enabled") def test_seer_explorer_flag_enabled(self) -> None: - with self.feature( - {"organizations:gen-ai-features": True, "organizations:seer-explorer": True} - ): + with self.feature({"organizations:seer-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() - with self.feature( - { - "organizations:gen-ai-features": True, - "organizations:seer-explorer": True, - } - ): + with self.feature({"organizations:seer-explorer": True}): result = has_seer_agent_access_with_detail(self.org, self.user) assert result == ( False, diff --git a/tests/sentry/seer/autofix/test_autofix_agent.py b/tests/sentry/seer/autofix/test_autofix_agent.py index d028e7538c73..f8e441ad8988 100644 --- a/tests/sentry/seer/autofix/test_autofix_agent.py +++ b/tests/sentry/seer/autofix/test_autofix_agent.py @@ -1832,7 +1832,7 @@ def setUp(self): super().setUp() self.group = self.create_group(project=self.project) - def _push(self, mock_post, features="organizations:gen-ai-features", **kwargs): + def _push(self, mock_post, features=None, **kwargs): """Push with a minimal run state and return the payload sent to Seer.""" mock_post.return_value = MagicMock(status=200) state = SeerRunState( @@ -1844,7 +1844,7 @@ def _push(self, mock_post, features="organizations:gen-ai-features", **kwargs): metadata={"group_id": self.group.id}, ) - with self.feature(features): + with self.feature(features or {}): trigger_push_changes( group=self.group, run_id=123, @@ -1888,7 +1888,6 @@ def test_opens_as_draft_when_review_request_enabled(self, mock_post): payload = self._push( mock_post, features={ - "organizations:gen-ai-features": True, "organizations:autofix-pr-iteration-review-request": True, }, ) diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index ecfaf5a7414d..e4a13da0bb47 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch import pytest +from django.test import override_settings from sentry.constants import ( SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, @@ -128,6 +129,7 @@ def test_autofix_state_validate_parses_nested_structures(self) -> None: assert state.coding_agents["agent-1"].status == CodingAgentStatus.COMPLETED +@override_settings(SENTRY_SELF_HOSTED=False) class TestIsIssueEligibleForSeerAutomation(TestCase): """Test the is_issue_eligible_for_seer_automation function.""" @@ -138,75 +140,68 @@ def setUp(self) -> None: self.group = self.create_group(project=self.project) def test_returns_false_for_unsupported_issue_categories(self) -> None: - """Test returns False for unsupported issue categories like REPLAY and FEEDBACK.""" - from sentry.issues.grouptype import FeedbackGroup, ReplayRageClickType + """Test returns False for unsupported issue categories like FEEDBACK.""" + from sentry.issues.grouptype import FeedbackGroup - # Create groups with unsupported categories - replay_group = self.create_group(project=self.project, type=ReplayRageClickType.type_id) feedback_group = self.create_group(project=self.project, type=FeedbackGroup.type_id) - assert is_issue_eligible_for_seer_automation(replay_group) is False assert is_issue_eligible_for_seer_automation(feedback_group) is False def test_returns_true_for_supported_issue_categories(self) -> None: """Test returns True for supported issue categories when all conditions are met.""" - with self.feature("organizations:gen-ai-features"): - with patch("sentry.quotas.backend.check_seer_quota") as mock_budget: - mock_budget.return_value = True - self.project.update_option("sentry:seer_scanner_automation", True) + with patch("sentry.quotas.backend.check_seer_quota") as mock_budget: + mock_budget.return_value = True + self.project.update_option("sentry:seer_scanner_automation", True) - # Test supported categories - using default error group - result = is_issue_eligible_for_seer_automation(self.group) + # Test supported categories - using default error group + result = is_issue_eligible_for_seer_automation(self.group) - assert result is True + assert result is True - def test_returns_false_when_gen_ai_features_not_enabled(self) -> None: - """Test returns False when organizations:gen-ai-features feature flag is not enabled.""" + @override_settings(SENTRY_SELF_HOSTED=True) + def test_returns_false_when_self_hosted(self) -> None: + """Test returns False when Seer is not available (self-hosted).""" result = is_issue_eligible_for_seer_automation(self.group) assert result is False def test_returns_false_when_ai_features_hidden(self) -> None: """Test returns False when sentry:hide_ai_features option is enabled.""" - with self.feature("organizations:gen-ai-features"): - self.organization.update_option("sentry:hide_ai_features", True) - result = is_issue_eligible_for_seer_automation(self.group) - assert result is False + self.organization.update_option("sentry:hide_ai_features", True) + result = is_issue_eligible_for_seer_automation(self.group) + assert result is False def test_returns_false_when_scanner_automation_disabled_and_not_always_trigger(self) -> None: """Test returns False when scanner automation is disabled and issue type doesn't always trigger.""" - with self.feature("organizations:gen-ai-features"): - self.project.update_option("sentry:seer_scanner_automation", False) - result = is_issue_eligible_for_seer_automation(self.group) - assert result is False + self.project.update_option("sentry:seer_scanner_automation", False) + result = is_issue_eligible_for_seer_automation(self.group) + assert result is False @patch("sentry.quotas.backend.check_seer_quota") def test_returns_false_when_no_budget_available(self, mock_has_budget): """Test returns False when organization has no available budget for scanner.""" - with self.feature("organizations:gen-ai-features"): - self.project.update_option("sentry:seer_scanner_automation", True) - mock_has_budget.return_value = False + self.project.update_option("sentry:seer_scanner_automation", True) + mock_has_budget.return_value = False - result = is_issue_eligible_for_seer_automation(self.group) + result = is_issue_eligible_for_seer_automation(self.group) - assert result is False - mock_has_budget.assert_called_once_with( - org_id=self.organization.id, data_category=DataCategory.SEER_SCANNER - ) + assert result is False + mock_has_budget.assert_called_once_with( + org_id=self.organization.id, data_category=DataCategory.SEER_SCANNER + ) @patch("sentry.quotas.backend.check_seer_quota") def test_returns_true_when_all_conditions_met(self, mock_has_budget): """Test returns True when all eligibility conditions are met.""" - with self.feature("organizations:gen-ai-features"): - self.project.update_option("sentry:seer_scanner_automation", True) + self.project.update_option("sentry:seer_scanner_automation", True) - mock_has_budget.return_value = True + mock_has_budget.return_value = True - result = is_issue_eligible_for_seer_automation(self.group) + result = is_issue_eligible_for_seer_automation(self.group) - assert result is True - mock_has_budget.assert_called_once_with( - org_id=self.organization.id, data_category=DataCategory.SEER_SCANNER - ) + assert result is True + mock_has_budget.assert_called_once_with( + org_id=self.organization.id, data_category=DataCategory.SEER_SCANNER + ) @patch("sentry.quotas.backend.check_seer_quota") def test_returns_true_when_issue_type_always_triggers( @@ -214,17 +209,16 @@ def test_returns_true_when_issue_type_always_triggers( mock_has_budget, ): """Test returns True when issue type has always_trigger_seer_automation even if scanner automation is disabled.""" - with self.feature("organizations:gen-ai-features"): - # Disable scanner automation - self.project.update_option("sentry:seer_scanner_automation", False) + # Disable scanner automation + self.project.update_option("sentry:seer_scanner_automation", False) - mock_has_budget.return_value = True + mock_has_budget.return_value = True - # Mock the group's issue_type to always trigger - with patch.object(self.group.issue_type, "always_trigger_seer_automation", True): - result = is_issue_eligible_for_seer_automation(self.group) + # Mock the group's issue_type to always trigger + with patch.object(self.group.issue_type, "always_trigger_seer_automation", True): + result = is_issue_eligible_for_seer_automation(self.group) - assert result is True + assert result is True class TestIsSeerSeatBasedTierEnabled(TestCase): diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 50fb72261bd3..f6c546872e35 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -31,7 +31,6 @@ from sentry.testutils.cases import APITestCase, SnubaTestCase, TestCase from sentry.testutils.helpers.action_log import capture_action_log from sentry.testutils.helpers.datetime import before_now -from sentry.testutils.helpers.features import with_feature from sentry.testutils.skips import requires_snuba from sentry.types.activity import ActivityType from sentry.utils.cache import cache @@ -72,7 +71,6 @@ def test_post_process_kickoff_creates_system_activity( @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class IssueSummaryTest(APITestCase, SnubaTestCase, OccurrenceTestMixin): def setUp(self) -> None: super().setUp() @@ -882,7 +880,6 @@ def test_stopping_point_mapping(self, score, expected): @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True) -@with_feature({"organizations:gen-ai-features": True}) class TestRunAutomationStoppingPoint(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -950,8 +947,7 @@ def test_without_seat_based_tier( mock_seat_based_tier, ): mock_seat_based_tier.return_value = False - with self.feature({"organizations:gen-ai-features": True}): - run_automation(self.group, self.user, self.event, SeerAutomationSource.POST_PROCESS) + run_automation(self.group, self.user, self.event, SeerAutomationSource.POST_PROCESS) mock_trigger.assert_called_once() assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.CODE_CHANGES @@ -1032,7 +1028,6 @@ def test_upper_bound_combinations(self, fixability, user_pref, expected): @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True) -@with_feature({"organizations:gen-ai-features": True}) class TestRunAutomationWithUpperBound(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1107,7 +1102,7 @@ def test_fixability_limits_permissive_user_preference( assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.ROOT_CAUSE -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class TestGetAndUpdateGroupFixabilityScore(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1249,7 +1244,6 @@ def test_no_summary_in_cache_calls_seer_without_summary(self, mock_request): @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class TestIsGroupEligibleForAutomation(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1268,10 +1262,10 @@ def test_returns_true_when_all_checks_pass(self, mock_fixability, mock_quota, mo assert is_group_eligible_for_automation(self.group) is True + @override_settings(SENTRY_SELF_HOSTED=True) @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") def test_returns_false_without_seer_access(self, mock_fixability): - with self.feature({"organizations:gen-ai-features": False}): - assert is_group_eligible_for_automation(self.group) is False + assert is_group_eligible_for_automation(self.group) is False mock_fixability.assert_not_called() @@ -1339,7 +1333,7 @@ def test_returns_false_when_rate_limited(self, mock_fixability, mock_quota, mock @patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True) -@with_feature({"organizations:gen-ai-features": True}) +@override_settings(SENTRY_SELF_HOSTED=False) class TestGetAutomationStoppingPoint(TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/code_review/test_preflight.py b/tests/sentry/seer/code_review/test_preflight.py index 5b3f3905a30e..a4c734c887c9 100644 --- a/tests/sentry/seer/code_review/test_preflight.py +++ b/tests/sentry/seer/code_review/test_preflight.py @@ -47,14 +47,14 @@ def _create_service( # Legal AI consent tests # ------------------------------------------------------------------------- - def test_denied_when_gen_ai_feature_flag_disabled(self) -> None: + @override_settings(SENTRY_SELF_HOSTED=True) + def test_denied_when_self_hosted(self) -> None: service = self._create_service() result = service.check() assert result.allowed is False assert result.denial_reason == PreflightDenialReason.ORG_LEGAL_AI_CONSENT_NOT_GRANTED - @with_feature("organizations:gen-ai-features") def test_denied_when_hide_ai_features_enabled(self) -> None: self.organization.update_option("sentry:hide_ai_features", True) @@ -68,7 +68,7 @@ def test_denied_when_hide_ai_features_enabled(self) -> None: # Org feature enablement tests # ------------------------------------------------------------------------- - @with_feature(["organizations:gen-ai-features", "organizations:code-review-beta"]) + @with_feature(["organizations:code-review-beta"]) def test_denied_when_beta_org_has_no_repo_settings(self) -> None: service = self._create_service() result = service.check() @@ -76,7 +76,7 @@ def test_denied_when_beta_org_has_no_repo_settings(self) -> None: assert result.allowed is False assert result.denial_reason == PreflightDenialReason.REPO_CODE_REVIEW_DISABLED - @with_feature(["organizations:gen-ai-features", "organizations:code-review-beta"]) + @with_feature(["organizations:code-review-beta"]) def test_allowed_when_beta_org_has_repo_settings_enabled(self) -> None: self.create_repository_settings( repository=self.repo, @@ -95,7 +95,6 @@ def test_allowed_when_beta_org_has_repo_settings_enabled(self) -> None: assert result.allowed is True assert result.denial_reason is None - @with_feature("organizations:gen-ai-features") def test_denied_when_org_has_no_seer_feature_flags(self) -> None: service = self._create_service() result = service.check() @@ -107,7 +106,7 @@ def test_denied_when_org_has_no_seer_feature_flags(self) -> None: # Seer-added (legacy usage-based) org tests - not eligible for code review # ------------------------------------------------------------------------- - @with_feature(["organizations:gen-ai-features", "organizations:seer-added"]) + @with_feature(["organizations:seer-added"]) def test_denied_when_seer_added_only_org_not_eligible(self) -> None: service = self._create_service() result = service.check() @@ -115,7 +114,7 @@ def test_denied_when_seer_added_only_org_not_eligible(self) -> None: assert result.allowed is False assert result.denial_reason == PreflightDenialReason.ORG_NOT_ELIGIBLE_FOR_CODE_REVIEW - @with_feature(["organizations:gen-ai-features", "organizations:seer-added"]) + @with_feature(["organizations:seer-added"]) def test_denied_when_seer_added_only_org_even_with_repo_settings_enabled(self) -> None: self.create_repository_settings( repository=self.repo, @@ -136,7 +135,6 @@ def test_denied_when_seer_added_only_org_even_with_repo_settings_enabled(self) - @with_feature( [ - "organizations:gen-ai-features", "organizations:seer-added", "organizations:code-review-beta", ] @@ -164,7 +162,7 @@ def test_allowed_when_seer_added_and_code_review_beta_org_has_repo_settings(self # Seat-based org tests # ------------------------------------------------------------------------- - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_seat_based_org_has_no_repo_settings(self) -> None: service = self._create_service() result = service.check() @@ -172,7 +170,7 @@ def test_denied_when_seat_based_org_has_no_repo_settings(self) -> None: assert result.allowed is False assert result.denial_reason == PreflightDenialReason.REPO_CODE_REVIEW_DISABLED - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_seat_based_org_has_repo_settings_disabled(self) -> None: self.create_repository_settings( repository=self.repo, @@ -185,7 +183,7 @@ def test_denied_when_seat_based_org_has_repo_settings_disabled(self) -> None: assert result.denial_reason == PreflightDenialReason.REPO_CODE_REVIEW_DISABLED @patch("sentry.quotas.backend.check_seer_quota") - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_allowed_when_seat_based_org_has_repo_settings_enabled( self, mock_check_quota: MagicMock ) -> None: @@ -215,7 +213,7 @@ def test_allowed_when_seat_based_org_has_repo_settings_enabled( # ------------------------------------------------------------------------- @patch("sentry.quotas.backend.check_seer_quota") - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_returns_repo_settings_when_allowed(self, mock_check_quota: MagicMock) -> None: mock_check_quota.return_value = True @@ -246,7 +244,6 @@ def test_returns_repo_settings_when_allowed(self, mock_check_quota: MagicMock) - @patch("sentry.quotas.backend.check_seer_quota") @with_feature( [ - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seat-based-seer-enabled", ] @@ -282,7 +279,7 @@ def test_uses_repo_settings_when_has_both_code_review_beta_and_seat_based_featur # Billing tests # ------------------------------------------------------------------------- - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_missing_integration(self) -> None: self.create_repository_settings( repository=self.repo, @@ -305,7 +302,7 @@ def test_denied_when_missing_integration(self) -> None: "sentry.seer.code_review.preflight.instance_hostname", side_effect=InstanceHostnameError("missing"), ) - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_integration_missing_hostname( self, mock_hostname: MagicMock, mock_capture: MagicMock ) -> None: @@ -326,7 +323,7 @@ def test_denied_when_integration_missing_hostname( assert result.denial_reason == PreflightDenialReason.BILLING_MISSING_CONTRIBUTOR_INFO mock_capture.assert_called_once() - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_missing_external_identifier(self) -> None: self.create_repository_settings( repository=self.repo, @@ -344,7 +341,7 @@ def test_denied_when_missing_external_identifier(self) -> None: assert result.allowed is False assert result.denial_reason == PreflightDenialReason.BILLING_MISSING_CONTRIBUTOR_INFO - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_contributor_does_not_exist(self) -> None: self.create_repository_settings( repository=self.repo, @@ -358,7 +355,7 @@ def test_denied_when_contributor_does_not_exist(self) -> None: assert result.denial_reason == PreflightDenialReason.ORG_CONTRIBUTOR_NOT_FOUND @patch("sentry.quotas.backend.check_seer_quota") - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_quota_check_fails(self, mock_check_quota: MagicMock) -> None: mock_check_quota.return_value = False @@ -382,7 +379,6 @@ def test_denied_when_quota_check_fails(self, mock_check_quota: MagicMock) -> Non @patch("sentry.quotas.backend.check_seer_quota") @with_feature( [ - "organizations:gen-ai-features", "organizations:seat-based-seer-enabled", "organizations:code-review-beta", ] @@ -417,7 +413,7 @@ def test_checks_seats_when_both_code_review_beta_and_seat_based_features_are_ena mock_check_quota.assert_called_once() @patch("sentry.quotas.backend.check_seer_quota") - @with_feature(["organizations:gen-ai-features", "organizations:seat-based-seer-enabled"]) + @with_feature(["organizations:seat-based-seer-enabled"]) def test_denied_when_pr_author_is_excluded(self, mock_check_quota: MagicMock) -> None: mock_check_quota.return_value = True diff --git a/tests/sentry/seer/code_review/webhooks/test_merge_request.py b/tests/sentry/seer/code_review/webhooks/test_merge_request.py index ab38411ec0e5..3860b4062f89 100644 --- a/tests/sentry/seer/code_review/webhooks/test_merge_request.py +++ b/tests/sentry/seer/code_review/webhooks/test_merge_request.py @@ -77,7 +77,6 @@ class _MergeRequestHandlerTestBase(GitLabTestCase): """ CODE_REVIEW_FEATURES = { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -163,7 +162,6 @@ def _call_handler(self, event: dict[str, Any]) -> None: class MergeRequestEventWebhookTest(_MergeRequestHandlerTestBase): @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -181,7 +179,6 @@ def test_open_uses_review_request_endpoint(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -217,7 +214,7 @@ def test_validation_failure_is_captured_and_review_dropped(self) -> None: ) self.mock_seer.assert_not_called() - @with_feature({"organizations:gen-ai-features", "organizations:code-review-beta"}) + @with_feature({"organizations:code-review-beta"}) def test_skips_when_gitlab_flag_disabled(self) -> None: # The GitLab MR handler is gated on organizations:seer-gitlab-support, # independent of the other code-review flags. @@ -231,7 +228,6 @@ def test_skips_when_gitlab_flag_disabled(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -249,7 +245,6 @@ def test_close_uses_pr_closed_endpoint(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -267,7 +262,6 @@ def test_merge_uses_pr_closed_endpoint(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -285,7 +279,6 @@ def test_update_uses_review_request_endpoint(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -302,7 +295,6 @@ def test_update_without_oldrev_is_skipped(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -320,7 +312,6 @@ def test_update_with_unrelated_changes_is_skipped(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -342,7 +333,6 @@ def test_undraft_update_uses_review_request_endpoint(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -361,7 +351,6 @@ def test_undraft_update_via_work_in_progress_uses_review_request_endpoint(self) @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -379,7 +368,6 @@ def test_undraft_update_trigger_is_ready_for_review(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -397,7 +385,6 @@ def test_undraft_update_filtered_when_ready_trigger_disabled(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -413,7 +400,6 @@ def test_skips_draft_mr(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -429,7 +415,6 @@ def test_skips_work_in_progress_mr(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -445,7 +430,6 @@ def test_close_still_sends_for_draft_mr(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -461,7 +445,6 @@ def test_skips_unsupported_action(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -477,7 +460,6 @@ def test_skips_unknown_action(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -494,7 +476,6 @@ def test_skips_missing_action(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -524,7 +505,6 @@ def test_skips_when_code_review_not_enabled(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -541,7 +521,6 @@ def test_skips_missing_last_commit(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -557,7 +536,6 @@ def test_open_filtered_when_trigger_disabled(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -573,7 +551,6 @@ def test_update_filtered_when_trigger_disabled(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -589,7 +566,6 @@ def test_close_filtered_when_no_triggers_configured(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -605,7 +581,6 @@ def test_close_sends_when_triggers_configured(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -624,7 +599,6 @@ def test_payload_contains_correct_pr_id(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -643,7 +617,6 @@ def test_payload_contains_gitlab_provider(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -664,7 +637,6 @@ def test_payload_owner_and_name_use_path_not_display_name(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -683,7 +655,6 @@ def test_payload_owner_and_name_handle_subgroups(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -702,7 +673,6 @@ def test_payload_is_private_true_for_private_project(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -721,7 +691,6 @@ def test_payload_is_private_true_for_internal_project(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -740,7 +709,6 @@ def test_payload_is_private_false_for_public_project(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -759,7 +727,6 @@ def test_payload_is_private_none_when_visibility_absent(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -778,7 +745,6 @@ def test_payload_trigger_on_ready_for_review_for_open(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -797,7 +763,6 @@ def test_payload_trigger_on_new_commit_for_update(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -816,7 +781,6 @@ def test_payload_contains_trigger_user_from_event(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -847,7 +811,6 @@ def test_open_with_gitlab_space_utc_timestamp_enqueues(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -871,7 +834,6 @@ def test_open_with_iso8601_timestamp_still_enqueues(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -899,7 +861,6 @@ def test_open_with_unparseable_timestamp_captures_and_falls_back(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -916,7 +877,6 @@ def test_duplicate_delivery_within_window_skipped(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -944,7 +904,6 @@ def test_duplicate_delivery_after_ttl_processes_again(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1039,7 +998,6 @@ def _call_handler(self, event: dict[str, Any]) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1058,7 +1016,6 @@ def test_sentry_review_comment_schedules_seer_task(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1076,7 +1033,6 @@ def test_payload_trigger_is_on_command_phrase(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1095,7 +1051,6 @@ def test_payload_contains_trigger_comment_id_and_type(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1113,7 +1068,6 @@ def test_payload_trigger_user_is_commenter(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1134,7 +1088,6 @@ def test_eyes_reaction_added_to_note(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1151,7 +1104,6 @@ def test_non_review_command_is_ignored(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1168,7 +1120,6 @@ def test_issue_note_is_ignored(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1185,7 +1136,6 @@ def test_non_create_action_is_ignored(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1202,7 +1152,6 @@ def test_sentry_review_case_insensitive(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1230,7 +1179,6 @@ def test_skips_when_feature_flag_disabled(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1255,7 +1203,6 @@ def test_skips_when_integration_is_none(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1273,7 +1220,6 @@ def test_reaction_failure_does_not_block_seer_task(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1320,7 +1266,6 @@ class MergeRequestReactionTest(_MergeRequestHandlerTestBase): @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1338,7 +1283,6 @@ def test_eyes_reaction_added_for_open_action(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1365,7 +1309,6 @@ def test_stale_hooray_reaction_deleted_before_eyes_added(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1391,7 +1334,6 @@ def test_other_users_hooray_reaction_not_deleted(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1410,7 +1352,6 @@ def test_no_reaction_for_close_action(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1429,7 +1370,6 @@ def test_no_reaction_for_merge_action(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } @@ -1449,7 +1389,6 @@ def test_reaction_failure_does_not_block_seer_task(self) -> None: @with_feature( { - "organizations:gen-ai-features", "organizations:code-review-beta", "organizations:seer-gitlab-support", } diff --git a/tests/sentry/seer/code_review/webhooks/test_pull_request.py b/tests/sentry/seer/code_review/webhooks/test_pull_request.py index 096f5aaf88b5..4ccc05f60cc4 100644 --- a/tests/sentry/seer/code_review/webhooks/test_pull_request.py +++ b/tests/sentry/seer/code_review/webhooks/test_pull_request.py @@ -248,7 +248,7 @@ def test_pull_request_closed_uses_pr_closed_endpoint(self) -> None: def test_pull_request_opened_filtered_when_trigger_disabled_post_ga(self) -> None: triggers = [CodeReviewTrigger.ON_NEW_COMMIT] - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with ( self.code_review_setup(triggers=triggers, features=features), self.tasks(), @@ -263,7 +263,7 @@ def test_pull_request_opened_filtered_when_trigger_disabled_post_ga(self) -> Non def test_pull_request_synchronize_filtered_when_trigger_disabled_post_ga(self) -> None: triggers = [CodeReviewTrigger.ON_READY_FOR_REVIEW] - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with ( self.code_review_setup(triggers=triggers, features=features), self.tasks(), @@ -278,7 +278,7 @@ def test_pull_request_synchronize_filtered_when_trigger_disabled_post_ga(self) - def test_pull_request_ready_for_review_filtered_when_trigger_disabled_post_ga(self) -> None: triggers = [CodeReviewTrigger.ON_NEW_COMMIT] - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with ( self.code_review_setup(triggers=triggers, features=features), self.tasks(), @@ -301,7 +301,7 @@ def test_pull_request_closed_filtered_when_no_triggers_configured_post_ga(self) helper skips RepositorySettings creation when triggers=[], which would cause the preflight to deny the request before reaching the handler under test. """ - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with self.feature(features), self.tasks(): event = orjson.loads(PULL_REQUEST_OPENED_EVENT_EXAMPLE) event["action"] = "closed" @@ -320,7 +320,7 @@ def test_pull_request_closed_filtered_when_no_triggers_configured_post_ga(self) def test_pull_request_closed_not_filtered_when_triggers_configured_post_ga(self) -> None: """Test that closed action reaches Seer when at least one trigger is configured.""" triggers: list[CodeReviewTrigger] = [CodeReviewTrigger.ON_READY_FOR_REVIEW] - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with ( self.code_review_setup(triggers=triggers, features=features), self.tasks(), @@ -335,7 +335,7 @@ def test_pull_request_closed_not_filtered_when_triggers_configured_post_ga(self) def test_pull_request_opened_works_when_trigger_enabled_post_ga(self) -> None: triggers = [CodeReviewTrigger.ON_READY_FOR_REVIEW] - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with ( self.code_review_setup(triggers=triggers, features=features), self.tasks(), @@ -350,7 +350,7 @@ def test_pull_request_opened_works_when_trigger_enabled_post_ga(self) -> None: def test_pull_request_ready_for_review_works_when_trigger_enabled_post_ga(self) -> None: triggers = [CodeReviewTrigger.ON_READY_FOR_REVIEW] - features = {"organizations:gen-ai-features", "organizations:seat-based-seer-enabled"} + features = {"organizations:seat-based-seer-enabled"} with ( self.code_review_setup(triggers=triggers, features=features), self.tasks(), diff --git a/tests/sentry/seer/code_review/webhooks/test_review_request.py b/tests/sentry/seer/code_review/webhooks/test_review_request.py index a8f707d3ee3b..a22eba47a5f6 100644 --- a/tests/sentry/seer/code_review/webhooks/test_review_request.py +++ b/tests/sentry/seer/code_review/webhooks/test_review_request.py @@ -15,7 +15,7 @@ from sentry.testutils.cases import TestCase from sentry.testutils.helpers.features import with_feature -CODE_REVIEW_FEATURES = {"organizations:gen-ai-features", "organizations:code-review-beta"} +CODE_REVIEW_FEATURES = {"organizations:code-review-beta"} def _event(**overrides: object) -> PullRequestReviewEvent: diff --git a/tests/sentry/seer/endpoints/test_group_ai_autofix.py b/tests/sentry/seer/endpoints/test_group_ai_autofix.py index e59e63660e1b..7eae91bc703e 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/seer/endpoints/test_group_ai_autofix.py @@ -60,7 +60,6 @@ def _user_context_length_calls(mock_distribution: Mock) -> list: @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class GroupAutofixEndpointTest(APITestCase, SnubaTestCase): def _get_url(self, group_id: int) -> str: return f"/api/0/organizations/{self.organization.slug}/issues/{group_id}/autofix/" @@ -1486,7 +1485,7 @@ def test_open_pr_coding_disabled(self): assert response.status_code == 403, response.data -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class GroupAutofixConditionalGetTest(APITestCase): def _get_url(self, group_id: int) -> str: return f"/api/0/organizations/{self.organization.slug}/issues/{group_id}/autofix/" diff --git a/tests/sentry/seer/endpoints/test_group_ai_summary.py b/tests/sentry/seer/endpoints/test_group_ai_summary.py index 7dcb07ba1862..2f7cded4e72d 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_summary.py +++ b/tests/sentry/seer/endpoints/test_group_ai_summary.py @@ -1,14 +1,15 @@ from unittest.mock import ANY, MagicMock, patch +from django.test import override_settings + from sentry.seer.autofix.constants import SeerAutomationSource from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers.features import with_feature from sentry.testutils.skips import requires_snuba pytestmark = [requires_snuba] -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class GroupAiSummaryEndpointTest(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/endpoints/test_group_autofix_repos.py b/tests/sentry/seer/endpoints/test_group_autofix_repos.py index 676f9ddafe66..699aa466f553 100644 --- a/tests/sentry/seer/endpoints/test_group_autofix_repos.py +++ b/tests/sentry/seer/endpoints/test_group_autofix_repos.py @@ -3,11 +3,9 @@ from django.test import override_settings from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers.features import with_feature @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class GroupAutofixReposEndpointTest(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/endpoints/test_group_autofix_setup_check.py b/tests/sentry/seer/endpoints/test_group_autofix_setup_check.py index 83d4accd8cfc..0a5667e0f480 100644 --- a/tests/sentry/seer/endpoints/test_group_autofix_setup_check.py +++ b/tests/sentry/seer/endpoints/test_group_autofix_setup_check.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +from django.test import override_settings + from sentry.integrations.types import IntegrationProviderSlug from sentry.models.repository import Repository from sentry.seer.autofix.constants import AutofixAutomationTuningSettings @@ -8,7 +10,6 @@ ) from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, SnubaTestCase, TestCase -from sentry.testutils.helpers.features import with_feature from sentry.testutils.silo import assume_test_silo_mode from sentry.utils.cache import cache @@ -92,7 +93,7 @@ def test_unsupported_gitlab_integration(self) -> None: assert result == "integration_missing" -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class GroupAIAutofixEndpointSuccessTest(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -274,7 +275,7 @@ def test_missing_integration(self) -> None: assert response.data["seerReposLinked"] is False -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class GroupAIAutofixSetupFreeCohortTest(APITestCase, SnubaTestCase): """Tests for free cohort org behavior in the /autofix/setup/ endpoint.""" diff --git a/tests/sentry/seer/endpoints/test_organization_agent_token.py b/tests/sentry/seer/endpoints/test_organization_agent_token.py index 0c0f43cc2a9e..06a5c9f47ef4 100644 --- a/tests/sentry/seer/endpoints/test_organization_agent_token.py +++ b/tests/sentry/seer/endpoints/test_organization_agent_token.py @@ -770,7 +770,7 @@ def test_end_to_end_read_allowed_write_denied(self) -> None: @pytest.mark.sentry_metrics @pytest.mark.seer_agent_token_matrix @requires_snuba -@override_settings(SEER_API_SHARED_SECRET=SECRET) +@override_settings(SEER_API_SHARED_SECRET=SECRET, SENTRY_SELF_HOSTED=False) class AgentTokenPublicGetMatrixTest(APITestCase): """Differential, full-stack authentication coverage for the public API. @@ -793,6 +793,11 @@ class AgentTokenPublicGetMatrixTest(APITestCase): def setUp(self) -> None: super().setUp() + rate_limit_patcher = patch( + "sentry.middleware.ratelimit.get_rate_limit_value", return_value=None + ) + rate_limit_patcher.start() + self.addCleanup(rate_limit_patcher.stop) self.owner = self.create_user() self.org = self.create_organization(owner=self.owner) self.team = self.create_team(organization=self.org) @@ -1303,7 +1308,6 @@ def _feature_flags( "ExternalUserDetailsEndpoint": "organizations:integrations-codeowners", "ExternalUserEndpoint": "organizations:integrations-codeowners", "EventAttachmentDetailsEndpoint": "organizations:event-attachments", - "GroupAutofixEndpoint": "organizations:gen-ai-features", "GroupIntegrationDetailsEndpoint": "organizations:integrations-issue-basic", "OrganizationEventsEndpoint": "organizations:discover-basic", "OrganizationGroupSearchViewsEndpoint": "organizations:issue-views", diff --git a/tests/sentry/seer/endpoints/test_organization_seer_agent_chat.py b/tests/sentry/seer/endpoints/test_organization_seer_agent_chat.py index 418845ae756a..cb2cfd145219 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_agent_chat.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_agent_chat.py @@ -22,7 +22,6 @@ @with_feature("organizations:seer-explorer") @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class OrganizationSeerAgentChatEndpointTest(APITestCase): def setUp(self) -> None: super().setUp() @@ -550,14 +549,10 @@ def test_new_run_denied_without_seer_explorer_flag(self) -> None: assert response.status_code == 403 + @override_settings(SENTRY_SELF_HOSTED=True) def test_get_denied_without_seer_access(self) -> None: """GET should be denied when the org has neither seer-explorer nor base Seer access.""" - with self.feature( - { - "organizations:seer-explorer": False, - "organizations:gen-ai-features": False, - } - ): + with self.feature({"organizations:seer-explorer": False}): response = self.client.get(self.url) assert response.status_code == 403 @@ -674,7 +669,6 @@ def test_outbox_path_flush_error_marks_failed_and_raises( @with_feature("organizations:seer-explorer") @override_settings(SENTRY_SELF_HOSTED=False) -@with_feature("organizations:gen-ai-features") class OrganizationSeerAgentChatContextEngineTest(APITestCase): """End-to-end tests verifying is_context_engine_enabled reaches make_agent_chat_request.""" diff --git a/tests/sentry/seer/endpoints/test_organization_seer_agent_update.py b/tests/sentry/seer/endpoints/test_organization_seer_agent_update.py index e4000c2b807a..8e1ec1e3fe1b 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_agent_update.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_agent_update.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch import orjson +from django.test import override_settings from rest_framework import status from sentry.integrations.types import ExternalProviders @@ -11,7 +12,7 @@ @with_feature("organizations:seer-explorer") -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class TestOrganizationSeerAgentUpdate(APITestCase): def setUp(self) -> None: super().setUp() @@ -257,7 +258,7 @@ def test_explorer_update_feature_flag_disabled(self, mock_has_access: MagicMock) @with_feature("organizations:seer-explorer") -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class TestOrganizationSeerAgentUpdateCodingDisabled(APITestCase): def setUp(self) -> None: super().setUp() @@ -305,7 +306,7 @@ def test_non_coding_payload_allowed_when_coding_disabled( @with_feature("organizations:seer-explorer") -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class TestOrganizationSeerAgentUpdateCommitAuthor(APITestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/endpoints/test_organization_seer_runs.py b/tests/sentry/seer/endpoints/test_organization_seer_runs.py index 0cf6d6f786be..32edd025a851 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_runs.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_runs.py @@ -13,7 +13,6 @@ @override_settings(SENTRY_SELF_HOSTED=False) @with_feature("organizations:seer-explorer") -@with_feature("organizations:gen-ai-features") class OrganizationSeerRunsEndpointTest(APITestCase): endpoint = "sentry-api-0-organization-seer-runs" diff --git a/tests/sentry/seer/endpoints/test_organization_seer_workflows.py b/tests/sentry/seer/endpoints/test_organization_seer_workflows.py index 3f60676e0d01..5c22be112150 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_workflows.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_workflows.py @@ -431,15 +431,14 @@ def test_history_hides_runs_outside_user_access(self) -> None: def create_agent_workflow( self, strategy: SeerWorkflowStrategy, feature_id: str ) -> SeerWorkflowRun: - with self.feature("organizations:gen-ai-features"): - return create_workflow_run( - SeerAgentClient(self.organization, self.user), - strategy=strategy, - feature_id=feature_id, - title="Test workflow", - payload={}, - extras={"project_ids": [], "results": []}, - ) + return create_workflow_run( + SeerAgentClient(self.organization, self.user), + strategy=strategy, + feature_id=feature_id, + title="Test workflow", + payload={}, + extras={"project_ids": [], "results": []}, + ) @override_settings(SENTRY_SELF_HOSTED=False) @@ -525,11 +524,9 @@ def test_requires_feature_and_seer_access(self) -> None: self.get_error_response( self.organization.slug, strategy="duplicate_monitors", status_code=404 ) - with self.feature( - { - "organizations:seer-workflows-monitor-cleanup": True, - "organizations:gen-ai-features": False, - } + with ( + override_settings(SENTRY_SELF_HOSTED=True), + self.feature("organizations:seer-workflows-monitor-cleanup"), ): response = self.get_error_response( self.organization.slug, strategy="duplicate_monitors", status_code=403 @@ -538,9 +535,7 @@ def test_requires_feature_and_seer_access(self) -> None: limit.assert_not_called() limit.return_value = True - with self.feature( - ["organizations:seer-workflows-monitor-cleanup", "organizations:gen-ai-features"] - ): + with self.feature("organizations:seer-workflows-monitor-cleanup"): self.get_error_response( self.organization.slug, strategy="duplicate_monitors", status_code=429 ) @@ -564,9 +559,7 @@ def test_callback_for_deleted_user_marks_run_failed(self) -> None: assert agent_run.extras["error"] == "The triggering user no longer exists." def trigger(self): - with self.feature( - ["organizations:seer-workflows-monitor-cleanup", "organizations:gen-ai-features"] - ): + with self.feature("organizations:seer-workflows-monitor-cleanup"): response = self.get_success_response( self.organization.slug, strategy="duplicate_monitors", status_code=202 ) diff --git a/tests/sentry/seer/endpoints/test_search_agent_start.py b/tests/sentry/seer/endpoints/test_search_agent_start.py index f2037a321016..1c06fb5f2ce7 100644 --- a/tests/sentry/seer/endpoints/test_search_agent_start.py +++ b/tests/sentry/seer/endpoints/test_search_agent_start.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest +from django.test import override_settings from rest_framework import status from sentry.seer.endpoints.search_agent_start import send_search_agent_start_request @@ -105,7 +106,7 @@ def test_flag_options_are_sent_to_seer(self, mock_request: Mock) -> None: @with_feature("organizations:gen-ai-search-agent-translate") -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class SearchAgentStartEndpointTest(APITestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/endpoints/test_search_agent_state.py b/tests/sentry/seer/endpoints/test_search_agent_state.py index 55b378f0dffd..e35bc823f55f 100644 --- a/tests/sentry/seer/endpoints/test_search_agent_state.py +++ b/tests/sentry/seer/endpoints/test_search_agent_state.py @@ -6,8 +6,7 @@ from sentry.testutils.cases import APITestCase -@override_settings(SENTRY_SELF_HOSTED=False) -@override_settings(SEER_AUTOFIX_URL="https://seer.example.com") +@override_settings(SEER_AUTOFIX_URL="https://seer.example.com", SENTRY_SELF_HOSTED=False) class SearchAgentStateEndpointTest(APITestCase): endpoint = "sentry-api-0-search-agent-state" @@ -16,7 +15,6 @@ def setUp(self) -> None: self.login_as(self.user) self.features = { "organizations:gen-ai-search-agent-translate": True, - "organizations:gen-ai-features": True, } @patch("sentry.seer.endpoints.search_agent_state.make_search_agent_state_request") diff --git a/tests/sentry/seer/endpoints/test_trace_explorer_ai_query.py b/tests/sentry/seer/endpoints/test_trace_explorer_ai_query.py index f860fbeac320..c0a669d72ae9 100644 --- a/tests/sentry/seer/endpoints/test_trace_explorer_ai_query.py +++ b/tests/sentry/seer/endpoints/test_trace_explorer_ai_query.py @@ -1,12 +1,12 @@ from unittest.mock import patch +from django.test import override_settings from rest_framework import status from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.features import with_feature -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class TraceExplorerAIQueryTest(APITestCase): def setUp(self) -> None: super().setUp() @@ -58,6 +58,20 @@ def test_query_successful(self, mock_send_request) -> None: }, ) + @override_settings(SENTRY_SELF_HOSTED=True) + def test_denied_on_self_hosted(self) -> None: + response = self.client.post( + self.url, + data={ + "project_ids": [self.project.id], + "natural_language_query": "Find slow transactions", + }, + format="json", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.data == {"detail": "Organization does not have access to this feature"} + def test_query_missing_parameters(self) -> None: response = self.client.post( self.url, diff --git a/tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py b/tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py index 6fddfd355b86..29a2b4fb4478 100644 --- a/tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py +++ b/tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py @@ -1,7 +1,8 @@ from unittest.mock import patch +from django.test import override_settings + from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.features import with_feature class TraceExplorerAISetupTest(APITestCase): @@ -9,7 +10,7 @@ class TraceExplorerAISetupTest(APITestCase): method = "post" - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.endpoints.trace_explorer_ai_setup.fire_setup_request") def test_simple(self, mock_fire_setup_request): self.login_as(self.user) @@ -30,7 +31,7 @@ def test_simple(self, mock_fire_setup_request): }, ) - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.endpoints.trace_explorer_ai_setup.fire_setup_request") def test_rejects_project_from_other_org(self, mock_fire_setup_request): """Test that requesting projects from another org returns 403""" @@ -48,7 +49,7 @@ def test_rejects_project_from_other_org(self, mock_fire_setup_request): assert response.data == {"detail": "You do not have permission to perform this action."} mock_fire_setup_request.assert_not_called() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.endpoints.trace_explorer_ai_setup.fire_setup_request") def test_rejects_nonexistent_project(self, mock_fire_setup_request): """Test that requesting non-existent project returns same error as inaccessible project""" @@ -63,7 +64,7 @@ def test_rejects_nonexistent_project(self, mock_fire_setup_request): assert response.data == {"detail": "You do not have permission to perform this action."} mock_fire_setup_request.assert_not_called() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.endpoints.trace_explorer_ai_setup.fire_setup_request") def test_empty_projects_still_calls_seer(self, mock_fire_setup_request): """Test that empty project list is handled""" @@ -85,7 +86,8 @@ def test_empty_projects_still_calls_seer(self, mock_fire_setup_request): }, ) - def test_requires_feature_flag(self) -> None: + @override_settings(SENTRY_SELF_HOSTED=True) + def test_denied_on_self_hosted(self) -> None: self.login_as(self.user) response = self.get_error_response( @@ -96,7 +98,7 @@ def test_requires_feature_flag(self) -> None: assert response.data == {"detail": "Organization does not have access to this feature"} - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.endpoints.trace_explorer_ai_setup.fire_setup_request") def test_invalid_project_id_returns_400(self, mock_fire_setup_request): """Test that non-integer project_id returns 400""" @@ -111,7 +113,7 @@ def test_invalid_project_id_returns_400(self, mock_fire_setup_request): assert response.data["detail"] == "Invalid project_id value" mock_fire_setup_request.assert_not_called() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.seer.endpoints.trace_explorer_ai_setup.fire_setup_request") def test_negative_project_id_returns_400(self, mock_fire_setup_request): """Test that negative project_id (like -1 sentinel) returns 400""" @@ -126,7 +128,7 @@ def test_negative_project_id_returns_400(self, mock_fire_setup_request): assert response.data["detail"] == "Invalid project_id value" mock_fire_setup_request.assert_not_called() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_requires_authentication(self) -> None: response = self.get_error_response( self.organization.slug, diff --git a/tests/sentry/seer/endpoints/test_trace_explorer_ai_translate_agentic.py b/tests/sentry/seer/endpoints/test_trace_explorer_ai_translate_agentic.py index 1de4c0b9559a..a156100a5365 100644 --- a/tests/sentry/seer/endpoints/test_trace_explorer_ai_translate_agentic.py +++ b/tests/sentry/seer/endpoints/test_trace_explorer_ai_translate_agentic.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock, patch +from django.test import override_settings from rest_framework import status from sentry.testutils.cases import APITestCase @@ -7,7 +8,7 @@ @with_feature("organizations:seer-explorer") -@with_feature("organizations:gen-ai-features") +@override_settings(SENTRY_SELF_HOSTED=False) class SearchAgentTranslateEndpointTest(APITestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/entrypoints/slack/test_tasks.py b/tests/sentry/seer/entrypoints/slack/test_tasks.py index fd3a5ec2bc9c..fbe44e81e576 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, } @@ -931,13 +930,13 @@ def test_unlinked_identity_records_halt( @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @patch("sentry.analytics.record") + @override_settings(SENTRY_SELF_HOSTED=True) def test_no_agent_access_records_halt( self, mock_record, mock_lifecycle_record, ): - with self.feature({"organizations:gen-ai-features": False}): - process_reaction_for_slack(**self.defaults) + process_reaction_for_slack(**self.defaults) assert_not_analytics_event(mock_record, SlackSeerAgentFeedback) assert_halt_metric(mock_lifecycle_record, ProcessReactionHaltReason.NO_AGENT_ACCESS) diff --git a/tests/sentry/seer/entrypoints/test_operator.py b/tests/sentry/seer/entrypoints/test_operator.py index 5c68f8dd34b0..bfaf73b4c29b 100644 --- a/tests/sentry/seer/entrypoints/test_operator.py +++ b/tests/sentry/seer/entrypoints/test_operator.py @@ -514,37 +514,23 @@ def test_process_autofix_updates_skips_entrypoint_without_access( ) def test_can_trigger_autofix_returns_false_without_seer_access(self) -> None: + self.organization.update_option("sentry:hide_ai_features", True) assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False @patch("sentry.quotas.backend.check_seer_quota", return_value=True) def test_can_trigger_autofix_returns_true_when_all_conditions_met(self, mock_quota): - with self.feature( - { - "organizations:gen-ai-features": True, - } - ): - assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is True + assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is True @patch("sentry.quotas.backend.check_seer_quota", return_value=True) def test_can_trigger_autofix_returns_false_for_ineligible_category(self, mock_quota): from sentry.issues.grouptype import FeedbackGroup feedback_group = self.create_group(project=self.project, type=FeedbackGroup.type_id) - with self.feature( - { - "organizations:gen-ai-features": True, - } - ): - assert SeerAutofixOperator.can_trigger_autofix(group=feedback_group) is False + assert SeerAutofixOperator.can_trigger_autofix(group=feedback_group) is False @patch("sentry.quotas.backend.check_seer_quota", return_value=False) def test_can_trigger_autofix_returns_false_without_quota(self, mock_quota): - with self.feature( - { - "organizations:gen-ai-features": True, - } - ): - assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False + assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False @patch.object(SeerAutofixOperator, "has_access", return_value=True) def test_seer_event_creates_activity_rca_completed(self, _mock_has_access): @@ -956,7 +942,6 @@ def test_has_access_with_seer_agent(self): with ( self.feature( { - "organizations:gen-ai-features": True, "organizations:seer-explorer": True, } ), @@ -979,9 +964,9 @@ def test_has_access_with_seer_agent(self): entrypoint_key=MockNoAccessEntrypoint.key, ) + @override_settings(SENTRY_SELF_HOSTED=True) def test_has_access_without_seer_agent(self): - with self.feature({"organizations:gen-ai-features": False}): - assert not SeerAgentOperator.has_access(organization=self.organization) + assert not SeerAgentOperator.has_access(organization=self.organization) class TestSeerOperatorCompletionHook(TestCase): diff --git a/tests/sentry/seer/test_seer_setup.py b/tests/sentry/seer/test_seer_setup.py index 710e69e2ae40..947f10ed56c5 100644 --- a/tests/sentry/seer/test_seer_setup.py +++ b/tests/sentry/seer/test_seer_setup.py @@ -2,23 +2,15 @@ from sentry.seer.seer_setup import has_seer_access, has_seer_access_with_detail from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.features import with_feature @override_settings(SENTRY_SELF_HOSTED=False) class HasSeerAccessTest(TestCase): - @with_feature("organizations:gen-ai-features") def test_allowed(self) -> None: org = self.create_organization() assert has_seer_access(org) is True assert has_seer_access_with_detail(org) == (True, None) - def test_denied_without_flag(self) -> None: - org = self.create_organization() - assert has_seer_access(org) is False - assert has_seer_access_with_detail(org) == (False, "Feature flag not enabled") - - @with_feature("organizations:gen-ai-features") def test_denied_when_hidden(self) -> None: org = self.create_organization() org.update_option("sentry:hide_ai_features", True) @@ -28,7 +20,6 @@ def test_denied_when_hidden(self) -> None: "AI features are disabled for this organization.", ) - @with_feature("organizations:gen-ai-features") @override_settings(SENTRY_SELF_HOSTED=True) def test_denied_on_self_hosted(self) -> None: org = self.create_organization() diff --git a/tests/sentry/seer/workflows/test_runs.py b/tests/sentry/seer/workflows/test_runs.py index 961482ef0963..d88ff7e90921 100644 --- a/tests/sentry/seer/workflows/test_runs.py +++ b/tests/sentry/seer/workflows/test_runs.py @@ -156,15 +156,14 @@ def test_dispatch_failure_is_reported_without_overwriting_a_completed_result(sel assert status["error"] is None def create_run(self) -> SeerRun: - with self.feature("organizations:gen-ai-features"): - workflow = create_workflow_run( - SeerAgentClient(self.organization, self.user), - strategy=SeerWorkflowStrategy.AGENTIC_TRIAGE, - feature_id="test_workflow", - title="Test workflow", - payload={}, - extras={"summary": None}, - ) + workflow = create_workflow_run( + SeerAgentClient(self.organization, self.user), + strategy=SeerWorkflowStrategy.AGENTIC_TRIAGE, + feature_id="test_workflow", + title="Test workflow", + payload={}, + extras={"summary": None}, + ) run = workflow.executions.get().seer_run assert run is not None return run diff --git a/tests/sentry/tasks/seer/test_agentic_triage.py b/tests/sentry/tasks/seer/test_agentic_triage.py index fe68a8629e9f..2d015d7e1b07 100644 --- a/tests/sentry/tasks/seer/test_agentic_triage.py +++ b/tests/sentry/tasks/seer/test_agentic_triage.py @@ -35,6 +35,7 @@ _complete_run, _current_schedule_id, _dispatch_pending_shards, + _get_eligible_orgs_from_batch, _get_eligible_projects, _record_run_error, _update_run_extras, @@ -87,7 +88,6 @@ def test_code_mode_flag_applies_to_every_dispatched_shard(default_organization, with with_feature( { - "organizations:gen-ai-features": True, "organizations:seer-night-shift-code-mode": enabled, } ): @@ -113,7 +113,6 @@ def test_redispatch_preserves_recorded_code_mode_after_flag_is_disabled(default_ with with_feature( { - "organizations:gen-ai-features": True, "organizations:seer-night-shift-code-mode": True, } ): @@ -125,7 +124,6 @@ def test_redispatch_preserves_recorded_code_mode_after_flag_is_disabled(default_ with with_feature( { - "organizations:gen-ai-features": True, "organizations:seer-night-shift-code-mode": False, } ): @@ -294,6 +292,7 @@ def test_uses_configured_agentic_triage_schedule(self) -> None: @django_db_all +@override_settings(SENTRY_SELF_HOSTED=False) class TestScheduleAgenticTriage(TestCase): def create_org_with_seer(self): """Create an org with a SeerProjectRepository so it survives the pre-filter.""" @@ -303,6 +302,18 @@ def create_org_with_seer(self): self.create_seer_project_repository(project=project, repository=repo) return org + def test_eligible_orgs_empty_on_self_hosted(self) -> None: + org = self.create_organization() + with self.feature( + { + "organizations:seer-night-shift": [org.slug], + "organizations:seat-based-seer-enabled": [org.slug], + } + ): + assert _get_eligible_orgs_from_batch([org]) == [org] + with override_settings(SENTRY_SELF_HOSTED=True): + assert _get_eligible_orgs_from_batch([org]) == [] + def test_disabled_by_option(self) -> None: with ( self.options({"seer.night_shift.enable": False}), @@ -322,7 +333,6 @@ def test_dispatches_eligible_orgs(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ), @@ -354,7 +364,6 @@ def test_dispatches_with_run_options(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ), @@ -380,7 +389,6 @@ def test_redelivery_dispatches_same_schedule_id(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ), @@ -409,7 +417,6 @@ def test_skips_orgs_without_seat_based_seer(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], # seat-based-seer-enabled intentionally omitted } ), @@ -433,7 +440,6 @@ def test_dispatches_legacy_orgs_when_enabled(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], # seat-based-seer-enabled intentionally omitted } ), @@ -454,7 +460,6 @@ def test_skips_orgs_with_hidden_ai(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ), @@ -474,7 +479,6 @@ def test_skips_orgs_with_code_generation_disabled(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ), @@ -495,7 +499,6 @@ def test_skips_orgs_without_seer_project_repository(self) -> None: self.feature( { "organizations:seer-night-shift": [org.slug], - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ), @@ -873,8 +876,7 @@ def test_filters_recently_skipped_groups(self) -> None: mark_skipped(skipped_group.id) try: - with self.feature("organizations:gen-ai-features"): - run_agentic_triage_for_org(org.id) + run_agentic_triage_for_org(org.id) finally: redis_clusters.get("default").delete(skip_cache_key(skipped_group.id)) @@ -1057,7 +1059,6 @@ def test_chunking_preserves_order_across_even_shards(self) -> None: with ( self.options({"seer.night_shift.shard_size": 2}), - self.feature("organizations:gen-ai-features"), patch( "sentry.tasks.seer.agentic_triage.cron.fixability_score_strategy", return_value=scored, @@ -1082,7 +1083,6 @@ def test_chunking_single_shard_when_size_exceeds_count(self) -> None: with ( self.options({"seer.night_shift.shard_size": 10}), - self.feature("organizations:gen-ai-features"), patch( "sentry.tasks.seer.agentic_triage.cron.fixability_score_strategy", return_value=scored, @@ -1104,7 +1104,6 @@ def test_non_positive_shard_size_clamps_to_one(self) -> None: with ( self.options({"seer.night_shift.shard_size": 0}), - self.feature("organizations:gen-ai-features"), patch( "sentry.tasks.seer.agentic_triage.cron.fixability_score_strategy", return_value=scored, @@ -1125,10 +1124,7 @@ def test_dispatches_candidates_to_seer_feature(self) -> None: project, "fixable", seer_fixability_score=0.9, times_seen=5, priority=75 ) - with ( - self.feature("organizations:gen-ai-features"), - patch("sentry.seer.agentic_triage.delivery.trigger_autofix_agent") as mock_autofix, - ): + with patch("sentry.seer.agentic_triage.delivery.trigger_autofix_agent") as mock_autofix: run_agentic_triage_for_org(org.id) # Autofix is fired by Seer's pushed-back verdicts, not in-process. @@ -1166,8 +1162,7 @@ def test_payload_carries_automation_tuning_for_legacy_orgs(self) -> None: ) self._store_event_and_update_group(project, "fixable", seer_fixability_score=0.9) - with self.feature("organizations:gen-ai-features"): - run_agentic_triage_for_org(org.id) + run_agentic_triage_for_org(org.id) _, body = _dispatched_feature_body(org) assert body["payload"]["candidates"][0]["automation_tuning"] == "high" @@ -1179,7 +1174,6 @@ def test_payload_omits_automation_tuning_for_seat_based_orgs(self) -> None: self._store_event_and_update_group(project, "fixable", seer_fixability_score=0.9) with ( - self.feature("organizations:gen-ai-features"), patch( "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_based_tier_enabled", return_value=True, @@ -1205,8 +1199,7 @@ def test_payload_carries_per_project_automation_tuning_within_one_org(self) -> N always, "always-fixable", seer_fixability_score=0.9 ) - with self.feature("organizations:gen-ai-features"): - run_agentic_triage_for_org(org.id) + run_agentic_triage_for_org(org.id) _, body = _dispatched_feature_body(org) tuning_by_group_id = { @@ -1229,7 +1222,6 @@ def test_allowed_project_slugs_gives_each_project_its_own_quota(self) -> None: ) with ( - self.feature("organizations:gen-ai-features"), self.options( { "seer.night_shift.org_tweaks": { @@ -1265,10 +1257,7 @@ def test_shards_candidates_across_feature_runs(self) -> None: for i in range(3) ] - with ( - self.options({"seer.night_shift.shard_size": 2}), - self.feature("organizations:gen-ai-features"), - ): + with self.options({"seer.night_shift.shard_size": 2}): run_agentic_triage_for_org(org.id) run = SeerWorkflowRun.objects.get(organization=org) @@ -1315,7 +1304,6 @@ def fail_second_dispatch(client, *args, **kwargs): with ( self.options({"seer.night_shift.shard_size": 1}), - self.feature("organizations:gen-ai-features"), patch( "sentry.tasks.seer.agentic_triage.cron.fixability_score_strategy", return_value=scored, @@ -1369,6 +1357,7 @@ def test_no_candidates_skips_dispatch(self) -> None: def test_no_seer_access_keeps_shard_plan_for_resume(self) -> None: org = self.create_organization() + org.update_option("sentry:hide_ai_features", True) project = self.create_project(organization=org) self._make_eligible(project) self._store_event_and_update_group( @@ -1400,7 +1389,6 @@ def test_dispatch_failure_records_error(self) -> None: ) with ( - self.feature("organizations:gen-ai-features"), patch( "sentry.seer.agent.client.SeerAgentClient.start_feature_run", side_effect=RuntimeError("boom"), @@ -1422,8 +1410,7 @@ def test_outbox_drain_mirrors_run_against_seer(self) -> None: project, "fixable", seer_fixability_score=0.9, times_seen=5 ) - with self.feature("organizations:gen-ai-features"): - run_agentic_triage_for_org(org.id) + run_agentic_triage_for_org(org.id) seer_run = SeerRun.objects.get(organization=org, type=SeerRunType.FEATURE_RUN) assert seer_run.mirror_status == SeerRunMirrorStatus.PENDING diff --git a/tests/sentry/tasks/seer/test_explorer_index.py b/tests/sentry/tasks/seer/test_explorer_index.py index 36e65829a102..8cb7c22f04bb 100644 --- a/tests/sentry/tasks/seer/test_explorer_index.py +++ b/tests/sentry/tasks/seer/test_explorer_index.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from django.test import override_settings from sentry.constants import ObjectStatus from sentry.models.promptsactivity import PromptsActivity @@ -18,8 +19,23 @@ from sentry.viewer_context import ActorType, get_viewer_context +@override_settings(SENTRY_SELF_HOSTED=False) @django_db_all class TestGetSeerAgentEnabledProjects(TestCase): + def test_returns_nothing_on_self_hosted(self) -> None: + org = self.create_organization() + project = self.create_project(organization=org) + project.flags.has_transactions = True + project.save() + + with ( + freeze_time(datetime(2024, 1, 15, project.id % 23, tzinfo=UTC)), + self.feature({"organizations:seer-explorer-index": [org.slug]}), + ): + assert list(get_seer_explorer_enabled_projects()) == [(project.id, org.id)] + with override_settings(SENTRY_SELF_HOSTED=True): + assert list(get_seer_explorer_enabled_projects()) == [] + @freeze_time("2024-01-15 12:00:00") def test_returns_projects_with_feature_flag(self) -> None: org1 = self.create_organization() @@ -55,7 +71,6 @@ def test_returns_projects_with_feature_flag(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org1.slug, org2.slug], "organizations:seer-explorer-index": [org1.slug, org2.slug], } ): @@ -93,7 +108,6 @@ def test_excludes_inactive_projects(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-explorer-index": [org.slug], } ): @@ -119,7 +133,6 @@ def test_excludes_projects_with_hide_ai_features(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-explorer-index": [org.slug], } ): @@ -138,7 +151,6 @@ def test_excludes_projects_without_seer_acknowledgement(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-explorer-index": [org.slug], } ): @@ -161,7 +173,6 @@ def test_excludes_projects_without_transactions(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-explorer-index": [org.slug], } ): @@ -192,7 +203,6 @@ def test_includes_only_projects_matching_hour_shard(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-explorer-index": [org.slug], } ): @@ -222,12 +232,7 @@ def test_excludes_projects_without_seer_billing_plan(self) -> None: feature="seer_autofix_setup_acknowledged", ) - with self.feature( - { - "organizations:gen-ai-features": [org.slug], - } - ): - result = list(get_seer_explorer_enabled_projects()) + result = list(get_seer_explorer_enabled_projects()) assert len(result) == 0 assert project.id not in [p[0] for p in result] @@ -249,7 +254,6 @@ def test_includes_projects_with_legacy_seer_plan(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-added": [org.slug], } ): @@ -276,7 +280,6 @@ def test_includes_projects_with_seat_based_plan(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seat-based-seer-enabled": [org.slug], } ): @@ -287,6 +290,7 @@ def test_includes_projects_with_seat_based_plan(self) -> None: assert project.id in project_ids +@override_settings(SENTRY_SELF_HOSTED=False) @django_db_all class TestScheduleExplorerIndex(TestCase): def test_skips_when_killswitch_enabled(self) -> None: @@ -314,7 +318,6 @@ def test_schedules_projects(self) -> None: with self.feature( { - "organizations:gen-ai-features": [org.slug], "organizations:seer-explorer-index": [org.slug], } ): diff --git a/tests/sentry/tasks/test_llm_issue_detection.py b/tests/sentry/tasks/test_llm_issue_detection.py index 6302b60c1f66..e9796b96c835 100644 --- a/tests/sentry/tasks/test_llm_issue_detection.py +++ b/tests/sentry/tasks/test_llm_issue_detection.py @@ -4,6 +4,7 @@ import pytest from django.db.models import F +from django.test import override_settings from sentry.issues.grouptype import AIDetectedDBGroupType from sentry.models.project import Project @@ -24,7 +25,6 @@ ) from sentry.testutils.cases import APITransactionTestCase, SnubaTestCase, SpanTestCase, TestCase from sentry.testutils.helpers.datetime import before_now -from sentry.testutils.helpers.features import with_feature class LLMIssueDetectionTest(TestCase): @@ -42,7 +42,7 @@ def _budget_ok_response() -> Mock: response.data = b'{"has_budget": true}' return response - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( @@ -65,7 +65,7 @@ def test_detect_llm_issues_no_transactions( ) mock_seer_request.assert_not_called() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch("sentry.tasks.llm_issue_detection.trace_data.Spans.run_table_query") @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @@ -190,7 +190,7 @@ def test_general_type_skips_occurrence_creation(self, mock_produce_occurrence): ) assert not mock_produce_occurrence.called - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch("sentry.tasks.llm_issue_detection.trace_data.Spans.run_table_query") @@ -237,7 +237,7 @@ def test_detect_llm_issues_full_flow( assert seer_request.organization_id == self.organization.id assert len(seer_request.traces) == 1 - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch("sentry.tasks.llm_issue_detection.trace_data.Spans.run_table_query") @@ -272,7 +272,7 @@ def test_detect_llm_issues_seer_error_logged( assert mock_seer_request.call_count == 1 assert mock_logger_error.call_count == 1 - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( "sentry.tasks.llm_issue_detection.trace_data.get_project_top_transaction_traces_for_llm_detection" @@ -290,7 +290,7 @@ def test_check_budget_fail_open(self, mock_budget_request, mock_get_transactions mock_get_transactions.assert_called_once() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( "sentry.tasks.llm_issue_detection.trace_data.get_project_top_transaction_traces_for_llm_detection" @@ -309,7 +309,7 @@ def test_check_budget_over_budget( mock_get_transactions.assert_not_called() mock_seer_request.assert_not_called() - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( "sentry.tasks.llm_issue_detection.trace_data.get_project_top_transaction_traces_for_llm_detection" @@ -330,7 +330,7 @@ def test_plan_tier_forwarded_to_seer( == f"{SEER_CHECK_BUDGET_ENDPOINT_PATH}/:organization_id" ) - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( "sentry.tasks.llm_issue_detection.trace_data.get_project_top_transaction_traces_for_llm_detection" @@ -347,7 +347,7 @@ def test_plan_tier_defaults_to_business( budget_url = mock_budget_request.call_args[0][1] assert "plan_tier=business" in budget_url - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( @@ -368,7 +368,7 @@ def test_traces_sent_per_plan_tier( assert len(seer_request.traces) == expected assert seer_request.plan_tier == plan_tier - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch("sentry.tasks.llm_issue_detection.detection.make_issue_detection_request") @patch( @@ -393,7 +393,7 @@ def test_traces_per_invocation_option_override( class LLMIssueDetectionProjectFilterTest(TestCase): - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.tasks.llm_issue_detection.detection.make_signed_seer_api_request") @patch( "sentry.tasks.llm_issue_detection.trace_data.get_project_top_transaction_traces_for_llm_detection" diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index ff28649431b7..d37d56023b48 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3054,7 +3054,7 @@ def test_step_is_skipped_by_fully_specified_condition( class KickOffSeerAutomationTestMixin(BasePostProcessGroupMixin): @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_with_features(self, mock_generate_summary_and_run_automation): self.project.update_option("sentry:seer_scanner_automation", True) event = self.create_event( @@ -3073,8 +3073,9 @@ def test_kick_off_seer_automation_with_features(self, mock_generate_summary_and_ event.group.id, trigger_path="old_seer_automation" ) + @override_settings(SENTRY_SELF_HOSTED=True) @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - def test_kick_off_seer_automation_without_org_feature( + def test_kick_off_seer_automation_when_self_hosted( self, mock_generate_summary_and_run_automation ): self.project.update_option("sentry:seer_scanner_automation", True) @@ -3092,7 +3093,7 @@ def test_kick_off_seer_automation_without_org_feature( mock_generate_summary_and_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_without_scanner_on( self, mock_generate_summary_and_run_automation ): @@ -3113,7 +3114,7 @@ def test_kick_off_seer_automation_without_scanner_on( mock_generate_summary_and_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_skips_existing_fixability_score( self, mock_generate_summary_and_run_automation ): @@ -3138,7 +3139,7 @@ def test_kick_off_seer_automation_skips_existing_fixability_score( mock_generate_summary_and_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_skips_existing_issue( self, mock_generate_summary_and_run_automation ): @@ -3162,7 +3163,7 @@ def test_kick_off_seer_automation_skips_existing_issue( mock_generate_summary_and_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_skips_with_existing_fixability_score( self, mock_generate_summary_and_run_automation ): @@ -3195,7 +3196,7 @@ def test_kick_off_seer_automation_skips_with_existing_fixability_score( @patch("sentry.seer.autofix.utils.is_seer_scanner_rate_limited") @patch("sentry.quotas.backend.check_seer_quota") @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_rate_limit_only_checked_after_all_other_checks_pass( self, mock_generate_summary_and_run_automation, @@ -3266,7 +3267,7 @@ def test_rate_limit_only_checked_after_all_other_checks_pass( mock_generate_summary_and_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_skips_when_lock_held( self, mock_generate_summary_and_run_automation ): @@ -3315,7 +3316,7 @@ def test_kick_off_seer_automation_skips_when_lock_held( ) @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") - @with_feature("organizations:gen-ai-features") + @override_settings(SENTRY_SELF_HOSTED=False) def test_kick_off_seer_automation_with_hide_ai_features_enabled( self, mock_generate_summary_and_run_automation ): @@ -3403,7 +3404,7 @@ def _seat_based_post_process(self, **group_overrides): return event @patch("sentry.tasks.seer.autofix.generate_issue_summary_only.delay") - @with_feature({"organizations:gen-ai-features": True}) + @override_settings(SENTRY_SELF_HOSTED=False) def test_seat_based_org_skips_old_issues( self, mock_generate_summary_only, mock_seat_based_tier ): @@ -3411,7 +3412,7 @@ def test_seat_based_org_skips_old_issues( mock_generate_summary_only.assert_not_called() @patch("sentry.tasks.seer.autofix.generate_issue_summary_only.delay") - @with_feature({"organizations:gen-ai-features": True}) + @override_settings(SENTRY_SELF_HOSTED=False) def test_seat_based_org_skips_when_fixability_exists( self, mock_generate_summary_only, mock_seat_based_tier ): @@ -3422,6 +3423,7 @@ def test_seat_based_org_skips_when_fixability_exists( class SeerAutomationHelperFunctionsTestMixin(BasePostProcessGroupMixin): """Unit tests for is_issue_eligible_for_seer_automation.""" + @override_settings(SENTRY_SELF_HOSTED=False) @patch("sentry.quotas.backend.check_seer_quota", return_value=True) @patch("sentry.features.has", return_value=True) def test_is_issue_eligible_for_seer_automation(self, mock_features_has, mock_has_budget): @@ -3447,12 +3449,11 @@ def test_is_issue_eligible_for_seer_automation(self, mock_features_has, mock_has mock_category.return_value = GroupCategory.FEEDBACK assert is_issue_eligible_for_seer_automation(group) is False - # Missing feature flag - mock_features_has.return_value = False - assert is_issue_eligible_for_seer_automation(group) is False + # Seer unavailable on self-hosted + with override_settings(SENTRY_SELF_HOSTED=True): + 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 is_issue_eligible_for_seer_automation(group) is False self.organization.update_option("sentry:hide_ai_features", False) @@ -3503,7 +3504,7 @@ class PostProcessGroupErrorTest( ): @patch("sentry.seer.autofix.utils.is_seer_seat_based_tier_enabled", return_value=True) @patch("sentry.tasks.seer.autofix.generate_issue_summary_only.delay") - @with_feature({"organizations:gen-ai-features": True}) + @override_settings(SENTRY_SELF_HOSTED=False) def test_seat_based_org_generates_summary_for_new_issues( self, mock_generate_summary_only, mock_seat_based_tier ): diff --git a/tests/sentry/tasks/test_web_vitals_issue_detection.py b/tests/sentry/tasks/test_web_vitals_issue_detection.py index 6086d7d0eaa0..3dc5dabec921 100644 --- a/tests/sentry/tasks/test_web_vitals_issue_detection.py +++ b/tests/sentry/tasks/test_web_vitals_issue_detection.py @@ -47,7 +47,6 @@ def test_run_detection_dispatches_sub_tasks_when_enabled(self, mock_delay): "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), ): run_web_vitals_issue_detection() @@ -65,7 +64,6 @@ def test_run_detection_skips_when_no_github_code_mappings(self, mock_delay): "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), ): run_web_vitals_issue_detection() @@ -81,7 +79,6 @@ def test_run_detection_skips_when_not_allowlisted(self, mock_delay): "issue-detection.web-vitals-detection.projects-allowlist": [], } ), - self.feature("organizations:gen-ai-features"), ): run_web_vitals_issue_detection() @@ -173,7 +170,6 @@ def test_run_detection_produces_occurrences(self, mock_produce_occurrence_to_kaf "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection() @@ -291,7 +287,6 @@ def test_run_detection_groups_rendering_vitals(self, mock_produce_occurrence_to_ "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection() @@ -377,7 +372,6 @@ def test_run_detection_does_not_produce_occurrences_for_existing_issues( "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection() @@ -421,7 +415,6 @@ def test_run_detection_does_not_create_issue_on_insufficient_samples( "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection() @@ -502,7 +495,6 @@ def test_run_detection_selects_trace_closest_to_p75_web_vital_value( "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection() @@ -611,7 +603,6 @@ def test_run_detection_selects_trace_from_worst_score(self, mock_produce_occurre "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection() @@ -644,7 +635,6 @@ def test_run_detection_does_not_run_for_project_when_user_has_disabled( "issue-detection.web-vitals-detection.projects-allowlist": [project.id], } ), - self.feature("organizations:gen-ai-features"), TaskRunner(), ): run_web_vitals_issue_detection()