diff --git a/src/sentry/seer/autofix/autofix.py b/src/sentry/seer/autofix/autofix.py index 8b7284dfd43a2f..5a6547aeb291f7 100644 --- a/src/sentry/seer/autofix/autofix.py +++ b/src/sentry/seer/autofix/autofix.py @@ -35,7 +35,6 @@ ) from sentry.seer.autofix.utils import ( AutofixStoppingPoint, - get_autofix_repos_from_project_code_mappings, get_org_default_seer_automation_handoff, get_project_seer_preferences, make_autofix_start_request, @@ -638,34 +637,34 @@ def get_all_tags_overview( def _resolve_project_preference( - organization: Organization, project: Project, fallback_repos: list[dict] + organization: Organization, project: Project ) -> SeerProjectPreference | None: """ Resolve the Seer project preference for a project before triggering autofix. - If an existing preference is found in Seer, returns it. - If not, creates one from fallback_repos. + Returns the existing preference if one exists. If not, creates a new one + with empty repos and org default settings. """ if features.has("organizations:seer-project-settings-read-from-sentry", organization): - preference = read_preference_from_sentry_db(project) - else: - try: - preference = get_project_seer_preferences(project.id).preference - except (SeerApiError, SeerApiResponseValidationError): - logger.exception( - "seer.resolve_project_preference.get_failed", - extra={"project_id": project.id, "organization_id": organization.id}, - ) - return None + return read_preference_from_sentry_db(project) - if preference: - return preference + try: + preference = get_project_seer_preferences(project.id).preference + if preference: + return preference + except (SeerApiError, SeerApiResponseValidationError): + logger.exception( + "seer.resolve_project_preference.get_failed", + extra={"project_id": project.id, "organization_id": organization.id}, + ) + return None + # No preference exists — create one with org defaults. default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization) preference = SeerProjectPreference( organization_id=organization.id, project_id=project.id, - repositories=fallback_repos, + repositories=[], automated_run_stopping_point=default_stopping_point, automation_handoff=default_handoff, ) @@ -684,7 +683,7 @@ def _resolve_project_preference( write_preference_to_sentry_db(project, preference) except Exception: logger.exception( - "seer.write_preferences.resolve_project_preference.sentry_db_write_failed", + "seer.resolve_project_preference.write_failed", extra={"project_id": project.id, "organization_id": organization.id}, exc_info=True, ) @@ -738,19 +737,14 @@ def trigger_autofix( return _respond_with_error("Cannot fix issues without an event.", 400) code_mappings = get_sorted_code_mapping_configs(group.project) - code_mappings_repos = get_autofix_repos_from_project_code_mappings( - group.project, code_mappings=code_mappings - ) - # Resolve the project preference from Seer, or bootstrap one from code mapping repos. - # On success, preference.repositories becomes the source of truth for repos - # (even if empty — matching Seer's behavior of unconditionally using preference repos). - # On failure, we fall back to the original code mapping repos above. - preference = _resolve_project_preference(group.organization, group.project, code_mappings_repos) + # Resolve the project preference, or create a new one with org defaults. + # Preference repos are the source of truth (even if empty). + preference = _resolve_project_preference(group.organization, group.project) if preference: repos = [repo.dict() for repo in preference.repositories] else: - repos = code_mappings_repos + repos = [] # Pre-resolve stacktrace frame paths using code mappings so Seer can skip # expensive git tree fetches for large repos. diff --git a/src/sentry/seer/autofix/autofix_agent.py b/src/sentry/seer/autofix/autofix_agent.py index a0f7c11e3d1c94..6c13835836a7b1 100644 --- a/src/sentry/seer/autofix/autofix_agent.py +++ b/src/sentry/seer/autofix/autofix_agent.py @@ -513,17 +513,16 @@ def trigger_coding_agent_handoff( repo_definitions: list[SeerRepoDefinition] = [] if features.has("organizations:seer-project-settings-read-from-sentry", group.organization): preference = read_preference_from_sentry_db(group.project) - if preference: - repo_definitions = preference.repositories - if preference.automation_handoff: - auto_create_pr = preference.automation_handoff.auto_create_pr + repo_definitions = preference.repositories + if preference.automation_handoff: + auto_create_pr = preference.automation_handoff.auto_create_pr else: try: - preference = get_project_seer_preferences(group.project_id).preference - if preference: - repo_definitions = preference.repositories - if preference.automation_handoff: - auto_create_pr = preference.automation_handoff.auto_create_pr + pref = get_project_seer_preferences(group.project_id).preference + if pref: + repo_definitions = pref.repositories + if pref.automation_handoff: + auto_create_pr = pref.automation_handoff.auto_create_pr except Exception: logger.exception( "autofix.coding_agent_handoff.get_preferences_error", diff --git a/src/sentry/seer/autofix/coding_agent.py b/src/sentry/seer/autofix/coding_agent.py index 5b99ea907cc4cc..76568a0e9eefef 100644 --- a/src/sentry/seer/autofix/coding_agent.py +++ b/src/sentry/seer/autofix/coding_agent.py @@ -237,7 +237,7 @@ def _launch_agents_for_repos( try: project = Project.objects.get_from_cache(id=autofix_state.request.project_id) preference = read_preference_from_sentry_db(project) - if preference and preference.automation_handoff: + if preference.automation_handoff: auto_create_pr = preference.automation_handoff.auto_create_pr except Project.DoesNotExist: logger.exception( @@ -250,9 +250,9 @@ def _launch_agents_for_repos( ) else: try: - preference = get_project_seer_preferences(autofix_state.request.project_id).preference - if preference and preference.automation_handoff: - auto_create_pr = preference.automation_handoff.auto_create_pr + pref = get_project_seer_preferences(autofix_state.request.project_id).preference + if pref and pref.automation_handoff: + auto_create_pr = pref.automation_handoff.auto_create_pr except (SeerApiError, SeerApiResponseValidationError): logger.exception( "coding_agent.get_project_seer_preferences_error", diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index 4ab3bb9039ae26..94bc9e0710c629 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -506,8 +506,7 @@ def get_automation_stopping_point(group: Group) -> AutofixStoppingPoint: fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score) if features.has("organizations:seer-project-settings-read-from-sentry", group.organization): - preference = read_preference_from_sentry_db(group.project) - user_preference = preference.automated_run_stopping_point if preference else None + user_preference = read_preference_from_sentry_db(group.project).automated_run_stopping_point else: user_preference = _fetch_user_preference(group.project.id) diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index 7ec8a3254943a9..46d9f00d6cecd6 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -490,16 +490,16 @@ def _get_handoff_config_if_applicable( # Check project preferences if features.has("organizations:seer-project-settings-read-from-sentry", group.organization): - preference = read_preference_from_sentry_db(group.project) - else: - try: - preference = get_project_seer_preferences(group.project_id).preference - except (SeerApiError, SeerApiResponseValidationError): - logger.exception( - "autofix.on_completion_hook.get_preferences_failed", - extra={"group_id": group.id, "project_id": group.project_id}, - ) - return None + return read_preference_from_sentry_db(group.project).automation_handoff + + try: + preference = get_project_seer_preferences(group.project_id).preference + except (SeerApiError, SeerApiResponseValidationError): + logger.exception( + "autofix.on_completion_hook.get_preferences_failed", + extra={"group_id": group.id, "project_id": group.project_id}, + ) + return None if not preference: return None @@ -510,6 +510,7 @@ def _clear_handoff_preference( cls, project: Project, run_id: int, organization: Organization ) -> None: """Clear automation_handoff from project preferences after integration is not found.""" + preference: SeerProjectPreference | None = None if features.has("organizations:seer-project-settings-read-from-sentry", organization): preference = read_preference_from_sentry_db(project) else: diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index 187411356c1474..dc5d72d7012433 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -734,7 +734,7 @@ def _build_automation_handoff( ) -def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference | None: +def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference: """Read a single project's Seer preferences from Sentry DB. For now, should only be used under feature flag `organizations:seer-project-settings-read-from-sentry`.""" @@ -749,12 +749,6 @@ def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference | if (repo_def := build_repo_definition_from_project_repo(project_repo)) is not None ] - has_configured_options = any( - ProjectOption.objects.isset(project, key) for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS - ) - if not repo_definitions and not has_configured_options: - return None - return SeerProjectPreference( organization_id=project.organization_id, project_id=project.id, @@ -767,7 +761,7 @@ def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference | def bulk_read_preferences_from_sentry_db( organization_id: int, project_ids: list[int] -) -> dict[int, SeerProjectPreference | None]: +) -> dict[int, SeerProjectPreference]: """Bulk read Seer preferences from Sentry DB. For now, should only be used under feature flag `organizations:seer-project-settings-read-from-sentry`.""" @@ -793,15 +787,8 @@ def bulk_read_preferences_from_sentry_db( for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS } - result: dict[int, SeerProjectPreference | None] = {} + result: dict[int, SeerProjectPreference] = {} for project in projects: - has_configured_options = any( - project_options[key][project.id] is not None - for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS - ) - if project.id not in repo_definitions_by_project and not has_configured_options: - result[project.id] = None - continue def _get_project_option(key: str) -> Any: value = project_options[key][project.id] @@ -831,7 +818,7 @@ def bulk_read_preferences( Always returns ``dict[int, SeerProjectPreference | None]`` regardless of the underlying read path (Sentry DB or Seer API).""" if features.has("organizations:seer-project-settings-read-from-sentry", organization): - return bulk_read_preferences_from_sentry_db(organization.id, project_ids) + return bulk_read_preferences_from_sentry_db(organization.id, project_ids) # type: ignore[return-value] raw = bulk_get_project_preferences(organization.id, project_ids) tuning_by_id = ProjectOption.objects.get_value_bulk_id( @@ -887,8 +874,7 @@ def has_project_connected_repos( has_repos = False if features.has("organizations:seer-project-settings-read-from-sentry", organization): - preference = read_preference_from_sentry_db(project) - has_repos = bool(preference and preference.repositories) + has_repos = bool(read_preference_from_sentry_db(project).repositories) else: try: preference = get_project_seer_preferences(project.id).preference diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 9d5a4c76cb3374..208f1dfbc2e8e7 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -626,6 +626,7 @@ def trigger_coding_agent_launch( if features.has("organizations:seer-project-settings-dual-write", organization): project = Project.objects.get_from_cache(id=project_id) + preference: SeerProjectPreference | None = None if features.has( "organizations:seer-project-settings-read-from-sentry", organization ): @@ -879,7 +880,7 @@ def get_project_preferences(*, organization_id: int, project_id: int) -> dict | """Get Seer project preferences for a single project. Raises Project.DoesNotExist if the project is not found or doesn't belong to the org. - Returns None if the project has no configured preferences. + Returns None if the project has no preference row in Seer DB. """ project = Project.objects.get_from_cache(id=project_id) if project.organization_id != organization_id: @@ -887,13 +888,10 @@ def get_project_preferences(*, organization_id: int, project_id: int) -> dict | organization = Organization.objects.get_from_cache(id=organization_id) if features.has("organizations:seer-project-settings-read-from-sentry", organization): - preference = read_preference_from_sentry_db(project) - else: - preference = get_project_seer_preferences(project_id).preference + return read_preference_from_sentry_db(project).dict() - if preference is None: - return None - return preference.dict() + preference = get_project_seer_preferences(project_id).preference + return preference.dict() if preference else None def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]) -> dict: @@ -909,8 +907,8 @@ def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int] str(project_id): preference.dict() if preference else None for project_id, preference in preferences.items() } - else: - return bulk_get_project_seer_preferences(organization_id, project_ids) + + return bulk_get_project_seer_preferences(organization_id, project_ids) seer_method_registry: dict[str, Callable] = { # return type must be serialized diff --git a/tests/sentry/seer/autofix/test_autofix.py b/tests/sentry/seer/autofix/test_autofix.py index 12518967915e56..4b6aa388bf6204 100644 --- a/tests/sentry/seer/autofix/test_autofix.py +++ b/tests/sentry/seer/autofix/test_autofix.py @@ -24,7 +24,12 @@ from sentry.seer.autofix.constants import AutofixReferrer from sentry.seer.autofix.types import AutofixSelectRootCausePayload from sentry.seer.explorer.utils import _convert_profile_to_execution_tree -from sentry.seer.models import SeerApiError, SeerProjectPreference, SeerRawPreferenceResponse +from sentry.seer.models import ( + SeerApiError, + SeerAutomationHandoffConfiguration, + SeerProjectPreference, + SeerRawPreferenceResponse, +) from sentry.seer.utils import get_github_username_for_user from sentry.testutils.cases import APITestCase, SnubaTestCase, TestCase from sentry.testutils.helpers.datetime import before_now @@ -1113,19 +1118,16 @@ def _mock_repo(self, name: str = "sentry", external_id: str = "123") -> dict: @patch("sentry.seer.autofix.autofix.set_project_seer_preference") @patch("sentry.seer.autofix.autofix.get_project_seer_preferences") def test_returns_existing_preference(self, mock_get_prefs, mock_set_pref, mock_write_sentry): - existing_repos = [self._mock_repo("seer", "999")] mock_get_prefs.return_value = SeerRawPreferenceResponse( preference=SeerProjectPreference( organization_id=self.organization.id, project_id=self.project.id, - repositories=existing_repos, + repositories=[self._mock_repo("seer", "999")], automated_run_stopping_point="root_cause", ) ) - result = _resolve_project_preference( - self.organization, self.project, [self._mock_repo("sentry", "123")] - ) + result = _resolve_project_preference(self.organization, self.project) assert result is not None assert len(result.repositories) == 1 @@ -1138,38 +1140,54 @@ def test_returns_existing_preference(self, mock_get_prefs, mock_set_pref, mock_w @patch("sentry.seer.autofix.autofix.write_preference_to_sentry_db") @patch("sentry.seer.autofix.autofix.set_project_seer_preference") @patch("sentry.seer.autofix.autofix.get_project_seer_preferences") - def test_creates_preference_from_code_mappings_and_org_defaults( + def test_returns_existing_preference_with_empty_repos( self, mock_get_prefs, mock_set_pref, mock_write_sentry ): - mock_get_prefs.return_value = SeerRawPreferenceResponse(preference=None) - self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") - self.organization.update_option("sentry:auto_open_prs", True) + mock_get_prefs.return_value = SeerRawPreferenceResponse( + preference=SeerProjectPreference( + organization_id=self.organization.id, + project_id=self.project.id, + repositories=[], + automated_run_stopping_point="root_cause", + automation_handoff=SeerAutomationHandoffConfiguration( + handoff_point="root_cause", + target="cursor_background_agent", + integration_id=42, + auto_create_pr=True, + ), + ) + ) - code_mapping_repos = [self._mock_repo("sentry", "123")] - result = _resolve_project_preference(self.organization, self.project, code_mapping_repos) + result = _resolve_project_preference(self.organization, self.project) assert result is not None - assert result.project_id == self.project.id - assert result.organization_id == self.organization.id - assert len(result.repositories) == 1 - assert result.repositories[0].name == "sentry" - assert result.repositories[0].external_id == "123" - assert result.automated_run_stopping_point == "open_pr" - mock_set_pref.assert_called_once() - mock_write_sentry.assert_called_once() + assert result.repositories == [] + assert result.automated_run_stopping_point == "root_cause" + assert result.automation_handoff is not None + assert result.automation_handoff.handoff_point == "root_cause" + assert result.automation_handoff.target == "cursor_background_agent" + assert result.automation_handoff.integration_id == 42 + assert result.automation_handoff.auto_create_pr is True + mock_set_pref.assert_not_called() + mock_write_sentry.assert_not_called() @patch("sentry.seer.autofix.autofix.write_preference_to_sentry_db") @patch("sentry.seer.autofix.autofix.set_project_seer_preference") @patch("sentry.seer.autofix.autofix.get_project_seer_preferences") - def test_creates_preference_with_empty_repos_when_no_fallback( + def test_no_preference_creates_one_with_org_defaults( self, mock_get_prefs, mock_set_pref, mock_write_sentry ): mock_get_prefs.return_value = SeerRawPreferenceResponse(preference=None) + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") + self.organization.update_option("sentry:auto_open_prs", True) - result = _resolve_project_preference(self.organization, self.project, []) + result = _resolve_project_preference(self.organization, self.project) assert result is not None + assert result.project_id == self.project.id + assert result.organization_id == self.organization.id assert result.repositories == [] + assert result.automated_run_stopping_point == "open_pr" mock_set_pref.assert_called_once() mock_write_sentry.assert_called_once() @@ -1179,7 +1197,7 @@ def test_creates_preference_with_empty_repos_when_no_fallback( def test_returns_none_on_get_api_error(self, mock_get_prefs, mock_set_pref, mock_write_sentry): mock_get_prefs.side_effect = SeerApiError("test error", 500) - result = _resolve_project_preference(self.organization, self.project, [self._mock_repo()]) + result = _resolve_project_preference(self.organization, self.project) assert result is None mock_set_pref.assert_not_called() @@ -1192,7 +1210,7 @@ def test_returns_none_on_set_api_error(self, mock_get_prefs, mock_set_pref, mock mock_get_prefs.return_value = SeerRawPreferenceResponse(preference=None) mock_set_pref.side_effect = SeerApiError("test error", 500) - result = _resolve_project_preference(self.organization, self.project, [self._mock_repo()]) + result = _resolve_project_preference(self.organization, self.project) assert result is None mock_write_sentry.assert_not_called() @@ -1206,37 +1224,14 @@ def test_returns_preference_on_sentry_db_write_error( mock_get_prefs.return_value = SeerRawPreferenceResponse(preference=None) mock_write_sentry.side_effect = Exception() - result = _resolve_project_preference(self.organization, self.project, [self._mock_repo()]) + result = _resolve_project_preference(self.organization, self.project) assert result is not None assert result.project_id == self.project.id + assert result.repositories == [] mock_set_pref.assert_called_once() mock_write_sentry.assert_called_once() - @patch("sentry.seer.autofix.autofix.write_preference_to_sentry_db") - @patch("sentry.seer.autofix.autofix.set_project_seer_preference") - @patch("sentry.seer.autofix.autofix.get_project_seer_preferences") - def test_returns_preference_with_empty_repos( - self, mock_get_prefs, mock_set_pref, mock_write_sentry - ): - mock_get_prefs.return_value = SeerRawPreferenceResponse( - preference=SeerProjectPreference( - organization_id=self.organization.id, - project_id=self.project.id, - repositories=[], - automated_run_stopping_point="root_cause", - ) - ) - - fallback_repos = [self._mock_repo("sentry", "123")] - result = _resolve_project_preference(self.organization, self.project, fallback_repos) - - assert result is not None - assert len(result.repositories) == 0 - assert result.automated_run_stopping_point == "root_cause" - mock_set_pref.assert_not_called() - mock_write_sentry.assert_not_called() - @with_feature("organizations:seer-project-settings-read-from-sentry") @patch("sentry.seer.autofix.autofix.write_preference_to_sentry_db") @patch("sentry.seer.autofix.autofix.set_project_seer_preference") @@ -1246,17 +1241,14 @@ def test_reads_from_sentry_db( self, mock_get_prefs, mock_read_db, mock_set_pref, mock_write_sentry ): """When feature flag enabled, reads preferences from Sentry DB instead of Seer API.""" - existing_repos = [self._mock_repo("seer", "999")] mock_read_db.return_value = SeerProjectPreference( organization_id=self.organization.id, project_id=self.project.id, - repositories=existing_repos, + repositories=[self._mock_repo("seer", "999")], automated_run_stopping_point="root_cause", ) - result = _resolve_project_preference( - self.organization, self.project, [self._mock_repo("sentry", "123")] - ) + result = _resolve_project_preference(self.organization, self.project) assert result is not None assert result.repositories[0].name == "seer" diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index ac2bde7dc05256..2466a0463f970d 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -1268,9 +1268,13 @@ def setUp(self): name="test-org/other-repo", ) - def test_unconfigured_project_returns_none(self): + def test_unconfigured_project_returns_default_preference(self): result = read_preference_from_sentry_db(self.project) - assert result is None + assert result is not None + assert result.repositories == [] + assert result.automated_run_stopping_point == "code_changes" + assert result.automation_handoff is None + assert result.autofix_automation_tuning == AutofixAutomationTuningSettings.OFF def test_project_with_repos_only(self): spr = SeerProjectRepository.objects.create( @@ -1470,9 +1474,14 @@ def test_empty_project_ids_returns_empty(self): result = bulk_read_preferences_from_sentry_db(self.organization.id, []) assert result == {} - def test_unconfigured_project_returns_none(self): + def test_unconfigured_project_returns_default_preference(self): result = bulk_read_preferences_from_sentry_db(self.organization.id, [self.project1.id]) - assert result == {self.project1.id: None} + pref = result[self.project1.id] + assert pref is not None + assert pref.repositories == [] + assert pref.automated_run_stopping_point == "code_changes" + assert pref.automation_handoff is None + assert pref.autofix_automation_tuning == AutofixAutomationTuningSettings.OFF def test_bulk_returns_correct_preferences(self): SeerProjectRepository.objects.create( @@ -1526,7 +1535,9 @@ def test_autofix_automation_tuning_populated(self): assert pref1 is not None assert pref1.autofix_automation_tuning == AutofixAutomationTuningSettings.HIGH - assert result[self.project2.id] is None + pref2 = result[self.project2.id] + assert pref2 is not None + assert pref2.autofix_automation_tuning == AutofixAutomationTuningSettings.OFF def test_autofix_automation_tuning_defaults_to_off(self): SeerProjectRepository.objects.create( diff --git a/tests/sentry/seer/endpoints/test_group_ai_autofix.py b/tests/sentry/seer/endpoints/test_group_ai_autofix.py index c6bad444847f48..7b182583580849 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/seer/endpoints/test_group_ai_autofix.py @@ -6,6 +6,7 @@ from sentry.seer.autofix.constants import AutofixReferrer, AutofixStatus from sentry.seer.autofix.utils import AutofixState, AutofixStoppingPoint, CodebaseState from sentry.seer.explorer.client_models import SeerRunState +from sentry.seer.models import SeerProjectPreference from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.features import with_feature @@ -315,7 +316,7 @@ def __init__(self): @patch("sentry.seer.autofix.autofix._call_autofix") @patch("sentry.seer.autofix.autofix._get_trace_tree_for_event") @patch("sentry.tasks.seer.autofix.check_autofix_status.apply_async") - @patch("sentry.seer.autofix.autofix._resolve_project_preference", return_value=None) + @patch("sentry.seer.autofix.autofix._resolve_project_preference") def test_ai_autofix_post_endpoint( self, mock_resolve_pref, @@ -331,14 +332,18 @@ def test_ai_autofix_post_endpoint( release = self.create_release(project=self.project, version="1.0.0") - repo = self.create_repo( - project=self.project, - name="getsentry/sentry", - provider="integrations:github", - external_id="123", - integration_id=234, + mock_resolve_pref.return_value = SeerProjectPreference( + organization_id=self.organization.id, + project_id=self.project.id, + repositories=[ + { + "provider": "integrations:github", + "owner": "getsentry", + "name": "sentry", + "external_id": "123", + } + ], ) - self.create_code_mapping(project=self.project, repo=repo) data = load_data("python", timestamp=before_now(minutes=1)) event = self.store_event( @@ -475,7 +480,7 @@ def test_ai_autofix_post_without_code_mappings( @patch("sentry.seer.autofix.autofix._call_autofix") @patch("sentry.seer.autofix.autofix._get_trace_tree_for_event") @patch("sentry.tasks.seer.autofix.check_autofix_status.apply_async") - @patch("sentry.seer.autofix.autofix._resolve_project_preference", return_value=None) + @patch("sentry.seer.autofix.autofix._resolve_project_preference") def test_ai_autofix_post_without_event_id( self, mock_resolve_pref, @@ -491,14 +496,18 @@ def test_ai_autofix_post_without_event_id( release = self.create_release(project=self.project, version="1.0.0") - repo = self.create_repo( - project=self.project, - name="getsentry/sentry", - provider="integrations:github", - external_id="123", - integration_id=234, + mock_resolve_pref.return_value = SeerProjectPreference( + organization_id=self.organization.id, + project_id=self.project.id, + repositories=[ + { + "provider": "integrations:github", + "owner": "getsentry", + "name": "sentry", + "external_id": "123", + } + ], ) - self.create_code_mapping(project=self.project, repo=repo) data = load_data("python", timestamp=before_now(minutes=1)) event = self.store_event( @@ -562,7 +571,7 @@ def test_ai_autofix_post_without_event_id( @patch("sentry.seer.autofix.autofix._call_autofix") @patch("sentry.seer.autofix.autofix._get_trace_tree_for_event") @patch("sentry.tasks.seer.autofix.check_autofix_status.apply_async") - @patch("sentry.seer.autofix.autofix._resolve_project_preference", return_value=None) + @patch("sentry.seer.autofix.autofix._resolve_project_preference") def test_ai_autofix_post_without_event_id_no_recommended_event( self, mock_resolve_pref, @@ -578,14 +587,18 @@ def test_ai_autofix_post_without_event_id_no_recommended_event( release = self.create_release(project=self.project, version="1.0.0") - repo = self.create_repo( - project=self.project, - name="getsentry/sentry", - provider="integrations:github", - external_id="123", - integration_id=234, + mock_resolve_pref.return_value = SeerProjectPreference( + organization_id=self.organization.id, + project_id=self.project.id, + repositories=[ + { + "provider": "integrations:github", + "owner": "getsentry", + "name": "sentry", + "external_id": "123", + } + ], ) - self.create_code_mapping(project=self.project, repo=repo) data = load_data("python", timestamp=before_now(minutes=1)) event = self.store_event( diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 697fb232c9f095..c0ce038d23c475 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1547,15 +1547,17 @@ def test_get_project_preferences_returns_preference(self, mock_read: Any) -> Non mock_read.assert_called_once() @with_feature("organizations:seer-project-settings-read-from-sentry") - @patch("sentry.seer.endpoints.seer_rpc.read_preference_from_sentry_db") - def test_get_project_preferences_returns_none_when_no_preference(self, mock_read: Any) -> None: + def test_get_project_preferences_returns_default_when_no_preference(self) -> None: project = self.create_project(organization=self.organization) - mock_read.return_value = None result = get_project_preferences( - organization_id=self.organization.id, - project_id=project.id, - ) - assert result is None + organization_id=self.organization.id, project_id=project.id + ) + assert result is not None + assert result["project_id"] == project.id + assert result["organization_id"] == self.organization.id + assert result["repositories"] == [] + assert result["automated_run_stopping_point"] == "code_changes" + assert result["automation_handoff"] is None def test_get_project_preferences_raises_for_nonexistent_project(self) -> None: with pytest.raises(Project.DoesNotExist):