From 1de21d9c5dc976562603a0abb86e1862a10fe08b Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 14:14:55 -0700 Subject: [PATCH 1/3] ref(repositories): Remove feature flag branching for SeerProjectRepository reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the `organizations:project-repository-fk-reads` flag checks from all SeerProjectRepository read/write paths and related code mapping accesses in Seer code. Always use `project_repository` FK for queries and attribute access. The flag registration is left in place — it will be removed in a follow-up PR that also drops the old model columns. --- .../integrations/services/repository/impl.py | 17 +-- src/sentry/seer/autofix/autofix.py | 10 +- src/sentry/seer/autofix/utils.py | 128 ++++++------------ .../seer/code_review/contributor_seats.py | 15 +- src/sentry/seer/endpoints/group_ai_autofix.py | 7 +- .../organization_seer_onboarding_check.py | 12 +- .../seer/fetch_issues/by_function_name.py | 23 +--- src/sentry/seer/fetch_issues/utils.py | 29 +--- src/sentry/tasks/seer/night_shift/cron.py | 10 +- .../tasks/seer/night_shift/triage_tools.py | 21 +-- src/sentry/testutils/factories.py | 4 - src/sentry/testutils/helpers/backups.py | 4 +- .../sentry/seer/autofix/test_autofix_utils.py | 68 +++++----- .../code_review/test_contributor_seats.py | 3 +- ...rganization_autofix_automation_settings.py | 74 +++++++--- .../test_project_seer_preferences.py | 63 ++++++--- tests/sentry/tasks/seer/test_autofix.py | 4 +- 17 files changed, 217 insertions(+), 275 deletions(-) diff --git a/src/sentry/integrations/services/repository/impl.py b/src/sentry/integrations/services/repository/impl.py index 6a155731f2e6ed..2347c0aa6d1348 100644 --- a/src/sentry/integrations/services/repository/impl.py +++ b/src/sentry/integrations/services/repository/impl.py @@ -6,7 +6,6 @@ from django.db import IntegrityError, router, transaction from django.utils import timezone -from sentry import features from sentry.api.serializers import serialize from sentry.constants import ObjectStatus from sentry.db.postgres.transactions import enforce_constraints @@ -18,7 +17,6 @@ from sentry.models.code_review_event import CodeReviewEvent from sentry.models.commit import Commit from sentry.models.options.project_option import ProjectOption -from sentry.models.organization import Organization from sentry.models.projectcodeowners import ProjectCodeOwners from sentry.models.projectrepository import ProjectRepository from sentry.models.pullrequest import PullRequest @@ -244,17 +242,10 @@ def disassociate_organization_integration( Repository.objects.filter(id__in=repo_ids).update(integration_id=None) # Delete Seer preferences for this repository - org = Organization.objects.get(id=organization_id) - if features.has("organizations:project-repository-fk-reads", org): - SeerProjectRepository.objects.filter( - project_repository__repository_id__in=repo_ids, - project_repository__project__organization_id=organization_id, - ).delete() - else: - SeerProjectRepository.objects.filter( - repository_id__in=repo_ids, - project__organization_id=organization_id, - ).delete() + SeerProjectRepository.objects.filter( + project_repository__repository_id__in=repo_ids, + project_repository__project__organization_id=organization_id, + ).delete() # Delete Code Owners with a Code Mapping using the OrganizationIntegration ProjectCodeOwners.objects.filter( diff --git a/src/sentry/seer/autofix/autofix.py b/src/sentry/seer/autofix/autofix.py index 925d8584a9b465..fa18a776b783c6 100644 --- a/src/sentry/seer/autofix/autofix.py +++ b/src/sentry/seer/autofix/autofix.py @@ -189,7 +189,6 @@ def _get_serialized_event( def _pre_resolve_stacktrace_frames( serialized_event: dict[str, Any], code_mappings: list[RepositoryProjectPathConfig], - use_project_repository_fk: bool = False, ) -> None: """ Pre-resolve stacktrace frame repo_name and filename using Sentry's code mappings @@ -204,7 +203,7 @@ def _pre_resolve_stacktrace_frames( # Build ordered list of (repo_full_name, code_mapping) preserving global priority ordered_mappings: list[tuple[str, RepositoryProjectPathConfig]] = [] for cm in code_mappings: - repo = cm.project_repository.repository if use_project_repository_fk else cm.repository + repo = cm.project_repository.repository repo_name_sections = repo.name.split("/") if len(repo_name_sections) > 1 and repo.provider: ordered_mappings.append((repo.name, cm)) @@ -730,12 +729,7 @@ def trigger_legacy_autofix( # Pre-resolve stacktrace frame paths using code mappings so Seer can skip # expensive git tree fetches for large repos. try: - use_fk = features.has( - "organizations:project-repository-fk-reads", group.project.organization - ) - _pre_resolve_stacktrace_frames( - serialized_event, code_mappings, use_project_repository_fk=use_fk - ) + _pre_resolve_stacktrace_frames(serialized_event, code_mappings) except Exception: logger.exception("Failed to pre-resolve stacktrace frames") diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index 61f4669bf0b129..fece5e932e6276 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -463,18 +463,10 @@ def _write_preferences_to_sentry_db( list(Project.objects.select_for_update().filter(id__in=project_ids).order_by("id")) # Only delete SeerProjectRepository for active repos. - if features.has( - "organizations:project-repository-fk-reads", - project_preferences[0][0].organization, - ): - SeerProjectRepository.objects.filter( - project_repository__project_id__in=project_ids, - project_repository__repository__status=ObjectStatus.ACTIVE, - ).delete() - else: - SeerProjectRepository.objects.filter( - project_id__in=project_ids, repository__status=ObjectStatus.ACTIVE - ).delete() + SeerProjectRepository.objects.filter( + project_repository__project_id__in=project_ids, + project_repository__repository__status=ObjectStatus.ACTIVE, + ).delete() all_repo_ids = { repo_def.repository_id @@ -493,7 +485,7 @@ def _write_preferences_to_sentry_db( ) # Collect project repos to create. - project_repos_to_create: list[SeerProjectRepository] = [] + project_repos_to_create: list[tuple[Project, int, SeerProjectRepository]] = [] overrides_by_key: dict[tuple[int, int], list[BranchOverride]] = {} for project, pref in project_preferences: for repo_def in pref.repositories: @@ -514,11 +506,13 @@ def _write_preferences_to_sentry_db( continue project_repos_to_create.append( - SeerProjectRepository( - project=project, - repository_id=repo_def.repository_id, - branch_name=repo_def.branch_name, - instructions=repo_def.instructions, + ( + project, + repo_def.repository_id, + SeerProjectRepository( + branch_name=repo_def.branch_name, + instructions=repo_def.instructions, + ), ) ) @@ -528,27 +522,23 @@ def _write_preferences_to_sentry_db( ) if project_repos_to_create: - for spr in project_repos_to_create: + for project, repository_id, spr in project_repos_to_create: project_repo, _ = ProjectRepository.objects.get_or_create( - project=spr.project, - repository_id=spr.repository_id, + project=project, + repository_id=repository_id, defaults={"source": ProjectRepositorySource.SEER_PREFERENCE}, ) spr.project_repository = project_repo created_project_repos = SeerProjectRepository.objects.bulk_create( - project_repos_to_create + [spr for _, _, spr in project_repos_to_create] ) # Create branch overrides using the created project repos. overrides_to_create: list[SeerProjectRepositoryBranchOverride] = [] for seer_project_repo in created_project_repos: pr = seer_project_repo.project_repository - key = ( - (pr.project_id, pr.repository_id) - if pr is not None - else (seer_project_repo.project_id, seer_project_repo.repository_id) - ) + key = (pr.project_id, pr.repository_id) for override in overrides_by_key.get(key, []): overrides_to_create.append( SeerProjectRepositoryBranchOverride( @@ -608,16 +598,11 @@ def clear_preference_automation_handoff(project: Project) -> None: def build_repo_definition_from_project_repo( seer_project_repo: SeerProjectRepository, - use_project_repository_fk: bool = False, ) -> SeerRepoDefinition | None: """Build a SeerRepoDefinition from a SeerProjectRepository with its joined Repository. Returns None if Repository name is invalid.""" - if use_project_repository_fk: - pr = seer_project_repo.project_repository - repo = pr.repository if pr is not None else seer_project_repo.repository - else: - repo = seer_project_repo.repository + repo = seer_project_repo.project_repository.repository repo_name_sections = repo.name.split("/") if len(repo_name_sections) < 2: sentry_sdk.capture_exception(ValueError(f"Invalid repository name format: {repo.name}")) @@ -665,33 +650,18 @@ def build_automation_handoff( def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference: """Read a single project's Seer preferences from Sentry DB.""" - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - if use_fk: - seer_project_repo_qs = ( - SeerProjectRepository.objects.filter( - project_repository__project=project, - project_repository__repository__status=ObjectStatus.ACTIVE, - ) - .select_related("project_repository", "project_repository__repository") - .prefetch_related("branch_overrides") - ) - else: - seer_project_repo_qs = ( - SeerProjectRepository.objects.filter( - project=project, repository__status=ObjectStatus.ACTIVE - ) - .select_related("repository", "project_repository", "project_repository__repository") - .prefetch_related("branch_overrides") + seer_project_repo_qs = ( + SeerProjectRepository.objects.filter( + project_repository__project=project, + project_repository__repository__status=ObjectStatus.ACTIVE, ) + .select_related("project_repository", "project_repository__repository") + .prefetch_related("branch_overrides") + ) repo_definitions = [ repo_def for project_repo in seer_project_repo_qs - if ( - repo_def := build_repo_definition_from_project_repo( - project_repo, use_project_repository_fk=use_fk - ) - ) - is not None + if (repo_def := build_repo_definition_from_project_repo(project_repo)) is not None ] return SeerProjectPreference( @@ -713,29 +683,19 @@ def bulk_read_preferences_from_sentry_db( projects = list(Project.objects.filter(id__in=project_ids, organization_id=organization_id)) - org = Organization.objects.get(id=organization_id) repo_definitions_by_project: defaultdict[int, list[SeerRepoDefinition]] = defaultdict(list) - use_fk = features.has("organizations:project-repository-fk-reads", org) - if use_fk: - seer_repo_qs = SeerProjectRepository.objects.filter( + seer_repo_qs = ( + SeerProjectRepository.objects.filter( project_repository__project_id__in=project_ids, project_repository__repository__status=ObjectStatus.ACTIVE, - ).select_related("project_repository", "project_repository__repository") - else: - seer_repo_qs = SeerProjectRepository.objects.filter( - project_id__in=project_ids, repository__status=ObjectStatus.ACTIVE - ).select_related("repository", "project_repository", "project_repository__repository") - for seer_repo in seer_repo_qs.prefetch_related("branch_overrides"): - repo_def = build_repo_definition_from_project_repo( - seer_repo, use_project_repository_fk=use_fk ) + .select_related("project_repository", "project_repository__repository") + .prefetch_related("branch_overrides") + ) + for seer_repo in seer_repo_qs: + repo_def = build_repo_definition_from_project_repo(seer_repo) if repo_def is not None: - if use_fk: - pr = seer_repo.project_repository - pid = pr.project_id if pr is not None else seer_repo.project_id - else: - pid = seer_repo.project_id - repo_definitions_by_project[pid].append(repo_def) + repo_definitions_by_project[seer_repo.project_repository.project_id].append(repo_def) # get_value_bulk_id returns None for missing options, unlike project.get_option # which automatically falls back to the registered well-known key default. @@ -844,18 +804,11 @@ def _set_if_not_default(key: str, value: Any, default: Any) -> None: def has_project_connected_repos(organization: Organization, project: Project) -> bool: """Check if a project has connected repositories for Seer automation.""" - if features.has("organizations:project-repository-fk-reads", organization): - return SeerProjectRepository.objects.filter( - project_repository__project=project, - project_repository__project__organization_id=organization.id, - project_repository__project__status=ObjectStatus.ACTIVE, - project_repository__repository__status=ObjectStatus.ACTIVE, - ).exists() return SeerProjectRepository.objects.filter( - project=project, - project__organization_id=organization.id, - project__status=ObjectStatus.ACTIVE, - repository__status=ObjectStatus.ACTIVE, + project_repository__project=project, + project_repository__project__organization_id=organization.id, + project_repository__project__status=ObjectStatus.ACTIVE, + project_repository__repository__status=ObjectStatus.ACTIVE, ).exists() @@ -870,12 +823,9 @@ def get_autofix_repos_from_project_code_mappings( if code_mappings is None: code_mappings = get_sorted_code_mapping_configs(project) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) repos: dict[tuple, dict] = {} for code_mapping in code_mappings: - repo: Repository = ( - code_mapping.project_repository.repository if use_fk else code_mapping.repository - ) + repo: Repository = code_mapping.project_repository.repository repo_name_sections = repo.name.split("/") if ( diff --git a/src/sentry/seer/code_review/contributor_seats.py b/src/sentry/seer/code_review/contributor_seats.py index 42db8e6abc09a3..2617aa7a9ae7b3 100644 --- a/src/sentry/seer/code_review/contributor_seats.py +++ b/src/sentry/seer/code_review/contributor_seats.py @@ -40,18 +40,11 @@ def _is_autofix_enabled_for_repo(organization: Organization, repository_id: int) this repository, ie, if any project has this repository configured in Seer preferences. """ - if features.has("organizations:project-repository-fk-reads", organization): - return SeerProjectRepository.objects.filter( - project_repository__repository_id=repository_id, - project_repository__project__organization_id=organization.id, - project_repository__project__status=ObjectStatus.ACTIVE, - project_repository__repository__status=ObjectStatus.ACTIVE, - ).exists() return SeerProjectRepository.objects.filter( - repository_id=repository_id, - project__organization_id=organization.id, - project__status=ObjectStatus.ACTIVE, - repository__status=ObjectStatus.ACTIVE, + project_repository__repository_id=repository_id, + project_repository__project__organization_id=organization.id, + project_repository__project__status=ObjectStatus.ACTIVE, + project_repository__repository__status=ObjectStatus.ACTIVE, ).exists() diff --git a/src/sentry/seer/endpoints/group_ai_autofix.py b/src/sentry/seer/endpoints/group_ai_autofix.py index 552dd1c3ffd417..89fddc0bd50a85 100644 --- a/src/sentry/seer/endpoints/group_ai_autofix.py +++ b/src/sentry/seer/endpoints/group_ai_autofix.py @@ -490,8 +490,9 @@ def _get_legacy(self, request: Request, group: Group) -> Response: if project: code_mappings = get_sorted_code_mapping_configs(project=project) for mapping in code_mappings: - if mapping.repository.external_id: - repo_code_mappings[mapping.repository.external_id] = mapping + repo = mapping.project_repository.repository + if repo.external_id: + repo_code_mappings[repo.external_id] = mapping for repo_external_id, repo_state in autofix_codebase_state.items(): retrieved_mapping: RepositoryProjectPathConfig | None = repo_code_mappings.get( @@ -501,7 +502,7 @@ def _get_legacy(self, request: Request, group: Group) -> Response: if not retrieved_mapping: continue - mapping_repo: Repository = retrieved_mapping.repository + mapping_repo: Repository = retrieved_mapping.project_repository.repository repositories.append( { diff --git a/src/sentry/seer/endpoints/organization_seer_onboarding_check.py b/src/sentry/seer/endpoints/organization_seer_onboarding_check.py index c911dfa26222fb..9be01028dfac65 100644 --- a/src/sentry/seer/endpoints/organization_seer_onboarding_check.py +++ b/src/sentry/seer/endpoints/organization_seer_onboarding_check.py @@ -56,16 +56,10 @@ def is_autofix_enabled(organization: Organization) -> bool: Check if autofix/RCA is enabled for any active project in the organization, ie, if any project has repositories configured in Seer preferences. """ - if features.has("organizations:project-repository-fk-reads", organization): - return SeerProjectRepository.objects.filter( - project_repository__project__organization_id=organization.id, - project_repository__project__status=ObjectStatus.ACTIVE, - project_repository__repository__status=ObjectStatus.ACTIVE, - ).exists() return SeerProjectRepository.objects.filter( - project__organization_id=organization.id, - project__status=ObjectStatus.ACTIVE, - repository__status=ObjectStatus.ACTIVE, + project_repository__project__organization_id=organization.id, + project_repository__project__status=ObjectStatus.ACTIVE, + project_repository__repository__status=ObjectStatus.ACTIVE, ).exists() diff --git a/src/sentry/seer/fetch_issues/by_function_name.py b/src/sentry/seer/fetch_issues/by_function_name.py index 4fac016e9950e1..947a9021b00a98 100644 --- a/src/sentry/seer/fetch_issues/by_function_name.py +++ b/src/sentry/seer/fetch_issues/by_function_name.py @@ -8,10 +8,8 @@ from snuba_sdk import BooleanCondition, BooleanOp, Column, Condition, Entity, Function, Op, Query from snuba_sdk import Request as SnubaRequest -from sentry import features from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.models.group import Group, GroupStatus -from sentry.models.organization import Organization from sentry.models.project import Project from sentry.seer.constants import SeerSCMProvider from sentry.seer.fetch_issues import utils @@ -208,25 +206,14 @@ def _get_projects_and_filenames_from_source_file( org_id: int, repo_id: int, pr_filename: str, max_num_left_truncated_paths: int = 2 ) -> tuple[set[Project], set[str]]: # Fetch the code mappings in which the source_root is a substring at the start of pr_filename - org = Organization.objects.get(id=org_id) - use_fk = features.has("organizations:project-repository-fk-reads", org) - if use_fk: - base_qs = RepositoryProjectPathConfig.objects.filter( - organization_id=org_id, - project_repository__repository_id=repo_id, - ).select_related("project", "project_repository__project") - else: - base_qs = RepositoryProjectPathConfig.objects.filter( - organization_id=org_id, - repository_id=repo_id, - ).select_related("project") + base_qs = RepositoryProjectPathConfig.objects.filter( + organization_id=org_id, + project_repository__repository_id=repo_id, + ).select_related("project_repository__project") code_mappings = base_qs.annotate( substring_match=StrIndex(Value(pr_filename), "source_root") ).filter(substring_match=1) - if use_fk: - projects_set = {cm.project_repository.project for cm in code_mappings} - else: - projects_set = {cm.project for cm in code_mappings} + projects_set = {cm.project_repository.project for cm in code_mappings} sentry_filenames = { pr_filename.replace(code_mapping.source_root, code_mapping.stack_root, 1) for code_mapping in code_mappings diff --git a/src/sentry/seer/fetch_issues/utils.py b/src/sentry/seer/fetch_issues/utils.py index 5b8e601c13e358..ba59f106c39057 100644 --- a/src/sentry/seer/fetch_issues/utils.py +++ b/src/sentry/seer/fetch_issues/utils.py @@ -6,12 +6,10 @@ import sentry_sdk -from sentry import features from sentry.api.serializers import serialize from sentry.api.serializers.models.event import EventSerializer from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.models.group import Group -from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.repository import Repository from sentry.seer.constants import SeerSCMProvider @@ -91,31 +89,18 @@ def get_repo_and_projects( repo = filter_repo_by_provider(organization_id, provider, external_id, owner, name).first() if repo is None: raise Repository.DoesNotExist - org = Organization.objects.get(id=organization_id) - use_fk = features.has("organizations:project-repository-fk-reads", org) - if use_fk: - repo_configs = list( - RepositoryProjectPathConfig.objects.filter( - organization_id=organization_id, - project_repository__repository_id=repo.id, - ).select_related("project", "project_repository__project") - ) - else: - repo_configs = list( - RepositoryProjectPathConfig.objects.filter( - organization_id=organization_id, - repository_id=repo.id, - ).select_related("project", "project_repository__project") - ) + repo_configs = list( + RepositoryProjectPathConfig.objects.filter( + organization_id=organization_id, + project_repository__repository_id=repo.id, + ).select_related("project_repository__project") + ) projects = [] valid_configs = [] for config in repo_configs: try: - if use_fk: - project = config.project_repository.project - else: - project = config.project + project = config.project_repository.project except Project.DoesNotExist: continue else: diff --git a/src/sentry/tasks/seer/night_shift/cron.py b/src/sentry/tasks/seer/night_shift/cron.py index 8c62f3dfd062a1..7f19eea057b4fc 100644 --- a/src/sentry/tasks/seer/night_shift/cron.py +++ b/src/sentry/tasks/seer/night_shift/cron.py @@ -118,14 +118,12 @@ def schedule_night_shift( seer_org_ids: set[int] = set() for spr in RangeQuerySetWrapper[SeerProjectRepository]( - SeerProjectRepository.objects.filter(project__status=ObjectStatus.ACTIVE).select_related( - "project", "project_repository__project" - ), + SeerProjectRepository.objects.filter( + project_repository__project__status=ObjectStatus.ACTIVE + ).select_related("project_repository__project"), step=1000, ): - pr = spr.project_repository - project = pr.project if pr is not None else spr.project - seer_org_ids.add(project.organization_id) + seer_org_ids.add(spr.project_repository.project.organization_id) logger.info( "night_shift.schedule_org_ids_collected", diff --git a/src/sentry/tasks/seer/night_shift/triage_tools.py b/src/sentry/tasks/seer/night_shift/triage_tools.py index 75b8cc1e980c77..fb429291b917ab 100644 --- a/src/sentry/tasks/seer/night_shift/triage_tools.py +++ b/src/sentry/tasks/seer/night_shift/triage_tools.py @@ -3,7 +3,6 @@ from django.core.exceptions import BadRequest from pydantic import BaseModel, Field -from sentry import features from sentry.integrations.models.repository_project_path_config import ( RepositoryProjectPathConfig, ) @@ -200,22 +199,14 @@ def _format_linked_repos(project_id: int, organization: Organization) -> str: agent doesn't have to guess it from the project slug. Includes source_root because many repos place app code under a subdirectory (e.g. `python/`). """ - use_fk = features.has("organizations:project-repository-fk-reads", organization) - if use_fk: - configs = ( - RepositoryProjectPathConfig.objects.filter(project_repository__project_id=project_id) - .select_related("project_repository__repository") - .order_by("id") - ) - else: - configs = ( - RepositoryProjectPathConfig.objects.filter(project_id=project_id) - .select_related("repository") - .order_by("id") - ) + configs = ( + RepositoryProjectPathConfig.objects.filter(project_repository__project_id=project_id) + .select_related("project_repository__repository") + .order_by("id") + ) lines: list[str] = [] for cfg in configs: - repo_name = cfg.project_repository.repository.name if use_fk else cfg.repository.name + repo_name = cfg.project_repository.repository.name source_root = cfg.source_root or "" stack_root = cfg.stack_root or "" parts = [f"- {repo_name}"] diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index 62e99f8573fbdc..88dd6b8e385355 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -899,8 +899,6 @@ def create_code_mapping(project, repo=None, organization_integration=None, **kwa ) kwargs["project_repository"] = project_repo return RepositoryProjectPathConfig.objects.create( - project=project, - repository=repo, organization_integration_id=organization_integration.id, integration_id=organization_integration.integration_id, organization_id=organization_integration.organization_id, @@ -919,8 +917,6 @@ def create_seer_project_repository(project, repository=None, repository_id=None, defaults={"source": ProjectRepositorySource.SEER_PREFERENCE}, ) return SeerProjectRepository.objects.create( - project=project, - repository=repository, project_repository=project_repo, **kwargs, ) diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 8abfbebf44cfc3..8d5ed57bd29759 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -630,9 +630,7 @@ def create_exhaustive_organization( repository=repo, defaults={"source": ProjectRepositorySource.MANUAL}, ) - seer_project_repo = SeerProjectRepository.objects.create( - project=project, repository=repo, project_repository=project_repo - ) + seer_project_repo = SeerProjectRepository.objects.create(project_repository=project_repo) SeerProjectRepositoryBranchOverride.objects.create( seer_project_repository=seer_project_repo, tag_name="environment", diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index bbb99e1a5622d4..f470f6408ae3cd 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -527,11 +527,10 @@ def test_returns_true_via_project_repository_fk(self): ) pr = ProjectRepository.objects.create(project=self.project, repository=repo) SeerProjectRepository.objects.create( - project=self.project, repository=repo, project_repository=pr + project_repository=pr, ) - with self.feature("organizations:project-repository-fk-reads"): - assert has_project_connected_repos(self.organization, self.project) is True + assert has_project_connected_repos(self.organization, self.project) is True class TestDeduplicateRepositories(TestCase): @@ -669,7 +668,10 @@ def test_writes_project_options(self) -> None: ) assert self.project.get_option("sentry:seer_automation_handoff_integration_id") == 42 assert self.project.get_option("sentry:seer_automation_handoff_auto_create_pr") is True - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) def test_deletes_project_options_when_defaults(self) -> None: preference = SeerProjectPreference( @@ -715,8 +717,8 @@ def test_creates_seer_project_repository_with_branch_overrides(self) -> None: write_preference_to_sentry_db(self.project, preference) - seer_repo = SeerProjectRepository.objects.get(project=self.project) - assert seer_repo.repository_id == self.repo.id + seer_repo = SeerProjectRepository.objects.get(project_repository__project=self.project) + assert seer_repo.project_repository.repository_id == self.repo.id assert seer_repo.branch_name == "develop" assert seer_repo.instructions == "Use conventional commits" assert seer_repo.project_repository is not None @@ -756,7 +758,10 @@ def test_replaces_existing_preference_on_write(self) -> None: ) write_preference_to_sentry_db(self.project, preference_to_replace) - assert SeerProjectRepository.objects.filter(project=self.project).count() == 1 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 1 + ) assert ( SeerProjectRepositoryBranchOverride.objects.filter( seer_project_repository__project=self.project @@ -786,9 +791,9 @@ def test_replaces_existing_preference_on_write(self) -> None: ) write_preference_to_sentry_db(self.project, new_preference) - repos = SeerProjectRepository.objects.filter(project=self.project) + repos = SeerProjectRepository.objects.filter(project_repository__project=self.project) assert len(repos) == 1 - assert repos[0].repository_id == repo2.id + assert repos[0].project_repository.repository_id == repo2.id assert ( SeerProjectRepositoryBranchOverride.objects.filter( seer_project_repository__project=self.project @@ -829,11 +834,13 @@ def test_multiple_repos_for_one_project(self) -> None: write_preference_to_sentry_db(self.project, preference) - repos = SeerProjectRepository.objects.filter(project=self.project).order_by("repository_id") + repos = SeerProjectRepository.objects.filter( + project_repository__project=self.project + ).order_by("project_repository__repository_id") assert len(repos) == 2 - assert repos[0].repository_id == self.repo.id + assert repos[0].project_repository.repository_id == self.repo.id assert repos[0].branch_name == "develop" - assert repos[1].repository_id == repo2.id + assert repos[1].project_repository.repository_id == repo2.id assert repos[1].instructions == "Deploy carefully" def test_preserves_existing_seer_project_repository_for_inactive_repo(self) -> None: @@ -870,11 +877,11 @@ def test_preserves_existing_seer_project_repository_for_inactive_repo(self) -> N ) write_preference_to_sentry_db(self.project, new_preference) - project_repos = SeerProjectRepository.objects.filter(project=self.project).order_by( - "repository_id" - ) + project_repos = SeerProjectRepository.objects.filter( + project_repository__project=self.project + ).order_by("repository_id") assert len(project_repos) == 2 - project_repos_by_repo_id = {r.repository_id: r for r in project_repos} + project_repos_by_repo_id = {r.project_repository.repository_id: r for r in project_repos} assert project_repos_by_repo_id[disabled_repo.id].branch_name == "branch-1" assert project_repos_by_repo_id[disabled_repo.id].instructions == "kept across writes" assert project_repos_by_repo_id[self.repo.id].branch_name == "branch-2" @@ -908,9 +915,9 @@ def test_skips_new_seer_project_repository_for_inactive_repo(self) -> None: ) write_preference_to_sentry_db(self.project, new_preference) - repos = list(SeerProjectRepository.objects.filter(project=self.project)) + repos = list(SeerProjectRepository.objects.filter(project_repository__project=self.project)) assert len(repos) == 1 - assert repos[0].repository_id == self.repo.id + assert repos[0].project_repository.repository_id == self.repo.id assert repos[0].branch_name == "original" assert repos[0].instructions == "original instructions" @@ -958,15 +965,15 @@ def test_bulk_write_multiple_projects(self) -> None: bulk_write_preferences_to_sentry_db([self.project, project2], preferences) - p1_repos = SeerProjectRepository.objects.filter(project=self.project) + p1_repos = SeerProjectRepository.objects.filter(project_repository__project=self.project) assert len(p1_repos) == 1 - assert p1_repos[0].repository_id == self.repo.id + assert p1_repos[0].project_repository.repository_id == self.repo.id assert p1_repos[0].branch_name == "develop" assert self.project.get_option("sentry:seer_automated_run_stopping_point") == "open_pr" - p2_repos = SeerProjectRepository.objects.filter(project=project2) + p2_repos = SeerProjectRepository.objects.filter(project_repository__project=project2) assert len(p2_repos) == 1 - assert p2_repos[0].repository_id == repo2.id + assert p2_repos[0].project_repository.repository_id == repo2.id assert p2_repos[0].instructions == "Be careful" assert project2.get_option("sentry:seer_automated_run_stopping_point") == "code_changes" @@ -1008,9 +1015,9 @@ def test_bulk_write_replaces_per_project(self) -> None: # When bulk writing, existing repos for included projects are replaced, # but repos for projects NOT in the preferences list are untouched. - p1_repo = SeerProjectRepository.objects.get(project=self.project) + p1_repo = SeerProjectRepository.objects.get(project_repository__project=self.project) assert p1_repo.branch_name == "new-branch" - p2_repo = SeerProjectRepository.objects.get(project=project2) + p2_repo = SeerProjectRepository.objects.get(project_repository__project=project2) assert p2_repo.branch_name == "project-2-branch" @@ -1066,8 +1073,8 @@ def test_preserves_unrelated_preference_fields(self) -> None: assert self.project.get_option("sentry:seer_automated_run_stopping_point") == "open_pr" assert self.project.get_option("sentry:autofix_automation_tuning") == "high" - seer_repo = SeerProjectRepository.objects.get(project=self.project) - assert seer_repo.repository_id == self.repo.id + seer_repo = SeerProjectRepository.objects.get(project_repository__project=self.project) + assert seer_repo.project_repository.repository_id == self.repo.id assert seer_repo.branch_name == "develop" assert seer_repo.instructions == "Use conventional commits" @@ -1153,16 +1160,13 @@ def test_project_with_repos_only(self): def test_reads_via_project_repository_fk(self): pr = ProjectRepository.objects.create(project=self.project, repository=self.repo) SeerProjectRepository.objects.create( - project=self.project, - repository=self.repo, project_repository=pr, branch_name="main", ) - with self.feature("organizations:project-repository-fk-reads"): - result = read_preference_from_sentry_db(self.project) - assert len(result.repositories) == 1 - assert result.repositories[0].branch_name == "main" + result = read_preference_from_sentry_db(self.project) + assert len(result.repositories) == 1 + assert result.repositories[0].branch_name == "main" def test_autofix_automation_tuning_default(self): self.create_seer_project_repository( diff --git a/tests/sentry/seer/code_review/test_contributor_seats.py b/tests/sentry/seer/code_review/test_contributor_seats.py index eec5f042d3160e..1af02284e93978 100644 --- a/tests/sentry/seer/code_review/test_contributor_seats.py +++ b/tests/sentry/seer/code_review/test_contributor_seats.py @@ -70,8 +70,7 @@ def test_repo_is_inactive(self) -> None: def test_returns_true_via_project_repository_fk(self) -> None: self.create_seer_project_repository(project=self.project, repository=self.repo) - with self.feature("organizations:project-repository-fk-reads"): - assert _is_autofix_enabled_for_repo(self.organization, self.repo.id) is True + assert _is_autofix_enabled_for_repo(self.organization, self.repo.id) is True class ShouldIncrementContributorSeatTest(TestCase): diff --git a/tests/sentry/seer/endpoints/test_organization_autofix_automation_settings.py b/tests/sentry/seer/endpoints/test_organization_autofix_automation_settings.py index 9d4baf84289e70..a32e53683f338f 100644 --- a/tests/sentry/seer/endpoints/test_organization_autofix_automation_settings.py +++ b/tests/sentry/seer/endpoints/test_organization_autofix_automation_settings.py @@ -177,7 +177,9 @@ def test_post_creates_project_preferences(self): project.get_option("sentry:seer_automated_run_stopping_point") == AutofixStoppingPoint.OPEN_PR.value ) - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) def test_post_updates_each_preference_field_independently(self): project = self.create_project(organization=self.organization) @@ -330,8 +332,12 @@ def test_post_ignores_repo_mappings_not_in_project_ids(self): ) assert response.status_code == 204 - assert SeerProjectRepository.objects.filter(project=project1).count() == 1 - assert SeerProjectRepository.objects.filter(project=project2).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project1).count() == 1 + ) + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project2).count() == 0 + ) def test_post_updates_project_repo_mappings(self): project = self.create_project(organization=self.organization) @@ -373,9 +379,9 @@ def test_post_updates_project_repo_mappings(self): == AutofixStoppingPoint.OPEN_PR.value ) - seer_repos = list(SeerProjectRepository.objects.filter(project=project)) + seer_repos = list(SeerProjectRepository.objects.filter(project_repository__project=project)) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == repo.id + assert seer_repos[0].project_repository.repository_id == repo.id def test_post_clears_repos_with_empty_list(self): project = self.create_project(organization=self.organization) @@ -398,7 +404,9 @@ def test_post_clears_repos_with_empty_list(self): ) assert response.status_code == 204 - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) def test_post_overwrites_existing_repos(self): project = self.create_project(organization=self.organization) @@ -436,9 +444,9 @@ def test_post_overwrites_existing_repos(self): ) assert response.status_code == 204 - seer_repos = list(SeerProjectRepository.objects.filter(project=project)) + seer_repos = list(SeerProjectRepository.objects.filter(project_repository__project=project)) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == new_repo.id + assert seer_repos[0].project_repository.repository_id == new_repo.id def test_post_only_updates_projects_with_changes(self): project1 = self.create_project(organization=self.organization) @@ -478,10 +486,14 @@ def test_post_only_updates_projects_with_changes(self): assert response.status_code == 204 # project1 got the new mapping; project2's existing repo is untouched. - assert SeerProjectRepository.objects.filter(project=project1).count() == 1 - project2_repos = list(SeerProjectRepository.objects.filter(project=project2)) + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project1).count() == 1 + ) + project2_repos = list( + SeerProjectRepository.objects.filter(project_repository__project=project2) + ) assert len(project2_repos) == 1 - assert project2_repos[0].repository_id == existing_repo.id + assert project2_repos[0].project_repository.repository_id == existing_repo.id def test_post_appends_repos_when_append_flag_true(self): project = self.create_project(organization=self.organization) @@ -519,8 +531,12 @@ def test_post_appends_repos_when_append_flag_true(self): ) assert response.status_code == 204 - seer_repos = SeerProjectRepository.objects.filter(project=project).order_by("repository_id") - assert [r.repository_id for r in seer_repos] == sorted([existing_repo.id, new_repo.id]) + seer_repos = SeerProjectRepository.objects.filter( + project_repository__project=project + ).order_by("project_repository__repository_id") + assert [r.project_repository.repository_id for r in seer_repos] == sorted( + [existing_repo.id, new_repo.id] + ) def test_post_append_skips_duplicates(self): project = self.create_project(organization=self.organization) @@ -567,8 +583,12 @@ def test_post_append_skips_duplicates(self): assert response.status_code == 204 # Should only have 2 repos: the existing one and the new one (duplicate skipped) - seer_repos = SeerProjectRepository.objects.filter(project=project).order_by("repository_id") - assert [r.repository_id for r in seer_repos] == sorted([existing_repo.id, new_repo.id]) + seer_repos = SeerProjectRepository.objects.filter( + project_repository__project=project + ).order_by("project_repository__repository_id") + assert [r.project_repository.repository_id for r in seer_repos] == sorted( + [existing_repo.id, new_repo.id] + ) def test_post_creates_audit_log(self): project1 = self.create_project(organization=self.organization) @@ -629,9 +649,9 @@ def test_post_validates_repository_exists_in_organization(self): }, ) assert response.status_code == 204 - seer_repos = list(SeerProjectRepository.objects.filter(project=project)) + seer_repos = list(SeerProjectRepository.objects.filter(project_repository__project=project)) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == repo.id + assert seer_repos[0].project_repository.repository_id == repo.id def test_post_rejects_repository_not_in_organization(self): """Test that POST fails when repository doesn't exist in the organization""" @@ -655,7 +675,9 @@ def test_post_rejects_repository_not_in_organization(self): ) assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) def test_post_rejects_repository_from_different_organization(self): """Test that POST fails when repository exists but belongs to a different organization""" @@ -686,7 +708,9 @@ def test_post_rejects_repository_from_different_organization(self): ) assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) def test_post_rejects_mismatched_organization_id_in_repository_data(self): """Test that POST fails when repository organization_id doesn't match the organization.""" @@ -718,7 +742,9 @@ def test_post_rejects_mismatched_organization_id_in_repository_data(self): ) assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) def test_post_rejects_mismatched_repo_name_or_owner(self): """Test that POST fails when repository name/owner don't match the database record.""" @@ -749,7 +775,9 @@ def test_post_rejects_mismatched_repo_name_or_owner(self): ) assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) def test_post_rejects_unsupported_repo_provider(self): project = self.create_project(organization=self.organization) @@ -771,4 +799,6 @@ def test_post_rejects_unsupported_repo_provider(self): }, ) assert response.status_code == 400 - assert SeerProjectRepository.objects.filter(project=project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=project).count() == 0 + ) diff --git a/tests/sentry/seer/endpoints/test_project_seer_preferences.py b/tests/sentry/seer/endpoints/test_project_seer_preferences.py index f01802ac247001..808d362a7224b9 100644 --- a/tests/sentry/seer/endpoints/test_project_seer_preferences.py +++ b/tests/sentry/seer/endpoints/test_project_seer_preferences.py @@ -84,10 +84,12 @@ def test_post(self) -> None: assert response.status_code == 204 seer_repos = list( - SeerProjectRepository.objects.filter(project=self.project).select_related("repository") + SeerProjectRepository.objects.filter( + project_repository__project=self.project + ).select_related("project_repository__repository") ) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == self.repository.id + assert seer_repos[0].project_repository.repository_id == self.repository.id assert seer_repos[0].branch_name == "main" assert seer_repos[0].instructions == "test instructions" @@ -108,7 +110,10 @@ def test_invalid_request_data(self) -> None: # Should fail with a 400 error for invalid request data assert response.status_code == 400 - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) @patch( "sentry.seer.endpoints.project_seer_preferences.get_autofix_repos_from_project_code_mappings", @@ -146,7 +151,9 @@ def test_post_with_blank_string_fields(self) -> None: response = self.client.post(self.url, data=request_data) assert response.status_code == 204 - seer_repos = list(SeerProjectRepository.objects.filter(project=self.project)) + seer_repos = list( + SeerProjectRepository.objects.filter(project_repository__project=self.project) + ) assert len(seer_repos) == 1 assert seer_repos[0].branch_name == "" assert seer_repos[0].instructions == "" @@ -237,7 +244,10 @@ def test_post_with_invalid_automation_handoff_target(self) -> None: # Should fail with a 400 error for invalid request data assert response.status_code == 400 - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) @patch( "sentry.seer.endpoints.project_seer_preferences.get_autofix_repos_from_project_code_mappings", @@ -356,9 +366,11 @@ def test_post_validates_repository_exists_in_organization(self) -> None: response = self.client.post(self.url, data=request_data) assert response.status_code == 204 - seer_repos = list(SeerProjectRepository.objects.filter(project=self.project)) + seer_repos = list( + SeerProjectRepository.objects.filter(project_repository__project=self.project) + ) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == repo.id + assert seer_repos[0].project_repository.repository_id == repo.id def test_post_rejects_repository_not_in_organization(self) -> None: """Test that POST fails when repository doesn't exist in the organization""" @@ -379,7 +391,10 @@ def test_post_rejects_repository_not_in_organization(self) -> None: assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) def test_post_rejects_repository_from_different_organization(self) -> None: """Test that POST fails when repository exists but belongs to a different organization""" @@ -408,7 +423,10 @@ def test_post_rejects_repository_from_different_organization(self) -> None: assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) def test_post_rejects_mismatched_organization_id_in_repository_data(self) -> None: """Test that POST fails when repository organization_id doesn't match project's org.""" @@ -431,7 +449,10 @@ def test_post_rejects_mismatched_organization_id_in_repository_data(self) -> Non assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) def test_post_rejects_mismatched_repo_name_or_owner(self) -> None: """Test that POST fails when repository name/owner don't match the database record.""" @@ -452,7 +473,10 @@ def test_post_rejects_mismatched_repo_name_or_owner(self) -> None: assert response.status_code == 400 assert response.data["detail"] == "Invalid repository" - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) def test_post_rejects_unsupported_repo_provider(self) -> None: request_data = { @@ -471,7 +495,10 @@ def test_post_rejects_unsupported_repo_provider(self) -> None: response = self.client.post(self.url, data=request_data) assert response.status_code == 400 - assert SeerProjectRepository.objects.filter(project=self.project).count() == 0 + assert ( + SeerProjectRepository.objects.filter(project_repository__project=self.project).count() + == 0 + ) @with_feature("organizations:seer-gitlab-support") def test_post_accepts_gitlab_repo_with_feature_flag(self) -> None: @@ -500,10 +527,12 @@ def test_post_accepts_gitlab_repo_with_feature_flag(self) -> None: assert response.status_code == 204 seer_repos = list( - SeerProjectRepository.objects.filter(project=self.project).select_related("repository") + SeerProjectRepository.objects.filter( + project_repository__project=self.project + ).select_related("project_repository__repository") ) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == gitlab_repo.id + assert seer_repos[0].project_repository.repository_id == gitlab_repo.id @with_feature("organizations:seer-gitlab-support") def test_post_accepts_gitlab_bare_provider_with_feature_flag(self) -> None: @@ -532,7 +561,9 @@ def test_post_accepts_gitlab_bare_provider_with_feature_flag(self) -> None: assert response.status_code == 204 seer_repos = list( - SeerProjectRepository.objects.filter(project=self.project).select_related("repository") + SeerProjectRepository.objects.filter( + project_repository__project=self.project + ).select_related("project_repository__repository") ) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == gitlab_repo.id + assert seer_repos[0].project_repository.repository_id == gitlab_repo.id diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index 66b8e629120a32..f193cf0c7e3839 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -309,6 +309,6 @@ def test_preserves_existing_repositories(self) -> None: configure_seer_for_existing_org(organization_id=self.organization.id) - seer_repos = list(SeerProjectRepository.objects.filter(project=project)) + seer_repos = list(SeerProjectRepository.objects.filter(project_repository__project=project)) assert len(seer_repos) == 1 - assert seer_repos[0].repository_id == repo.id + assert seer_repos[0].project_repository.repository_id == repo.id From 2dcb64e89f3db5f94f48a3beb54c7c46c03bad21 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 14:40:52 -0700 Subject: [PATCH 2/3] keep writing to fields for now --- src/sentry/seer/autofix/utils.py | 2 ++ src/sentry/testutils/factories.py | 4 ++++ src/sentry/testutils/helpers/backups.py | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index fece5e932e6276..357968c59842da 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -528,6 +528,8 @@ def _write_preferences_to_sentry_db( repository_id=repository_id, defaults={"source": ProjectRepositorySource.SEER_PREFERENCE}, ) + spr.project = project + spr.repository_id = repository_id spr.project_repository = project_repo created_project_repos = SeerProjectRepository.objects.bulk_create( diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index 88dd6b8e385355..62e99f8573fbdc 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -899,6 +899,8 @@ def create_code_mapping(project, repo=None, organization_integration=None, **kwa ) kwargs["project_repository"] = project_repo return RepositoryProjectPathConfig.objects.create( + project=project, + repository=repo, organization_integration_id=organization_integration.id, integration_id=organization_integration.integration_id, organization_id=organization_integration.organization_id, @@ -917,6 +919,8 @@ def create_seer_project_repository(project, repository=None, repository_id=None, defaults={"source": ProjectRepositorySource.SEER_PREFERENCE}, ) return SeerProjectRepository.objects.create( + project=project, + repository=repository, project_repository=project_repo, **kwargs, ) diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 8d5ed57bd29759..8abfbebf44cfc3 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -630,7 +630,9 @@ def create_exhaustive_organization( repository=repo, defaults={"source": ProjectRepositorySource.MANUAL}, ) - seer_project_repo = SeerProjectRepository.objects.create(project_repository=project_repo) + seer_project_repo = SeerProjectRepository.objects.create( + project=project, repository=repo, project_repository=project_repo + ) SeerProjectRepositoryBranchOverride.objects.create( seer_project_repository=seer_project_repo, tag_name="environment", From dc296f7784eaabbf0a77ffee33d4cfc484e41478 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 15:08:27 -0700 Subject: [PATCH 3/3] fix tests --- tests/sentry/seer/autofix/test_autofix_utils.py | 4 ++++ tests/sentry/seer/endpoints/test_group_ai_autofix.py | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index f470f6408ae3cd..fe919d27183bac 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -527,6 +527,8 @@ def test_returns_true_via_project_repository_fk(self): ) pr = ProjectRepository.objects.create(project=self.project, repository=repo) SeerProjectRepository.objects.create( + project=self.project, + repository=repo, project_repository=pr, ) @@ -1160,6 +1162,8 @@ def test_project_with_repos_only(self): def test_reads_via_project_repository_fk(self): pr = ProjectRepository.objects.create(project=self.project, repository=self.repo) SeerProjectRepository.objects.create( + project=self.project, + repository=self.repo, project_repository=pr, branch_name="main", ) diff --git a/tests/sentry/seer/endpoints/test_group_ai_autofix.py b/tests/sentry/seer/endpoints/test_group_ai_autofix.py index f2f5ce53f0036c..c7b6a193f1c734 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/seer/endpoints/test_group_ai_autofix.py @@ -118,7 +118,7 @@ def __init__(self): self.integration_id = 42 mock_get_sorted_code_mapping_configs.return_value = [ - Mock(repository=TestRepo(), default_branch="main"), + Mock(project_repository=Mock(repository=TestRepo()), default_branch="main"), ] self.login_as(user=self.user) @@ -182,8 +182,8 @@ def __init__(self, external_id, name, provider, url, integration_id): repo2 = TestRepo("id456", "repo2", "gitlab", "example.com/repo2", 43) mock_get_sorted_code_mapping_configs.return_value = [ - Mock(repository=repo1, default_branch="main"), - Mock(repository=repo2, default_branch="master"), + Mock(project_repository=Mock(repository=repo1), default_branch="main"), + Mock(project_repository=Mock(repository=repo2), default_branch="master"), ] self.login_as(user=self.user) @@ -257,7 +257,9 @@ def __init__(self, external_id): # Create a repo with a different external_id than what's in the codebase mock_get_sorted_code_mapping_configs.return_value = [ - Mock(repository=TestRepo("different_id"), default_branch="main"), + Mock( + project_repository=Mock(repository=TestRepo("different_id")), default_branch="main" + ), ] self.login_as(user=self.user) @@ -300,7 +302,7 @@ def __init__(self): self.integration_id = 42 mock_get_sorted_code_mapping_configs.return_value = [ - Mock(repository=TestRepo(), default_branch="main"), + Mock(project_repository=Mock(repository=TestRepo()), default_branch="main"), ] self.login_as(user=self.user)