diff --git a/src/sentry/receivers/seer.py b/src/sentry/receivers/seer.py index 1f6ed04f3da2..0f0c7c6707c7 100644 --- a/src/sentry/receivers/seer.py +++ b/src/sentry/receivers/seer.py @@ -12,7 +12,7 @@ from sentry.models.project import Project from sentry.seer.autofix.constants import SeerAutomationSource from sentry.seer.autofix.trigger import is_issue_eligible_for_seer_automation -from sentry.seer.autofix.utils import is_seer_seat_based_tier_enabled +from sentry.seer.autofix.utils import is_seer_seat_or_token_billed_enabled from sentry.signals import integration_added, issue_assigned from sentry.tasks.seer.autofix import summarize_issue from sentry.tasks.seer.investigation import mark_investigation_telemetry_connected @@ -32,7 +32,7 @@ def dispatch_first_assignment_summary( ): return - if is_seer_seat_based_tier_enabled(project.organization): + if is_seer_seat_or_token_billed_enabled(project.organization): logger.info( "seer.issue_summary.first_assignment.skipped_seat_based_seer", extra={ diff --git a/src/sentry/seer/agentic_triage/delivery.py b/src/sentry/seer/agentic_triage/delivery.py index a1e4821bbe54..83d1427d21bc 100644 --- a/src/sentry/seer/agentic_triage/delivery.py +++ b/src/sentry/seer/agentic_triage/delivery.py @@ -27,7 +27,7 @@ AutofixStoppingPoint, bulk_read_preferences_from_sentry_db, is_seer_autotriggered_autofix_rate_limited_and_increment, - is_seer_seat_based_tier_enabled, + is_seer_seat_or_token_billed_enabled, ) from sentry.seer.models.agentic_triage import ( SeerAgenticTriageRunErrorType, @@ -313,8 +313,8 @@ def _process_verdicts( referrer = referrer_map[SeerAutomationSource.AGENTIC_TRIAGE] - # Rate limit only applies to legacy org plans - check_rate_limit = not is_seer_seat_based_tier_enabled(organization) + # Rate limit only applies to legacy usage-based orgs + check_rate_limit = not is_seer_seat_or_token_billed_enabled(organization) for group in fixable_groups: if check_rate_limit and is_seer_autotriggered_autofix_rate_limited_and_increment( diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index 33c2119afaf0..b2ce1a799e38 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -41,7 +41,7 @@ AutofixStoppingPoint, is_seer_autotriggered_autofix_rate_limited, is_seer_autotriggered_autofix_rate_limited_and_increment, - is_seer_seat_based_tier_enabled, + is_seer_seat_or_token_billed_enabled, ) from sentry.seer.entrypoints.cache import SeerOperatorAutofixCache from sentry.seer.entrypoints.operator import SeerAutofixOperator @@ -481,7 +481,7 @@ def get_automation_stopping_point(group: Group) -> AutofixStoppingPoint | None: """ user_preference = group.project.get_option("sentry:seer_automated_run_stopping_point") - if is_seer_seat_based_tier_enabled(group.organization): + if is_seer_seat_or_token_billed_enabled(group.organization): fixability_score = get_and_update_group_fixability_score(group) fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index 8a0c3c4ff489..eab22b63567e 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -115,7 +115,7 @@ def get_valid_automated_run_stopping_points( organization: Organization, ) -> frozenset[AutofixStoppingPoint]: """Return the set of stopping points valid for an org's billing tier.""" - if is_seer_seat_based_tier_enabled(organization): + if is_seer_seat_or_token_billed_enabled(organization): return SEAT_BASED_STOPPING_POINTS return USAGE_BASED_STOPPING_POINTS @@ -1046,6 +1046,16 @@ def is_seer_seat_based_tier_enabled(organization: Organization) -> bool: return has_seat_based_seer +def is_seer_seat_or_token_billed_enabled(organization: Organization) -> bool: + """ + Check if the organization is on seat-based or token-billed Seer, as opposed to + legacy usage-based Seer. Both use agentic triage for automation. + """ + return is_seer_seat_based_tier_enabled(organization) or features.has( + "organizations:token-billed-seer-enabled", organization + ) + + def is_seer_token_based_billing_onboarding_enabled(organization: Organization) -> bool: """Check if Seer onboarding changes for token-based billing apply to this organization.""" return features.has( @@ -1058,17 +1068,17 @@ def is_free_cohort_org(organization: Organization) -> bool: orgs that receive agentic triage and autofix without a Seer subscription. Returns True when the kill switch is NOT engaged (flag disabled = cohort - active), the org is NOT on a paid seat-based Seer plan, and the org - option is set to True. + active), the org is NOT on a paid seat-based or token-billed Seer plan, and + the org option is set to True. """ try: if features.has("organizations:agentic-triage-free-cohort-killswitch", organization): return False except Exception: return False - return not is_seer_seat_based_tier_enabled(organization) and bool( + return bool( OrganizationOption.objects.get_value(organization, "agentic-triage-free-cohort", False) - ) + ) and not is_seer_seat_or_token_billed_enabled(organization) def is_issue_category_eligible(group: Group) -> bool: diff --git a/src/sentry/seer/endpoints/project_seer_settings.py b/src/sentry/seer/endpoints/project_seer_settings.py index 754f09aafaf4..c2a34811bbd1 100644 --- a/src/sentry/seer/endpoints/project_seer_settings.py +++ b/src/sentry/seer/endpoints/project_seer_settings.py @@ -42,7 +42,7 @@ get_token_based_billing_project_repos, get_token_based_billing_repo_filter, get_valid_automated_run_stopping_points, - is_seer_seat_based_tier_enabled, + is_seer_seat_or_token_billed_enabled, is_seer_token_based_billing_onboarding_enabled, update_seer_project_settings, ) @@ -410,7 +410,7 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]: class ProjectSettingsUpdateSerializer(_BaseProjectSettingsUpdateSerializer): - """Seat-based (new) Seer: restricted tuning choices, stopping point sync.""" + """Seat-based and token-billed (new) Seer: restricted tuning choices, stopping point sync.""" automation_tuning = serializers.ChoiceField( choices=[AutofixAutomationTuningSettings.OFF, AutofixAutomationTuningSettings.MEDIUM], @@ -451,7 +451,7 @@ def get(self, request: Request, project: Project) -> Response: def put(self, request: Request, project: Project) -> Response: serializer_cls = ( ProjectSettingsUpdateSerializer - if is_seer_seat_based_tier_enabled(project.organization) + if is_seer_seat_or_token_billed_enabled(project.organization) else LegacyProjectSettingsUpdateSerializer ) serializer = serializer_cls( @@ -521,7 +521,7 @@ def get(self, request: Request, organization: Organization) -> Response: def put(self, request: Request, organization: Organization) -> Response: serializer_cls = ( BulkProjectSettingsUpdateSerializer - if is_seer_seat_based_tier_enabled(organization) + if is_seer_seat_or_token_billed_enabled(organization) else LegacyBulkProjectSettingsUpdateSerializer ) serializer = serializer_cls(data=request.data, context={"organization": organization}) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 1fe829845ec6..b1ec582ff5ab 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -26,7 +26,7 @@ AutomationCodingAgent, SeerProjectSettingsUpdate, get_org_default_seer_automation_handoff, - is_seer_seat_based_tier_enabled, + is_seer_seat_or_token_billed_enabled, update_seer_project_settings, ) from sentry.seer.similarity.types import GroupingVersion @@ -544,14 +544,14 @@ def set_default_project_autofix_automation_tuning( project.update_option( "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.OFF ) - elif is_seer_seat_based_tier_enabled(organization): - # Feature flag ON overrides everything except explicit "off" + elif is_seer_seat_or_token_billed_enabled(organization): + # Seat-based and token-billed orgs override everything except explicit "off" project.update_option( "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM, ) elif org_default: - # Feature flag OFF, use org's explicit value + # Legacy orgs use org's explicit value project.update_option("sentry:autofix_automation_tuning", org_default) @@ -559,11 +559,11 @@ def set_default_project_seer_scanner_automation( organization: Organization, project: Project ) -> None: """Called once at project creation time to set the initial seer scanner automation.""" - if is_seer_seat_based_tier_enabled(organization): - # Feature flag ON always sets scanner to True + if is_seer_seat_or_token_billed_enabled(organization): + # Seat-based and token-billed orgs always set scanner to True project.update_option("sentry:seer_scanner_automation", True) else: - # Feature flag OFF, use org's explicit value if set + # Legacy orgs use org's explicit value if set org_default = organization.get_option("sentry:default_seer_scanner_automation") if org_default is not None: project.update_option("sentry:seer_scanner_automation", org_default) @@ -573,7 +573,7 @@ def set_default_project_seer_preferences(organization: Organization, project: Pr """Called once at project creation time to set the initial automated run stopping point and automation handoff. """ - if not is_seer_seat_based_tier_enabled(organization): + if not is_seer_seat_or_token_billed_enabled(organization): return stopping_point, automation_handoff = get_org_default_seer_automation_handoff(organization) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index c51b93d836d8..a5cae01af730 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1643,7 +1643,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: get_issue_summary_lock_key, ) from sentry.seer.autofix.trigger import get_default_seer_automation_skip_reason - from sentry.seer.autofix.utils import is_seer_seat_based_tier_enabled + from sentry.seer.autofix.utils import is_seer_seat_or_token_billed_enabled from sentry.tasks.seer.autofix import ( generate_issue_summary_only, generate_summary_and_run_automation, @@ -1652,7 +1652,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: event = job["event"] group = event.group - if is_seer_seat_based_tier_enabled(group.organization): + if is_seer_seat_or_token_billed_enabled(group.organization): if group.seer_fixability_score is not None: return # Issues created in last 5 minutes only. This can be removed once this is live past 1 week. @@ -1660,7 +1660,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: if (timezone.now() - group.first_seen).total_seconds() > 300: return # Generate issue summary and fixability score if not cached. - # Agentic Triage handles automations for seat-based orgs but still needs these. + # Agentic Triage handles automations for seat-based and token-billed orgs but still needs these. cache_key = get_issue_summary_cache_key(group.id) if cache.get(cache_key) is None: lock_key, lock_name = get_issue_summary_lock_key(group.id) diff --git a/src/sentry/tasks/seer/agentic_triage/cron.py b/src/sentry/tasks/seer/agentic_triage/cron.py index 9386e15d1514..d7b17026b43f 100644 --- a/src/sentry/tasks/seer/agentic_triage/cron.py +++ b/src/sentry/tasks/seer/agentic_triage/cron.py @@ -37,7 +37,7 @@ bulk_read_preferences_from_sentry_db, is_free_cohort_org, is_seer_autotriggered_autofix_rate_limited, - is_seer_seat_based_tier_enabled, + is_seer_seat_or_token_billed_enabled, is_seer_token_based_billing_onboarding_enabled, ) from sentry.seer.models import SeerPermissionError @@ -704,7 +704,7 @@ class EligibleProject: tweaks: AgenticTriageTweaks stopping_point: AutofixStoppingPoint connected_repos: list[str] - # None for seat-based orgs, regardless of the project's own setting. + # None for seat-based and token-billed orgs, regardless of the project's own setting. automation_tuning: AutofixAutomationTuningSettings | None @@ -735,7 +735,7 @@ def _get_eligible_projects( organization.id, list(project_map), limit_repos=True ) - is_legacy_org = not is_seer_seat_based_tier_enabled(organization) + is_legacy_org = not is_seer_seat_or_token_billed_enabled(organization) eligible: list[EligibleProject] = [] for pid, project in project_map.items(): diff --git a/tests/sentry/core/endpoints/test_team_projects.py b/tests/sentry/core/endpoints/test_team_projects.py index 371134530053..4395b2c6e1a4 100644 --- a/tests/sentry/core/endpoints/test_team_projects.py +++ b/tests/sentry/core/endpoints/test_team_projects.py @@ -645,7 +645,7 @@ def test_project_autofix_tuning_not_set_if_org_option_not_set_in_db(self) -> Non project=project, key="sentry:autofix_automation_tuning" ).exists() - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + @patch("sentry.seer.similarity.utils.is_seer_seat_or_token_billed_enabled", return_value=True) def test_project_autofix_tuning_defaults_to_medium_with_seat_based_tier( self, mock_seat_based_tier: MagicMock ) -> None: @@ -665,7 +665,7 @@ def test_project_autofix_tuning_defaults_to_medium_with_seat_based_tier( ) assert autofix_tuning == "medium" - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + @patch("sentry.seer.similarity.utils.is_seer_seat_or_token_billed_enabled", return_value=True) def test_project_autofix_tuning_respects_explicit_off_even_with_seat_based_tier( self, mock_seat_based_tier: MagicMock ) -> None: @@ -685,7 +685,7 @@ def test_project_autofix_tuning_respects_explicit_off_even_with_seat_based_tier( ) assert autofix_tuning == "off" - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + @patch("sentry.seer.similarity.utils.is_seer_seat_or_token_billed_enabled", return_value=True) def test_project_autofix_tuning_seat_based_tier_overrides_non_off_org_default( self, mock_seat_based_tier: MagicMock ) -> None: diff --git a/tests/sentry/seer/agentic_triage/test_delivery.py b/tests/sentry/seer/agentic_triage/test_delivery.py index 4b78e696840a..da42dad21bd7 100644 --- a/tests/sentry/seer/agentic_triage/test_delivery.py +++ b/tests/sentry/seer/agentic_triage/test_delivery.py @@ -768,7 +768,7 @@ def test_seat_based_orgs_skip_the_rate_limit_check(self) -> None: return_value=True, ) as mock_rate_limited, patch( - "sentry.seer.agentic_triage.delivery.is_seer_seat_based_tier_enabled", + "sentry.seer.agentic_triage.delivery.is_seer_seat_or_token_billed_enabled", return_value=True, ), patch( diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index 9310ee4f7c13..be1e2f7742b4 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -21,6 +21,8 @@ from sentry.seer.autofix.trigger import is_issue_eligible_for_seer_automation from sentry.seer.autofix.utils import ( MAX_SEER_REPOS_PER_PROJECT, + SEAT_BASED_STOPPING_POINTS, + USAGE_BASED_STOPPING_POINTS, AutofixState, AutofixStoppingPoint, AutomationCodingAgent, @@ -33,8 +35,11 @@ get_org_default_seer_automation_handoff, get_repo_url_path, get_token_based_billing_project_repos, + get_valid_automated_run_stopping_points, has_project_connected_repos, + is_free_cohort_org, is_seer_seat_based_tier_enabled, + is_seer_seat_or_token_billed_enabled, read_preference_from_sentry_db, replace_all_seer_project_repos, update_seer_project_settings, @@ -274,6 +279,58 @@ def test_returns_cached_value(self) -> None: assert result is True +class TestIsSeerSeatOrTokenBilled(TestCase): + def setUp(self) -> None: + super().setUp() + self.organization = self.create_organization(name="test-org") + + def tearDown(self) -> None: + super().tearDown() + cache.delete(f"seer:seat-based-tier:{self.organization.id}") + + @with_feature("organizations:seat-based-seer-enabled") + def test_seat_based(self) -> None: + assert is_seer_seat_or_token_billed_enabled(self.organization) is True + assert get_valid_automated_run_stopping_points(self.organization) == ( + SEAT_BASED_STOPPING_POINTS + ) + + @with_feature("organizations:token-billed-seer-enabled") + def test_token_billed(self) -> None: + assert is_seer_seat_or_token_billed_enabled(self.organization) is True + assert get_valid_automated_run_stopping_points(self.organization) == ( + SEAT_BASED_STOPPING_POINTS + ) + + def test_legacy(self) -> None: + assert is_seer_seat_or_token_billed_enabled(self.organization) is False + assert get_valid_automated_run_stopping_points(self.organization) == ( + USAGE_BASED_STOPPING_POINTS + ) + + +class TestIsFreeCohortOrg(TestCase): + def setUp(self) -> None: + super().setUp() + self.organization = self.create_organization(name="test-org") + self.organization.update_option("agentic-triage-free-cohort", True) + + def tearDown(self) -> None: + super().tearDown() + cache.delete(f"seer:seat-based-tier:{self.organization.id}") + + def test_legacy(self) -> None: + assert is_free_cohort_org(self.organization) is True + + @with_feature("organizations:seat-based-seer-enabled") + def test_seat_based(self) -> None: + assert is_free_cohort_org(self.organization) is False + + @with_feature("organizations:token-billed-seer-enabled") + def test_token_billed(self) -> None: + assert is_free_cohort_org(self.organization) is False + + class HasProjectConnectedReposBaseTest(TestCase): """Test the has_project_connected_repos function.""" diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 122cee1a24c1..450856f34250 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -889,7 +889,7 @@ def test_stopping_point_mapping(self, score: float, expected: AutofixStoppingPoi @override_settings(SENTRY_SELF_HOSTED=False) -@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True) +@patch("sentry.seer.autofix.issue_summary.is_seer_seat_or_token_billed_enabled", return_value=True) class TestRunAutomationStoppingPoint(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1052,7 +1052,7 @@ def test_upper_bound_combinations( @override_settings(SENTRY_SELF_HOSTED=False) -@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True) +@patch("sentry.seer.autofix.issue_summary.is_seer_seat_or_token_billed_enabled", return_value=True) class TestRunAutomationWithUpperBound(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1363,7 +1363,7 @@ def test_returns_false_when_rate_limited( assert is_group_eligible_for_automation(self.group) is False -@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True) +@patch("sentry.seer.autofix.issue_summary.is_seer_seat_or_token_billed_enabled", return_value=True) @override_settings(SENTRY_SELF_HOSTED=False) class TestGetAutomationStoppingPoint(TestCase): def setUp(self) -> None: diff --git a/tests/sentry/seer/endpoints/test_project_seer_settings.py b/tests/sentry/seer/endpoints/test_project_seer_settings.py index e07e8c85944e..9a4252253b3e 100644 --- a/tests/sentry/seer/endpoints/test_project_seer_settings.py +++ b/tests/sentry/seer/endpoints/test_project_seer_settings.py @@ -13,7 +13,7 @@ @patch( - "sentry.seer.endpoints.project_seer_settings.is_seer_seat_based_tier_enabled", + "sentry.seer.endpoints.project_seer_settings.is_seer_seat_or_token_billed_enabled", return_value=True, ) class ProjectSeerSettingsEndpointBaseTest(APITestCase): @@ -473,7 +473,7 @@ def test_get_repos_count_excludes_repos_seer_cannot_use(self) -> None: @patch( - "sentry.seer.endpoints.project_seer_settings.is_seer_seat_based_tier_enabled", + "sentry.seer.endpoints.project_seer_settings.is_seer_seat_or_token_billed_enabled", return_value=True, ) class OrganizationSeerProjectSettingsEndpointBaseTest(APITestCase): diff --git a/tests/sentry/tasks/seer/test_agentic_triage.py b/tests/sentry/tasks/seer/test_agentic_triage.py index 9b79d346e0f9..628f1fb7ac43 100644 --- a/tests/sentry/tasks/seer/test_agentic_triage.py +++ b/tests/sentry/tasks/seer/test_agentic_triage.py @@ -738,7 +738,7 @@ def test_seat_based_orgs_skip_the_rate_limit_check(self) -> None: return_value=True, ), patch( - "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_based_tier_enabled", + "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_or_token_billed_enabled", return_value=True, ), ): @@ -754,13 +754,32 @@ def test_seat_based_orgs_get_no_automation_tuning(self) -> None: ) with patch( - "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_based_tier_enabled", + "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_or_token_billed_enabled", return_value=True, ): result = _get_eligible_projects(org, "manual") assert result[0].automation_tuning is None + def test_token_billed_orgs_are_not_legacy(self) -> None: + org = self.create_organization() + project = self._make_eligible(self.create_project(organization=org)) + project.update_option( + "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.LOW + ) + + with ( + patch( + "sentry.tasks.seer.agentic_triage.cron.is_seer_autotriggered_autofix_rate_limited", + return_value=True, + ), + self.feature("organizations:token-billed-seer-enabled"), + ): + result = _get_eligible_projects(org, "manual") + + assert [ep.project for ep in result] == [project] + assert result[0].automation_tuning is None + @override_settings(SENTRY_SELF_HOSTED=False) @django_db_all @@ -1229,7 +1248,7 @@ def test_payload_omits_automation_tuning_for_seat_based_orgs(self) -> None: with ( patch( - "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_based_tier_enabled", + "sentry.tasks.seer.agentic_triage.cron.is_seer_seat_or_token_billed_enabled", return_value=True, ), ): diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 6c8fc0492f42..1075f0fd3727 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3809,6 +3809,24 @@ def test_seat_based_org_generates_summary_for_new_issues( ) mock_generate_summary_only.assert_called_once_with(require_group(event).id) + @patch("sentry.seer.autofix.utils.is_seer_seat_based_tier_enabled", return_value=False) + @patch("sentry.tasks.seer.autofix.generate_summary_and_run_automation.delay") + @patch("sentry.tasks.seer.autofix.generate_issue_summary_only.delay") + @override_settings(SENTRY_SELF_HOSTED=False) + def test_token_billed_org_generates_summary_for_new_issues( + self, + mock_generate_summary_only: MagicMock, + mock_generate_summary_and_run_automation: MagicMock, + mock_seat_based_tier: MagicMock, + ) -> None: + event = self.create_event(data={"message": "testing"}, project_id=self.project.id) + with self.feature("organizations:token-billed-seer-enabled"): + self.call_post_process_group( + is_new=True, is_regression=False, is_new_group_environment=True, event=event + ) + mock_generate_summary_only.assert_called_once_with(require_group(event).id) + mock_generate_summary_and_run_automation.assert_not_called() + def setUp(self) -> None: super().setUp()