From 5f712b8ccdd049a247e7fe36abeb1b4cfdd79188 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 28 Sep 2026 12:03:36 -0700 Subject: [PATCH] ref(seer): Gate Seer access on not being self-hosted Fold a self-hosted check into has_seer_access and has_seer_access_with_detail next to the existing gen-ai-features flag, and add an areAiFeaturesAllowed frontend util that also checks ConfigStore.isSelfHosted. This is the first step toward replacing gen-ai-features with an explicit not-self-hosted rule. Tests run in self-hosted mode by default, so test classes that reach the real helpers now pin SENTRY_SELF_HOSTED=False. --- src/sentry/seer/seer_setup.py | 10 ++++- .../autofix/useOrganizationSeerSetup.tsx | 4 +- .../utils/seer/areAiFeaturesAllowed.spec.ts | 40 +++++++++++++++++++ static/app/utils/seer/areAiFeaturesAllowed.ts | 12 ++++++ .../views/issueDetails/hooks/useAiConfig.tsx | 4 +- .../test_organization_dashboard_generate.py | 3 ++ .../slack/test_message_builder.py | 5 +++ .../slack/webhooks/events/__init__.py | 2 + tests/sentry/pr_metrics/test_webhooks.py | 2 + tests/sentry/seer/agent/test_client_utils.py | 1 + .../sentry/seer/autofix/test_autofix_agent.py | 2 + .../sentry/seer/autofix/test_issue_summary.py | 5 +++ .../seer/endpoints/test_group_ai_autofix.py | 3 ++ .../endpoints/test_group_autofix_repos.py | 3 ++ .../test_organization_seer_agent_chat.py | 3 ++ .../endpoints/test_organization_seer_runs.py | 3 ++ .../test_organization_seer_workflows.py | 9 +++++ .../seer/endpoints/test_search_agent_state.py | 1 + .../seer/entrypoints/slack/test_tasks.py | 3 ++ .../sentry/seer/entrypoints/test_operator.py | 3 ++ tests/sentry/seer/test_seer_setup.py | 39 ++++++++++++++++++ tests/sentry/seer/workflows/test_runs.py | 2 + .../sentry/tasks/seer/test_agentic_triage.py | 5 +++ 23 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 static/app/utils/seer/areAiFeaturesAllowed.spec.ts create mode 100644 static/app/utils/seer/areAiFeaturesAllowed.ts create mode 100644 tests/sentry/seer/test_seer_setup.py diff --git a/src/sentry/seer/seer_setup.py b/src/sentry/seer/seer_setup.py index e0bfba6bc37c..e77315892bcf 100644 --- a/src/sentry/seer/seer_setup.py +++ b/src/sentry/seer/seer_setup.py @@ -10,6 +10,7 @@ ) from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser +from sentry.utils.settings import is_self_hosted def get_supported_scm_providers(organization: Organization | None = None) -> list[str]: @@ -27,8 +28,10 @@ def has_seer_access( organization: Organization | RpcOrganization, actor: User | AnonymousUser | RpcUser | None = None, ) -> bool: - return features.has("organizations:gen-ai-features", organization, actor=actor) and not bool( - organization.get_option("sentry:hide_ai_features") + 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")) ) @@ -36,6 +39,9 @@ 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" diff --git a/static/app/components/events/autofix/useOrganizationSeerSetup.tsx b/static/app/components/events/autofix/useOrganizationSeerSetup.tsx index 4773bbb3a48a..e9837d376ea4 100644 --- a/static/app/components/events/autofix/useOrganizationSeerSetup.tsx +++ b/static/app/components/events/autofix/useOrganizationSeerSetup.tsx @@ -1,6 +1,7 @@ import type {ApiQueryKey} from 'sentry/utils/api/apiQueryKey'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {useApiQuery, type UseApiQueryOptions} from 'sentry/utils/queryClient'; +import {areAiFeaturesAllowed as computeAreAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; interface OrganizationSeerSetupResponse { @@ -24,8 +25,7 @@ export function useOrganizationSeerSetup( ) { const organization = useOrganization(); const orgSlug = organization.slug; - const areAiFeaturesAllowed = - !organization.hideAiFeatures && organization.features.includes('gen-ai-features'); + const areAiFeaturesAllowed = computeAreAiFeaturesAllowed(organization); const queryData = useApiQuery( makeOrganizationSeerSetupQueryKey(orgSlug), diff --git a/static/app/utils/seer/areAiFeaturesAllowed.spec.ts b/static/app/utils/seer/areAiFeaturesAllowed.spec.ts new file mode 100644 index 000000000000..3f11286e12b6 --- /dev/null +++ b/static/app/utils/seer/areAiFeaturesAllowed.spec.ts @@ -0,0 +1,40 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {ConfigStore} from 'sentry/stores/configStore'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; + +describe('areAiFeaturesAllowed', () => { + beforeEach(() => { + ConfigStore.set('isSelfHosted', false); + }); + + it('allows when flagged, not hidden, and not self-hosted', () => { + const organization = OrganizationFixture({ + features: ['gen-ai-features'], + hideAiFeatures: false, + }); + expect(areAiFeaturesAllowed(organization)).toBe(true); + }); + + it('denies without the flag', () => { + const organization = OrganizationFixture({features: [], hideAiFeatures: false}); + expect(areAiFeaturesAllowed(organization)).toBe(false); + }); + + it('denies when the org hides AI features', () => { + const organization = OrganizationFixture({ + features: ['gen-ai-features'], + hideAiFeatures: true, + }); + expect(areAiFeaturesAllowed(organization)).toBe(false); + }); + + it('denies on self-hosted', () => { + ConfigStore.set('isSelfHosted', true); + const organization = OrganizationFixture({ + features: ['gen-ai-features'], + hideAiFeatures: false, + }); + expect(areAiFeaturesAllowed(organization)).toBe(false); + }); +}); diff --git a/static/app/utils/seer/areAiFeaturesAllowed.ts b/static/app/utils/seer/areAiFeaturesAllowed.ts new file mode 100644 index 000000000000..66084530b2c8 --- /dev/null +++ b/static/app/utils/seer/areAiFeaturesAllowed.ts @@ -0,0 +1,12 @@ +import {ConfigStore} from 'sentry/stores/configStore'; +import type {Organization} from 'sentry/types/organization'; + +export function areAiFeaturesAllowed( + organization: Pick +): boolean { + return ( + !ConfigStore.get('isSelfHosted') && + !organization.hideAiFeatures && + organization.features.includes('gen-ai-features') + ); +} diff --git a/static/app/views/issueDetails/hooks/useAiConfig.tsx b/static/app/views/issueDetails/hooks/useAiConfig.tsx index e9c7643873f8..89f4490647ed 100644 --- a/static/app/views/issueDetails/hooks/useAiConfig.tsx +++ b/static/app/views/issueDetails/hooks/useAiConfig.tsx @@ -2,6 +2,7 @@ import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup' import type {Group} from 'sentry/types/group'; import type {Project} from 'sentry/types/project'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; +import {areAiFeaturesAllowed as computeAreAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useIsSampleEvent} from 'sentry/views/issueDetails/utils'; @@ -34,8 +35,7 @@ export const useAiConfig = (group: Group, project: Project): AiConfigResult => { const issueTypeConfig = getConfigForIssueType(group, project); - const areAiFeaturesAllowed = - !organization.hideAiFeatures && organization.features.includes('gen-ai-features'); + const areAiFeaturesAllowed = computeAreAiFeaturesAllowed(organization); const isSummaryEnabled = issueTypeConfig.issueSummary.enabled; const isAutofixEnabled = issueTypeConfig.autofix; diff --git a/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py b/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py index b8166e10beb9..bdca7ac3e484 100644 --- a/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py +++ b/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py @@ -4,12 +4,15 @@ from typing import Any from unittest.mock import ANY, MagicMock, patch +from django.test import override_settings + 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/slack/test_message_builder.py b/tests/sentry/integrations/slack/test_message_builder.py index 67516afeaa73..db77174a8167 100644 --- a/tests/sentry/integrations/slack/test_message_builder.py +++ b/tests/sentry/integrations/slack/test_message_builder.py @@ -4,6 +4,8 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch +from django.test import override_settings + from sentry.grouping.grouptype import ErrorGroupType from sentry.integrations.messaging.message_builder import ( build_attachment_text, @@ -996,6 +998,7 @@ def _has_autofix_button(self, blocks: dict[str, Any]) -> bool: return True return False + @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: @@ -1003,6 +1006,7 @@ def test_autofix_button_shown_when_all_conditions_met(self, mock_quota: MagicMoc blocks = SlackIssuesMessageBuilder(group).build() assert self._has_autofix_button(blocks) + @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: @@ -1010,6 +1014,7 @@ def test_autofix_button_hidden_on_unfurl(self, mock_quota: MagicMock) -> None: blocks = SlackIssuesMessageBuilder(group, is_unfurl=True).build() assert not self._has_autofix_button(blocks) + @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: diff --git a/tests/sentry/integrations/slack/webhooks/events/__init__.py b/tests/sentry/integrations/slack/webhooks/events/__init__.py index 2ad29ccd5d82..7baa2039fc9f 100644 --- a/tests/sentry/integrations/slack/webhooks/events/__init__.py +++ b/tests/sentry/integrations/slack/webhooks/events/__init__.py @@ -2,6 +2,7 @@ from unittest.mock import patch import orjson +from django.test import override_settings from sentry.testutils.cases import APITestCase from sentry.testutils.helpers import install_slack @@ -54,6 +55,7 @@ def build_test_block(link): } +@override_settings(SENTRY_SELF_HOSTED=False) class BaseEventTest(APITestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/pr_metrics/test_webhooks.py b/tests/sentry/pr_metrics/test_webhooks.py index 437d335495f3..10885137b1ac 100644 --- a/tests/sentry/pr_metrics/test_webhooks.py +++ b/tests/sentry/pr_metrics/test_webhooks.py @@ -8,6 +8,7 @@ from django.conf import settings from django.core.cache import cache from django.db import OperationalError +from django.test import override_settings from sentry.analytics.events.pr_metrics_events import PrCloseMetricsEvent from sentry.integrations.github.webhook import PullRequestEventWebhook @@ -2172,6 +2173,7 @@ 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"]) @cell_silo_test class HandleWebhookForPrMetricsJudgeForwardTest(TestCase): diff --git a/tests/sentry/seer/agent/test_client_utils.py b/tests/sentry/seer/agent/test_client_utils.py index 48b85bdcecb1..2244609057ca 100644 --- a/tests/sentry/seer/agent/test_client_utils.py +++ b/tests/sentry/seer/agent/test_client_utils.py @@ -28,6 +28,7 @@ from sentry.viewer_context import ActorType, ViewerContext, viewer_context_scope +@override_settings(SENTRY_SELF_HOSTED=False) class TestHasSeerAgentAccessWithDetail(TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/autofix/test_autofix_agent.py b/tests/sentry/seer/autofix/test_autofix_agent.py index a16179f74107..d028e7538c73 100644 --- a/tests/sentry/seer/autofix/test_autofix_agent.py +++ b/tests/sentry/seer/autofix/test_autofix_agent.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch import pytest +from django.test import override_settings from rest_framework.exceptions import PermissionDenied from sentry.analytics.events.autofix_events import AiAutofixSolutionCompletedEvent @@ -1823,6 +1824,7 @@ def test_trigger_coding_agent_handoff_resolves_default_branch_when_empty( assert repos[0].branch_name == "main" +@override_settings(SENTRY_SELF_HOSTED=False) class TestTriggerPushChanges(TestCase): """Tests for trigger_push_changes function.""" diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 1e9c4adba664..50fb72261bd3 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -5,6 +5,7 @@ import orjson import pytest +from django.test import override_settings from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case from sentry.issues.action_log.types import SYSTEM_ACTOR, ActionSource, TriggerAutofixAction @@ -70,6 +71,7 @@ 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: @@ -878,6 +880,7 @@ def test_stopping_point_mapping(self, score, expected): assert _get_stopping_point_from_fixability(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): @@ -1027,6 +1030,7 @@ def test_upper_bound_combinations(self, fixability, user_pref, expected): assert result == 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): @@ -1244,6 +1248,7 @@ def test_no_summary_in_cache_calls_seer_without_summary(self, mock_request): assert "summary" not in payload +@override_settings(SENTRY_SELF_HOSTED=False) @with_feature("organizations:gen-ai-features") class TestIsGroupEligibleForAutomation(APITestCase, SnubaTestCase): def setUp(self) -> None: diff --git a/tests/sentry/seer/endpoints/test_group_ai_autofix.py b/tests/sentry/seer/endpoints/test_group_ai_autofix.py index 7dc181241fca..e59e63660e1b 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/seer/endpoints/test_group_ai_autofix.py @@ -2,6 +2,8 @@ import uuid from unittest.mock import ANY, Mock, call, patch +from django.test import override_settings + from sentry.integrations.services.integration import RpcIntegration from sentry.integrations.types import ExternalProviders from sentry.integrations.utils.github_permission_tiers import PR_ITERATION_TIER @@ -57,6 +59,7 @@ 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: diff --git a/tests/sentry/seer/endpoints/test_group_autofix_repos.py b/tests/sentry/seer/endpoints/test_group_autofix_repos.py index 4dc45333ccee..676f9ddafe66 100644 --- a/tests/sentry/seer/endpoints/test_group_autofix_repos.py +++ b/tests/sentry/seer/endpoints/test_group_autofix_repos.py @@ -1,9 +1,12 @@ from unittest.mock import MagicMock, patch +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: 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 574d7dd8a612..418845ae756a 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_agent_chat.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_agent_chat.py @@ -3,6 +3,7 @@ from unittest.mock import ANY, MagicMock, Mock, patch import pytest +from django.test import override_settings from sentry.seer.agent.client_models import ( MemoryBlock, @@ -20,6 +21,7 @@ @with_feature("organizations:seer-explorer") +@override_settings(SENTRY_SELF_HOSTED=False) @with_feature("organizations:gen-ai-features") class OrganizationSeerAgentChatEndpointTest(APITestCase): def setUp(self) -> None: @@ -671,6 +673,7 @@ 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_runs.py b/tests/sentry/seer/endpoints/test_organization_seer_runs.py index 925480646d54..0cf6d6f786be 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_runs.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_runs.py @@ -2,6 +2,8 @@ from typing import Any from unittest.mock import patch +from django.test import override_settings + from sentry.seer.models.run import SeerRunPullRequest, SeerRunType from sentry.seer.run_questions import QUESTIONS, question_hash from sentry.testutils.cases import APITestCase @@ -9,6 +11,7 @@ from sentry.testutils.helpers.features import with_feature +@override_settings(SENTRY_SELF_HOSTED=False) @with_feature("organizations:seer-explorer") @with_feature("organizations:gen-ai-features") class OrganizationSeerRunsEndpointTest(APITestCase): diff --git a/tests/sentry/seer/endpoints/test_organization_seer_workflows.py b/tests/sentry/seer/endpoints/test_organization_seer_workflows.py index 11f45f7e9988..3f60676e0d01 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_workflows.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_workflows.py @@ -1,5 +1,7 @@ from unittest.mock import patch +from django.test import override_settings + from sentry.hybridcloud.models.outbox import CellOutbox from sentry.hybridcloud.outbox.category import OutboxCategory from sentry.models.pullrequest import PullRequestLifecycleState @@ -21,6 +23,7 @@ from sentry.testutils.factories import Factories +@override_settings(SENTRY_SELF_HOSTED=False) class OrganizationSeerWorkflowsTest(APITestCase): endpoint = "sentry-api-0-organization-seer-workflows" @@ -439,12 +442,18 @@ def create_agent_workflow( ) +@override_settings(SENTRY_SELF_HOSTED=False) class OrganizationSeerMonitorCleanupTest(APITestCase): endpoint = "sentry-api-0-organization-seer-workflows" method = "post" 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.keep = self.create_detector(project=self.project, type="metric_issue", name="Keep") self.duplicate = self.create_detector( project=self.project, type="metric_issue", name="Copy" diff --git a/tests/sentry/seer/endpoints/test_search_agent_state.py b/tests/sentry/seer/endpoints/test_search_agent_state.py index 8baf8e4bfe6b..55b378f0dffd 100644 --- a/tests/sentry/seer/endpoints/test_search_agent_state.py +++ b/tests/sentry/seer/endpoints/test_search_agent_state.py @@ -6,6 +6,7 @@ from sentry.testutils.cases import APITestCase +@override_settings(SENTRY_SELF_HOSTED=False) @override_settings(SEER_AUTOFIX_URL="https://seer.example.com") class SearchAgentStateEndpointTest(APITestCase): endpoint = "sentry-api-0-search-agent-state" diff --git a/tests/sentry/seer/entrypoints/slack/test_tasks.py b/tests/sentry/seer/entrypoints/slack/test_tasks.py index f0f7815d8610..fd3a5ec2bc9c 100644 --- a/tests/sentry/seer/entrypoints/slack/test_tasks.py +++ b/tests/sentry/seer/entrypoints/slack/test_tasks.py @@ -1,5 +1,7 @@ from unittest.mock import ANY, MagicMock, patch +from django.test import override_settings + from sentry.integrations.messaging.metrics import SeerSlackHaltReason from sentry.seer.entrypoints.slack.analytics import ( SlackSeerAgentConversation, @@ -803,6 +805,7 @@ def test_mixed_unresolved_and_private_renders_both_sections(self) -> None: } +@override_settings(SENTRY_SELF_HOSTED=False) class ProcessReactionForSlackTest(TestCase): def setUp(self): super().setUp() diff --git a/tests/sentry/seer/entrypoints/test_operator.py b/tests/sentry/seer/entrypoints/test_operator.py index 735cc77cb7d5..5c68f8dd34b0 100644 --- a/tests/sentry/seer/entrypoints/test_operator.py +++ b/tests/sentry/seer/entrypoints/test_operator.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import pytest +from django.test import override_settings from fixtures.seer.webhooks import MOCK_RUN_ID from sentry.integrations.types import ExternalProviders @@ -109,6 +110,7 @@ def on_autofix_update( MockCachePayload(**cache_payload) +@override_settings(SENTRY_SELF_HOSTED=False) class SeerOperatorTest(TestCase): def setUp(self) -> None: self.entrypoint = MockAutofixEntrypoint() @@ -941,6 +943,7 @@ def on_agent_update( return None +@override_settings(SENTRY_SELF_HOSTED=False) class TestSeerAgentOperatorAccess(TestCase): def setUp(self) -> None: self.entrypoint = MockAgentEntrypoint() diff --git a/tests/sentry/seer/test_seer_setup.py b/tests/sentry/seer/test_seer_setup.py new file mode 100644 index 000000000000..710e69e2ae40 --- /dev/null +++ b/tests/sentry/seer/test_seer_setup.py @@ -0,0 +1,39 @@ +from django.test import override_settings + +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) + assert has_seer_access(org) is False + assert has_seer_access_with_detail(org) == ( + False, + "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() + assert has_seer_access(org) is False + assert has_seer_access_with_detail(org) == ( + False, + "Seer is not available on this installation.", + ) diff --git a/tests/sentry/seer/workflows/test_runs.py b/tests/sentry/seer/workflows/test_runs.py index 17fe2b42a6c3..961482ef0963 100644 --- a/tests/sentry/seer/workflows/test_runs.py +++ b/tests/sentry/seer/workflows/test_runs.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch import pytest +from django.test import override_settings from sentry.hybridcloud.models.outbox import CellOutbox from sentry.hybridcloud.outbox.category import OutboxCategory @@ -18,6 +19,7 @@ from sentry.testutils.cases import TestCase +@override_settings(SENTRY_SELF_HOSTED=False) class WorkflowRunTest(TestCase): def test_creation_links_execution_and_defers_dispatch(self) -> None: with patch("sentry.receivers.outbox.cell.make_feature_run_request") as dispatch: diff --git a/tests/sentry/tasks/seer/test_agentic_triage.py b/tests/sentry/tasks/seer/test_agentic_triage.py index 8a0dfb20273b..fe68a8629e9f 100644 --- a/tests/sentry/tasks/seer/test_agentic_triage.py +++ b/tests/sentry/tasks/seer/test_agentic_triage.py @@ -5,6 +5,7 @@ import pytest from django.conf import settings +from django.test import override_settings from taskbroker_client.scheduler.config import crontab from taskbroker_client.scheduler.runner import ScheduleEntry @@ -73,6 +74,7 @@ def _dispatched_feature_body(organization): return seer_run, outbox.payload["body"] +@override_settings(SENTRY_SELF_HOSTED=False) @django_db_all @pytest.mark.parametrize("enabled,mode", [(False, "off"), (True, "only")]) def test_code_mode_flag_applies_to_every_dispatched_shard(default_organization, enabled, mode): @@ -102,6 +104,7 @@ def test_code_mode_flag_applies_to_every_dispatched_shard(default_organization, assert outbox.payload["body"]["agent_run_options"]["enable_code_mode_tools"] == mode +@override_settings(SENTRY_SELF_HOSTED=False) @django_db_all def test_redispatch_preserves_recorded_code_mode_after_flag_is_disabled(default_organization): run = Factories.create_seer_workflow_run(organization=default_organization) @@ -704,6 +707,7 @@ def test_seat_based_orgs_get_no_automation_tuning(self) -> None: assert result[0].automation_tuning is None +@override_settings(SENTRY_SELF_HOSTED=False) @django_db_all class TestRunAgenticTriageForOrg(AgenticTriageFixtures, TestCase, SnubaTestCase): reset_snuba_data = False @@ -1029,6 +1033,7 @@ def test_scheduler_skips_projects_with_tweaks_disabled(self) -> None: assert [p.id for p in mock_score.call_args.args[0]] == [enabled.id] +@override_settings(SENTRY_SELF_HOSTED=False) @django_db_all class TestRunAgenticTriageFeatureDelivery(AgenticTriageFixtures, TestCase, SnubaTestCase): """Coverage for the dispatch path, which hands triage off to Seer's