From 2c472c4ff6790bc0fe691eb9f0f1c74aa23104a1 Mon Sep 17 00:00:00 2001 From: Suejung Shin Date: Fri, 17 Oct 2025 16:45:12 -0700 Subject: [PATCH 01/11] fix(prevent): Allow members to update config --- src/sentry/api/urls.py | 6 ++ .../prevent/endpoints/pr_review_config.py | 72 ++++++++++++++++++ .../endpoints/test_pr_review_config.py | 74 +++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/sentry/prevent/endpoints/pr_review_config.py create mode 100644 tests/sentry/prevent/endpoints/test_pr_review_config.py diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 9feebd98185168..c306cbd402f154 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -437,6 +437,7 @@ from sentry.prevent.endpoints.organization_github_repos import ( OrganizationPreventGitHubReposEndpoint, ) +from sentry.prevent.endpoints.pr_review_config import OrganizationPreventGitHubConfigEndpoint from sentry.releases.endpoints.organization_release_assemble import ( OrganizationReleaseAssembleEndpoint, ) @@ -2144,6 +2145,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: name="sentry-api-0-organization-repository-commits", ), # Prevent AI endpoints + re_path( + r"^(?P[^/]+)/prevent/github/config/$", + OrganizationPreventGitHubConfigEndpoint.as_view(), + name="sentry-api-0-organization-prevent-github-config", + ), re_path( r"^(?P[^/]+)/prevent/github/repos/$", OrganizationPreventGitHubReposEndpoint.as_view(), diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py new file mode 100644 index 00000000000000..f03fd74fb356d9 --- /dev/null +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -0,0 +1,72 @@ +from jsonschema import validate +from rest_framework.request import Request +from rest_framework.response import Response + +from sentry import audit_log +from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus +from sentry.api.base import region_silo_endpoint +from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission +from sentry.models.organization import Organization +from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT, PREVENT_AI_CONFIG_SCHEMA + +PREVENT_AI_CONFIG_GITHUB_OPTION = "sentry:prevent_ai_config_github" + + +class PreventAIConfigPermission(OrganizationPermission): + scope_map = { + "GET": ["org:read", "org:write", "org:admin"], + # allow any organization member to update the PR review config + "PUT": ["org:read", "org:write", "org:admin"], + } + + +@region_silo_endpoint +class OrganizationPreventGitHubConfigEndpoint(OrganizationEndpoint): + """ + Update the PR review config for a Sentry organization + + PUT /organizations/{organization_id_or_slug}/prevent/github/config/ + """ + + owner = ApiOwner.CODECOV + publish_status = { + "GET": ApiPublishStatus.EXPERIMENTAL, + "PUT": ApiPublishStatus.EXPERIMENTAL, + } + permission_classes = (PreventAIConfigPermission,) + + def get(self, request: Request, organization: Organization) -> Response: + """ + Get the Prevent AI GitHub configuration for an organization. + If not explicitly set, return the default. + """ + config = organization.get_option(PREVENT_AI_CONFIG_GITHUB_OPTION) + if config is None: + config = PREVENT_AI_CONFIG_GITHUB_DEFAULT + return Response({"preventAiConfigGithub": config}, status=200) + + def put(self, request: Request, organization: Organization) -> Response: + """ + Update the Prevent AI GitHub configuration for an organization. + """ + config = request.data.get("config") + if config is None: + return Response({"detail": "Missing 'config' parameter"}, status=400) + + try: + validate(config, PREVENT_AI_CONFIG_SCHEMA) + except Exception: + return Response({"detail": "Invalid config"}, status=400) + + organization.update_option(PREVENT_AI_CONFIG_GITHUB_OPTION, config) + + self.create_audit_entry( + request=request, + organization=organization, + target_object=organization.id, + event=audit_log.get_event_id("ORG_EDIT"), + data={"preventAiConfigGithub": "updated"}, + ) + + return Response({"preventAiConfigGithub": config}, status=200) diff --git a/tests/sentry/prevent/endpoints/test_pr_review_config.py b/tests/sentry/prevent/endpoints/test_pr_review_config.py new file mode 100644 index 00000000000000..e726180bacf88a --- /dev/null +++ b/tests/sentry/prevent/endpoints/test_pr_review_config.py @@ -0,0 +1,74 @@ +from unittest import mock + +from sentry.testutils.cases import APITestCase +from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT + +VALID_CONFIG = { + "schema_version": "v1", + "default_org_config": { + "org_defaults": { + "bug_prediction": { + "enabled": True, + "sensitivity": "medium", + "triggers": {"on_command_phrase": True, "on_ready_for_review": True}, + }, + "test_generation": { + "enabled": False, + "triggers": {"on_command_phrase": True, "on_ready_for_review": False}, + }, + "vanilla": { + "enabled": False, + "sensitivity": "medium", + "triggers": {"on_command_phrase": True, "on_ready_for_review": False}, + }, + }, + "repo_overrides": {}, + }, + "github_organizations": {}, +} + +INVALID_CONFIG = { + "schema_version": "v1", + "default_org_config": { + "org_defaults": {}, + }, +} + + +class OrganizationPreventGitHubConfigTest(APITestCase): + def setUp(self): + super().setUp() + self.org = self.create_organization(owner=self.user) + self.login_as(self.user) + self.url = f"/api/0/organizations/{self.org.slug}/prevent/github/config/" + + def test_missing_config_param_returns_400(self): + resp = self.client.put(self.url, data={}, format="json") + assert resp.status_code == 400 + assert resp.data["detail"] == "Missing 'config' parameter" + + def test_invalid_config_schema_returns_400(self): + resp = self.client.put(self.url, data={"config": INVALID_CONFIG}, format="json") + assert resp.status_code == 400 + assert "'github_organizations' is a required property" in resp.data["detail"] + + def test_valid_config_succeeds_and_sets_option(self): + resp = self.client.put(self.url, data={"config": VALID_CONFIG}, format="json") + assert resp.status_code == 200 + assert resp.data["preventAiConfigGithub"] == VALID_CONFIG + assert self.org.get_option("sentry:prevent_ai_config_github") == VALID_CONFIG + + @mock.patch("sentry.api.base.create_audit_entry") + def test_audit_entry_created(self, mock_create_audit_entry): + self.client.put(self.url, data={"config": VALID_CONFIG}, format="json") + assert mock_create_audit_entry.called + + def test_set_and_get_endpoint_returns_config(self): + resp = self.client.get(self.url) + assert resp.status_code == 200 + assert resp.data == {"preventAiConfigGithub": PREVENT_AI_CONFIG_GITHUB_DEFAULT} + + self.client.put(self.url, data={"config": VALID_CONFIG}, format="json") + resp = self.client.get(self.url) + assert resp.status_code == 200 + assert resp.data == {"preventAiConfigGithub": VALID_CONFIG} From d00ee36d8af7be52ce99ca158ca43a068db02d74 Mon Sep 17 00:00:00 2001 From: Suejung Shin Date: Fri, 17 Oct 2025 16:51:54 -0700 Subject: [PATCH 02/11] cleanup --- src/sentry/prevent/endpoints/pr_review_config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py index f03fd74fb356d9..09b40f14de0b5a 100644 --- a/src/sentry/prevent/endpoints/pr_review_config.py +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -24,9 +24,7 @@ class PreventAIConfigPermission(OrganizationPermission): @region_silo_endpoint class OrganizationPreventGitHubConfigEndpoint(OrganizationEndpoint): """ - Update the PR review config for a Sentry organization - - PUT /organizations/{organization_id_or_slug}/prevent/github/config/ + Get and set the GitHub PR review config for a Sentry organization """ owner = ApiOwner.CODECOV From c79771e8aa065ae6c3eaf2906bbd54f7ff1db4c4 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 20 Oct 2025 17:12:35 -0400 Subject: [PATCH 03/11] Create a prevent config model, no longer use organization options --- migrations_lockfile.txt | 2 + .../api/serializers/models/organization.py | 5 -- src/sentry/api/urls.py | 22 ++++---- src/sentry/conf/server.py | 1 + .../core/endpoints/organization_details.py | 19 ------- .../overwatch/endpoints/overwatch_rpc.py | 2 +- .../prevent/endpoints/pr_review_config.py | 44 +++++++++------ .../0001_initial_prevent_ai_configuration.py | 53 +++++++++++++++++++ src/sentry/prevent/migrations/__init__.py | 0 src/sentry/prevent/models.py | 29 ++++++++++ src/sentry/prevent/types/__init__.py | 0 .../types/config.py} | 23 ++------ 12 files changed, 128 insertions(+), 72 deletions(-) create mode 100644 src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py create mode 100644 src/sentry/prevent/migrations/__init__.py create mode 100644 src/sentry/prevent/models.py create mode 100644 src/sentry/prevent/types/__init__.py rename src/sentry/{types/prevent_config.py => prevent/types/config.py} (86%) diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 3eb97ec892c8aa..a6da304476ddce 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -25,6 +25,8 @@ notifications: 0002_notificationmessage_jsonfield preprod: 0017_break_commit_fks +prevent: 0001_initial_prevent_ai_configuration + releases: 0001_release_models replays: 0006_add_bulk_delete_job diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 06aaf3270632d1..efbdea91254d4b 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -77,7 +77,6 @@ from sentry.models.team import Team, TeamStatus from sentry.organizations.absolute_url import generate_organization_url from sentry.organizations.services.organization import RpcOrganizationSummary -from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.users.services.user.service import user_service @@ -697,10 +696,6 @@ def serialize( # type: ignore[override] "sentry:default_seer_scanner_automation", DEFAULT_SEER_SCANNER_AUTOMATION_DEFAULT, ), - "preventAiConfigGithub": obj.get_option( - "sentry:prevent_ai_config_github", - PREVENT_AI_CONFIG_GITHUB_DEFAULT, - ), "enablePrReviewTestGeneration": bool( obj.get_option( "sentry:enable_pr_review_test_generation", diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index c306cbd402f154..3665772818bb9c 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -1161,6 +1161,17 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: SyncReposEndpoint.as_view(), name="sentry-api-0-repositories-sync", ), + # Prevent AI endpoints + re_path( + r"^ai/github/config/(?P[^/]+)/$", + OrganizationPreventGitHubConfigEndpoint.as_view(), + name="sentry-api-0-organization-prevent-github-config", + ), + re_path( + r"^ai/github/repos/$", + OrganizationPreventGitHubReposEndpoint.as_view(), + name="sentry-api-0-organization-prevent-github-repos", + ), ] USER_URLS = [ @@ -2144,17 +2155,6 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: OrganizationRepositoryCommitsEndpoint.as_view(), name="sentry-api-0-organization-repository-commits", ), - # Prevent AI endpoints - re_path( - r"^(?P[^/]+)/prevent/github/config/$", - OrganizationPreventGitHubConfigEndpoint.as_view(), - name="sentry-api-0-organization-prevent-github-config", - ), - re_path( - r"^(?P[^/]+)/prevent/github/repos/$", - OrganizationPreventGitHubReposEndpoint.as_view(), - name="sentry-api-0-organization-prevent-github-repos", - ), re_path( r"^(?P[^/]+)/plugins/$", OrganizationPluginsEndpoint.as_view(), diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 5263f638dd01fb..c5f22dfa766b78 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -479,6 +479,7 @@ def env( "sentry.insights", "sentry.preprod", "sentry.releases", + "sentry.prevent", ) # Silence internal hints from Django's system checks diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 023b877bf0cc3b..f57653db6c97d1 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -5,7 +5,6 @@ from datetime import datetime, timedelta, timezone from typing import TypedDict -from django import forms from django.db import models, router, transaction from django.db.models.query_utils import DeferredAttribute from django.urls import reverse @@ -104,10 +103,8 @@ OrganizationSlugCollisionException, organization_provisioning_service, ) -from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT, PREVENT_AI_CONFIG_SCHEMA from sentry.users.services.user.serial import serialize_generic_user from sentry.utils.audit import create_audit_entry -from sentry.workflow_engine.endpoints.validators.utils import validate_json_schema ERR_DEFAULT_ORG = "You cannot remove the default organization." ERR_NO_USER = "This request requires an authenticated user." @@ -237,12 +234,6 @@ bool, DEFAULT_SEER_SCANNER_AUTOMATION_DEFAULT, ), - ( - "preventAiConfigGithub", - "sentry:prevent_ai_config_github", - dict, - PREVENT_AI_CONFIG_GITHUB_DEFAULT, - ), ( "enablePrReviewTestGeneration", "sentry:enable_pr_review_test_generation", @@ -348,7 +339,6 @@ class OrganizationSerializer(BaseOrganizationSerializer): ) enablePrReviewTestGeneration = serializers.BooleanField(required=False) enableSeerEnhancedAlerts = serializers.BooleanField(required=False) - preventAiConfigGithub = serializers.JSONField(required=False) enableSeerCoding = serializers.BooleanField(required=False) ingestThroughTrustedRelaysOnly = serializers.ChoiceField( choices=[("enabled", "enabled"), ("disabled", "disabled")], required=False @@ -459,14 +449,6 @@ def validate_samplingMode(self, value): return value - def validate_preventAiConfigGithub(self, value): - """Validate the structure using JSON Schema - generic error for invalid configs.""" - try: - validate_json_schema(value, PREVENT_AI_CONFIG_SCHEMA) - except forms.ValidationError: - raise serializers.ValidationError("Prevent AI config option is invalid") - return value - def validate(self, attrs): attrs = super().validate(attrs) if attrs.get("avatarType") == "upload": @@ -742,7 +724,6 @@ def create_console_platform_audit_log( "genAIConsent", "defaultAutofixAutomationTuning", "defaultSeerScannerAutomation", - "preventAiConfigGithub", "ingestThroughTrustedRelaysOnly", "enabledConsolePlatforms", ] diff --git a/src/sentry/overwatch/endpoints/overwatch_rpc.py b/src/sentry/overwatch/endpoints/overwatch_rpc.py index f3ef8a1c214cf9..36678dabce56e4 100644 --- a/src/sentry/overwatch/endpoints/overwatch_rpc.py +++ b/src/sentry/overwatch/endpoints/overwatch_rpc.py @@ -21,8 +21,8 @@ ) from sentry.models.organization import Organization from sentry.models.repository import Repository +from sentry.prevent.types.config import PREVENT_AI_CONFIG_GITHUB_DEFAULT from sentry.silo.base import SiloMode -from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT logger = logging.getLogger(__name__) diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py index 09b40f14de0b5a..7fcd64d00c6895 100644 --- a/src/sentry/prevent/endpoints/pr_review_config.py +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from jsonschema import validate from rest_framework.request import Request from rest_framework.response import Response @@ -8,7 +10,8 @@ from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.models.organization import Organization -from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT, PREVENT_AI_CONFIG_SCHEMA +from sentry.prevent.models import PreventAIConfiguration +from sentry.prevent.types.config import ORG_CONFIG_SCHEMA, PREVENT_AI_CONFIG_GITHUB_DEFAULT PREVENT_AI_CONFIG_GITHUB_OPTION = "sentry:prevent_ai_config_github" @@ -34,30 +37,36 @@ class OrganizationPreventGitHubConfigEndpoint(OrganizationEndpoint): } permission_classes = (PreventAIConfigPermission,) - def get(self, request: Request, organization: Organization) -> Response: + def get(self, request: Request, organization: Organization, git_organization: str) -> Response: """ - Get the Prevent AI GitHub configuration for an organization. - If not explicitly set, return the default. + Get the Prevent AI GitHub configuration for a specific git organization. """ - config = organization.get_option(PREVENT_AI_CONFIG_GITHUB_OPTION) - if config is None: - config = PREVENT_AI_CONFIG_GITHUB_DEFAULT - return Response({"preventAiConfigGithub": config}, status=200) + response_data = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) + + config = PreventAIConfiguration.objects.filter( + organization_id=organization.id, provider="github", git_organization=git_organization + ).first() + + if config: + response_data["github_organization"][git_organization] = config.data + + return Response(response_data, status=200) - def put(self, request: Request, organization: Organization) -> Response: + def put(self, request: Request, organization: Organization, git_organization: str) -> Response: """ Update the Prevent AI GitHub configuration for an organization. """ - config = request.data.get("config") - if config is None: - return Response({"detail": "Missing 'config' parameter"}, status=400) - try: - validate(config, PREVENT_AI_CONFIG_SCHEMA) + validate(request.data, ORG_CONFIG_SCHEMA) except Exception: return Response({"detail": "Invalid config"}, status=400) - organization.update_option(PREVENT_AI_CONFIG_GITHUB_OPTION, config) + PreventAIConfiguration.objects.update_or_create( + organization_id=organization.id, + provider="github", + git_organization=git_organization, + defaults={"data": request.data}, + ) self.create_audit_entry( request=request, @@ -67,4 +76,7 @@ def put(self, request: Request, organization: Organization) -> Response: data={"preventAiConfigGithub": "updated"}, ) - return Response({"preventAiConfigGithub": config}, status=200) + response_data = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) + response_data["github_organization"][git_organization] = request.data + + return Response(response_data, status=200) diff --git a/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py b/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py new file mode 100644 index 00000000000000..8bf0fa208a9e73 --- /dev/null +++ b/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.1 on 2025-10-20 18:37 + +from django.db import migrations, models + +import sentry.db.models.fields.bounded +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="PreventAIConfiguration", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ("date_updated", models.DateTimeField(auto_now=True)), + ("date_added", models.DateTimeField(auto_now_add=True)), + ( + "organization_id", + sentry.db.models.fields.bounded.BoundedBigIntegerField(db_index=True), + ), + ("provider", models.CharField(max_length=64)), + ("git_organization", models.CharField(max_length=255)), + ("data", models.JSONField(default=dict)), + ], + options={ + "db_table": "sentry_preventaiconfiguration", + "unique_together": {("organization_id", "provider", "git_organization")}, + }, + ), + ] diff --git a/src/sentry/prevent/migrations/__init__.py b/src/sentry/prevent/migrations/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/sentry/prevent/models.py b/src/sentry/prevent/models.py new file mode 100644 index 00000000000000..b419d9b062b10e --- /dev/null +++ b/src/sentry/prevent/models.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from django.db import models + +from sentry.backup.scopes import RelocationScope +from sentry.db.models.base import DefaultFieldsModel, region_silo_model +from sentry.db.models.fields.bounded import BoundedBigIntegerField + + +@region_silo_model +class PreventAIConfiguration(DefaultFieldsModel): + """ + Configuration for Prevent AI features for git organizations per Sentry organization. + + This model stores configurations for Prevent AI functionality, + allowing different settings for different git organizations and repos. + """ + + __relocation_scope__ = RelocationScope.Excluded + + organization_id = BoundedBigIntegerField(db_index=True) + provider = models.CharField(max_length=64) + git_organization = models.CharField(max_length=255) + data = models.JSONField(default=dict) + + class Meta: + app_label = "prevent" + db_table = "sentry_preventaiconfiguration" + unique_together = (("organization_id", "provider", "git_organization"),) diff --git a/src/sentry/prevent/types/__init__.py b/src/sentry/prevent/types/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/sentry/types/prevent_config.py b/src/sentry/prevent/types/config.py similarity index 86% rename from src/sentry/types/prevent_config.py rename to src/sentry/prevent/types/config.py index 68a7e89af9ca1d..1741e91358449c 100644 --- a/src/sentry/types/prevent_config.py +++ b/src/sentry/prevent/types/config.py @@ -78,31 +78,14 @@ "additionalProperties": False, } -_ORG_CONFIG_SCHEMA = { +ORG_CONFIG_SCHEMA = { "type": "object", "properties": { "org_defaults": _ORG_DEFAULTS_SCHEMA, "repo_overrides": _REPO_OVERRIDES_SCHEMA, - }, - "required": ["org_defaults", "repo_overrides"], - "additionalProperties": False, -} - - -PREVENT_AI_CONFIG_SCHEMA = { - "type": "object", - "properties": { "schema_version": {"type": "string", "enum": ["v1"]}, - "default_org_config": _ORG_CONFIG_SCHEMA, - "github_organizations": { - "type": "object", - "patternProperties": { - ".*": _ORG_CONFIG_SCHEMA, - }, - "additionalProperties": False, - }, }, - "required": ["schema_version", "default_org_config", "github_organizations"], + "required": ["org_defaults", "repo_overrides"], "additionalProperties": False, } @@ -136,5 +119,5 @@ }, "repo_overrides": {}, }, - "github_organizations": {}, + "github_organization": {}, } From 95f1d6ba3a13087e9d5484840c400f81f1ed05bf Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 20 Oct 2025 17:31:06 -0400 Subject: [PATCH 04/11] update tests --- .../endpoints/test_organization_details.py | 1037 ----------------- .../endpoints/test_pr_review_config.py | 164 ++- 2 files changed, 119 insertions(+), 1082 deletions(-) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index b81b05fa30c592..f052a9722fdb62 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1312,1043 +1312,6 @@ def test_default_seer_scanner_automation(self) -> None: self.get_success_response(self.organization.slug, **data) assert self.organization.get_option("sentry:default_seer_scanner_automation") is True - def test_prevent_ai_config_github(self) -> None: - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": { - "my_repo_name": { - "bug_prediction": { - "enabled": False, - "sensitivity": "low", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": True, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "critical", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - } - }, - }, - "github_organizations": {}, - } - } - self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:prevent_ai_config_github") - == data["preventAiConfigGithub"] - ) - - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": { - "my_repo_name": { - "bug_prediction": { - "enabled": False, - "sensitivity": "low", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": True, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "critical", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - }, - "my_other_repo_name": { - "bug_prediction": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - }, - }, - "github_organizations": {}, - } - } - self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:prevent_ai_config_github") - == data["preventAiConfigGithub"] - ) - - def test_prevent_ai_config_github_null_rejected(self) -> None: - """Test that setting preventAiConfigGithub to null is rejected""" - data = {"preventAiConfigGithub": None} - self.get_error_response(self.organization.slug, status_code=400, **data) - - def test_prevent_ai_config_github_get_default(self) -> None: - # Verify that when no config is set, it returns the default config - expected_default = { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": {}, - } - response = self.get_success_response(self.organization.slug) - assert response.data["preventAiConfigGithub"] == expected_default - - def test_prevent_ai_config_github_validation_missing_fields(self) -> None: - """Test that missing required fields are rejected""" - # Missing default_org_config - data: dict[str, dict[str, Any]] = { - "preventAiConfigGithub": {"schema_version": "v1", "github_organizations": {}} - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - # Verify we get a validation error for missing required field - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - def test_prevent_ai_config_github_validation_invalid_structure(self) -> None: - """Test that invalid structures are rejected""" - # Not an object - data_1 = {"preventAiConfigGithub": "invalid"} - response = self.get_error_response(self.organization.slug, status_code=400, **data_1) - # Check for validation error - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - # Missing feature fields - data_2: dict[str, dict] = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - # Missing triggers - } - }, - "repo_overrides": {}, - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data_2) - # Check for validation error - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - def test_prevent_ai_config_github_validation_missing_repo_overrides(self) -> None: - """Test missing repo_overrides field""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - # Missing repo_overrides - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - - def test_prevent_ai_config_github_validation_missing_trigger(self) -> None: - """Test missing trigger field""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - # Missing on_ready_for_review - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - - def test_prevent_ai_config_github_validation_missing_setting_field(self) -> None: - """Test missing setting field""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - # Missing vanilla feature - }, - "repo_overrides": {}, - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - - def test_prevent_ai_config_github_validation_wrong_data_types(self) -> None: - """Test wrong data types""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": "yes", # String instead of bool - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - # Should get object-level validation error - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - def test_prevent_ai_config_github_validation_repo_overrides(self) -> None: - """Test validation specifically for repo_overrides structure""" - - # Invalid repo override - missing features - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": { - "my_repo": { - # Missing all features - } - }, - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - # Should get object-level validation error - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - # Invalid repo override - wrong field types - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": { - "my_repo": { - "bug_prediction": { - "enabled": 1, # Number instead of bool - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - } - }, - }, - "github_organizations": {}, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - - # Valid repo overrides should work - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": { - "repo_1": { - "bug_prediction": { - "enabled": False, - "sensitivity": "low", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": True, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "critical", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - }, - "repo_2": { - "bug_prediction": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - }, - }, - "github_organizations": {}, - } - } - self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:prevent_ai_config_github") - == data["preventAiConfigGithub"] - ) - - def test_prevent_ai_config_github_with_single_organization(self) -> None: - """Test valid configuration with a single organization in github_organizations""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": { - "my-org": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": True, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "critical", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - }, - "repo_overrides": { - "sensitive-repo": { - "bug_prediction": { - "enabled": False, - "sensitivity": "low", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "low", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - }, - }, - }, - }, - } - } - self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:prevent_ai_config_github") - == data["preventAiConfigGithub"] - ) - - def test_prevent_ai_config_github_with_multiple_organizations(self) -> None: - """Test valid configuration with multiple organizations in github_organizations""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": { - "org-alpha": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "org-beta": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "low", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": True, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "critical", - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": { - "special-repo": { - "bug_prediction": { - "enabled": True, - "sensitivity": "critical", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - }, - }, - }, - } - } - self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:prevent_ai_config_github") - == data["preventAiConfigGithub"] - ) - - def test_prevent_ai_config_github_invalid_organization_structure(self) -> None: - """Test invalid organization structure in github_organizations""" - # Missing org_defaults in organization - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": { - "invalid-org": { - # Missing org_defaults - "repo_overrides": {}, - }, - }, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - # Invalid feature configuration in organization - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": { - "invalid-org": { - "org_defaults": { - "bug_prediction": { - "enabled": "invalid", # Should be boolean - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - }, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - - def test_prevent_ai_config_github_mixed_valid_invalid_organizations(self) -> None: - """Test configuration with both valid and invalid organizations""" - data = { - "preventAiConfigGithub": { - "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": True, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "github_organizations": { - "valid-org": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "high", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - "invalid-org": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "invalid_sensitivity", # Invalid enum value - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - "test_generation": { - "enabled": False, - "triggers": { - "on_command_phrase": False, - "on_ready_for_review": True, - }, - }, - "vanilla": { - "enabled": True, - "sensitivity": "medium", - "triggers": { - "on_command_phrase": True, - "on_ready_for_review": False, - }, - }, - }, - "repo_overrides": {}, - }, - }, - } - } - response = self.get_error_response(self.organization.slug, status_code=400, **data) - assert "preventAiConfigGithub" in response.data - error_msg = str(response.data["preventAiConfigGithub"]) - assert "Prevent AI config option is invalid" in error_msg - def test_enabled_console_platforms_present_in_response(self) -> None: response = self.get_success_response(self.organization.slug) assert "enabledConsolePlatforms" in response.data diff --git a/tests/sentry/prevent/endpoints/test_pr_review_config.py b/tests/sentry/prevent/endpoints/test_pr_review_config.py index e726180bacf88a..2f7776d186a930 100644 --- a/tests/sentry/prevent/endpoints/test_pr_review_config.py +++ b/tests/sentry/prevent/endpoints/test_pr_review_config.py @@ -1,37 +1,34 @@ +from copy import deepcopy from unittest import mock +from sentry.prevent.models import PreventAIConfiguration +from sentry.prevent.types.config import PREVENT_AI_CONFIG_GITHUB_DEFAULT from sentry.testutils.cases import APITestCase -from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT -VALID_CONFIG = { +VALID_ORG_CONFIG = { "schema_version": "v1", - "default_org_config": { - "org_defaults": { - "bug_prediction": { - "enabled": True, - "sensitivity": "medium", - "triggers": {"on_command_phrase": True, "on_ready_for_review": True}, - }, - "test_generation": { - "enabled": False, - "triggers": {"on_command_phrase": True, "on_ready_for_review": False}, - }, - "vanilla": { - "enabled": False, - "sensitivity": "medium", - "triggers": {"on_command_phrase": True, "on_ready_for_review": False}, - }, + "org_defaults": { + "bug_prediction": { + "enabled": True, + "sensitivity": "medium", + "triggers": {"on_command_phrase": True, "on_ready_for_review": True}, + }, + "test_generation": { + "enabled": False, + "triggers": {"on_command_phrase": True, "on_ready_for_review": False}, + }, + "vanilla": { + "enabled": False, + "sensitivity": "medium", + "triggers": {"on_command_phrase": True, "on_ready_for_review": False}, }, - "repo_overrides": {}, }, - "github_organizations": {}, + "repo_overrides": {}, } -INVALID_CONFIG = { +INVALID_ORG_CONFIG = { "schema_version": "v1", - "default_org_config": { - "org_defaults": {}, - }, + "org_defaults": {}, } @@ -40,35 +37,112 @@ def setUp(self): super().setUp() self.org = self.create_organization(owner=self.user) self.login_as(self.user) - self.url = f"/api/0/organizations/{self.org.slug}/prevent/github/config/" + self.git_org = "my-github-org" + self.url = f"/api/0/organizations/{self.org.slug}/prevent/ai/github/config/{self.git_org}/" - def test_missing_config_param_returns_400(self): - resp = self.client.put(self.url, data={}, format="json") - assert resp.status_code == 400 - assert resp.data["detail"] == "Missing 'config' parameter" + def test_get_returns_default_when_no_config(self): + resp = self.client.get(self.url) + assert resp.status_code == 200 + assert resp.data == PREVENT_AI_CONFIG_GITHUB_DEFAULT + assert resp.data["github_organization"] == {} + + def test_get_returns_config_when_exists(self): + PreventAIConfiguration.objects.create( + organization_id=self.org.id, + provider="github", + git_organization=self.git_org, + data=VALID_ORG_CONFIG, + ) - def test_invalid_config_schema_returns_400(self): - resp = self.client.put(self.url, data={"config": INVALID_CONFIG}, format="json") + resp = self.client.get(self.url) + assert resp.status_code == 200 + assert resp.data["github_organization"][self.git_org] == VALID_ORG_CONFIG + + def test_put_with_invalid_config_returns_400(self): + resp = self.client.put(self.url, data=INVALID_ORG_CONFIG, format="json") assert resp.status_code == 400 - assert "'github_organizations' is a required property" in resp.data["detail"] + assert resp.data["detail"] == "Invalid config" - def test_valid_config_succeeds_and_sets_option(self): - resp = self.client.put(self.url, data={"config": VALID_CONFIG}, format="json") + def test_put_with_valid_config_creates_entry(self): + resp = self.client.put(self.url, data=VALID_ORG_CONFIG, format="json") assert resp.status_code == 200 - assert resp.data["preventAiConfigGithub"] == VALID_CONFIG - assert self.org.get_option("sentry:prevent_ai_config_github") == VALID_CONFIG + assert resp.data["github_organization"][self.git_org] == VALID_ORG_CONFIG + + config = PreventAIConfiguration.objects.get( + organization_id=self.org.id, provider="github", git_organization=self.git_org + ) + assert config.data == VALID_ORG_CONFIG + + def test_put_updates_existing_config(self): + PreventAIConfiguration.objects.create( + organization_id=self.org.id, + provider="github", + git_organization=self.git_org, + data={"org_defaults": {"bug_prediction": {"enabled": False}}, "repo_overrides": {}}, + ) + + resp = self.client.put(self.url, data=VALID_ORG_CONFIG, format="json") + assert resp.status_code == 200 + + assert ( + PreventAIConfiguration.objects.filter( + organization_id=self.org.id, provider="github", git_organization=self.git_org + ).count() + == 1 + ) + + config = PreventAIConfiguration.objects.get( + organization_id=self.org.id, provider="github", git_organization=self.git_org + ) + assert config.data == VALID_ORG_CONFIG @mock.patch("sentry.api.base.create_audit_entry") def test_audit_entry_created(self, mock_create_audit_entry): - self.client.put(self.url, data={"config": VALID_CONFIG}, format="json") + self.client.put(self.url, data=VALID_ORG_CONFIG, format="json") assert mock_create_audit_entry.called - def test_set_and_get_endpoint_returns_config(self): - resp = self.client.get(self.url) - assert resp.status_code == 200 - assert resp.data == {"preventAiConfigGithub": PREVENT_AI_CONFIG_GITHUB_DEFAULT} + def test_different_git_orgs_have_separate_configs(self): + git_org_1 = "org-1" + git_org_2 = "org-2" - self.client.put(self.url, data={"config": VALID_CONFIG}, format="json") - resp = self.client.get(self.url) - assert resp.status_code == 200 - assert resp.data == {"preventAiConfigGithub": VALID_CONFIG} + url_1 = f"/api/0/organizations/{self.org.slug}/prevent/ai/github/config/{git_org_1}/" + url_2 = f"/api/0/organizations/{self.org.slug}/prevent/ai/github/config/{git_org_2}/" + + config_1 = deepcopy(VALID_ORG_CONFIG) + config_1["org_defaults"]["bug_prediction"]["enabled"] = True + config_1["org_defaults"]["test_generation"]["enabled"] = True + + config_2 = deepcopy(VALID_ORG_CONFIG) + config_2["org_defaults"]["bug_prediction"]["enabled"] = False + config_2["org_defaults"]["test_generation"]["enabled"] = False + + self.client.put(url_1, data=config_1, format="json") + self.client.put(url_2, data=config_2, format="json") + + resp_1 = self.client.get(url_1) + resp_2 = self.client.get(url_2) + + assert ( + resp_1.data["github_organization"][git_org_1]["org_defaults"]["bug_prediction"][ + "enabled" + ] + is True + ) + assert ( + resp_1.data["github_organization"][git_org_1]["org_defaults"]["test_generation"][ + "enabled" + ] + is True + ) + assert ( + resp_2.data["github_organization"][git_org_2]["org_defaults"]["bug_prediction"][ + "enabled" + ] + is False + ) + assert ( + resp_2.data["github_organization"][git_org_2]["org_defaults"]["test_generation"][ + "enabled" + ] + is False + ) From 03dc73b63c8184feb1d2bf2c8b22fe23e93a387d Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 20 Oct 2025 17:36:08 -0400 Subject: [PATCH 05/11] cleanup --- src/sentry/api/serializers/models/organization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index efbdea91254d4b..37f6d17e16066c 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -554,7 +554,6 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp streamlineOnly: bool defaultAutofixAutomationTuning: str defaultSeerScannerAutomation: bool - preventAiConfigGithub: dict[str, Any] enablePrReviewTestGeneration: bool enableSeerEnhancedAlerts: bool enableSeerCoding: bool From 4c72266dfa569d56ca8013d1e05061471bd60782 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 20 Oct 2025 17:37:57 -0400 Subject: [PATCH 06/11] cleanup unsued var --- src/sentry/prevent/endpoints/pr_review_config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py index 7fcd64d00c6895..c4fd8b20b1b75d 100644 --- a/src/sentry/prevent/endpoints/pr_review_config.py +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -13,8 +13,6 @@ from sentry.prevent.models import PreventAIConfiguration from sentry.prevent.types.config import ORG_CONFIG_SCHEMA, PREVENT_AI_CONFIG_GITHUB_DEFAULT -PREVENT_AI_CONFIG_GITHUB_OPTION = "sentry:prevent_ai_config_github" - class PreventAIConfigPermission(OrganizationPermission): scope_map = { From a266d314087de3e7c63bb3d01e14d050491e9b2c Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 20 Oct 2025 18:02:54 -0400 Subject: [PATCH 07/11] fix import --- tests/sentry/overwatch/endpoints/test_overwatch_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index 1fb1d5d1ada032..e402bbbcde441a 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -5,8 +5,8 @@ from django.urls import reverse from sentry.constants import ObjectStatus +from sentry.prevent.types.config import PREVENT_AI_CONFIG_GITHUB_DEFAULT from sentry.testutils.cases import APITestCase -from sentry.types.prevent_config import PREVENT_AI_CONFIG_GITHUB_DEFAULT class TestPreventPrReviewResolvedConfigsEndpoint(APITestCase): From b872e4afa10cb496d43efe042cd368b30aa52dbb Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Mon, 20 Oct 2025 18:40:25 -0400 Subject: [PATCH 08/11] fix mypy --- src/sentry/prevent/endpoints/pr_review_config.py | 5 +++-- tests/sentry/prevent/endpoints/test_pr_review_config.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py index c4fd8b20b1b75d..e8ff150ef05d44 100644 --- a/src/sentry/prevent/endpoints/pr_review_config.py +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -1,4 +1,5 @@ from copy import deepcopy +from typing import Any from jsonschema import validate from rest_framework.request import Request @@ -39,7 +40,7 @@ def get(self, request: Request, organization: Organization, git_organization: st """ Get the Prevent AI GitHub configuration for a specific git organization. """ - response_data = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) + response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) config = PreventAIConfiguration.objects.filter( organization_id=organization.id, provider="github", git_organization=git_organization @@ -74,7 +75,7 @@ def put(self, request: Request, organization: Organization, git_organization: st data={"preventAiConfigGithub": "updated"}, ) - response_data = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) + response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) response_data["github_organization"][git_organization] = request.data return Response(response_data, status=200) diff --git a/tests/sentry/prevent/endpoints/test_pr_review_config.py b/tests/sentry/prevent/endpoints/test_pr_review_config.py index 2f7776d186a930..93287679b781a6 100644 --- a/tests/sentry/prevent/endpoints/test_pr_review_config.py +++ b/tests/sentry/prevent/endpoints/test_pr_review_config.py @@ -1,4 +1,5 @@ from copy import deepcopy +from typing import Any from unittest import mock from sentry.prevent.models import PreventAIConfiguration @@ -108,11 +109,11 @@ def test_different_git_orgs_have_separate_configs(self): url_1 = f"/api/0/organizations/{self.org.slug}/prevent/ai/github/config/{git_org_1}/" url_2 = f"/api/0/organizations/{self.org.slug}/prevent/ai/github/config/{git_org_2}/" - config_1 = deepcopy(VALID_ORG_CONFIG) + config_1: dict[str, Any] = deepcopy(VALID_ORG_CONFIG) config_1["org_defaults"]["bug_prediction"]["enabled"] = True config_1["org_defaults"]["test_generation"]["enabled"] = True - config_2 = deepcopy(VALID_ORG_CONFIG) + config_2: dict[str, Any] = deepcopy(VALID_ORG_CONFIG) config_2["org_defaults"]["bug_prediction"]["enabled"] = False config_2["org_defaults"]["test_generation"]["enabled"] = False From 03560b15e4c8e5a4574359aa6894fb0d968212bb Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Wed, 22 Oct 2025 14:13:19 -0400 Subject: [PATCH 09/11] rename column to be _id & provider, create new audit log event --- src/sentry/api/urls.py | 2 +- src/sentry/audit_log/register.py | 9 ++++++- .../prevent/endpoints/pr_review_config.py | 27 ++++++++++++------- .../0001_initial_prevent_ai_configuration.py | 8 +++--- src/sentry/prevent/models.py | 6 ++--- .../endpoints/test_pr_review_config.py | 18 +++++++------ 6 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 3665772818bb9c..e232ad0cd84059 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -1163,7 +1163,7 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: ), # Prevent AI endpoints re_path( - r"^ai/github/config/(?P[^/]+)/$", + r"^ai/github/config/(?P[^/]+)/$", OrganizationPreventGitHubConfigEndpoint.as_view(), name="sentry-api-0-organization-prevent-github-config", ), diff --git a/src/sentry/audit_log/register.py b/src/sentry/audit_log/register.py index f31ff5cb037715..a006038e41e012 100644 --- a/src/sentry/audit_log/register.py +++ b/src/sentry/audit_log/register.py @@ -627,7 +627,14 @@ template="disconnected detector {detector_name} from workflow {workflow_name}", ) ) - +default_manager.add( + AuditLogEvent( + event_id=203, + name="PREVENT_CONFIG_EDIT", + api_name="prevent.config.edit", + template="prevent_ai.config.edit: {service} {git_organization}", + ) +) default_manager.add( AuditLogEvent( event_id=204, diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py index e8ff150ef05d44..aa5d4669ccf7d3 100644 --- a/src/sentry/prevent/endpoints/pr_review_config.py +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -36,22 +36,28 @@ class OrganizationPreventGitHubConfigEndpoint(OrganizationEndpoint): } permission_classes = (PreventAIConfigPermission,) - def get(self, request: Request, organization: Organization, git_organization: str) -> Response: + def get( + self, request: Request, organization: Organization, git_organization_id: str + ) -> Response: """ Get the Prevent AI GitHub configuration for a specific git organization. """ response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) config = PreventAIConfiguration.objects.filter( - organization_id=organization.id, provider="github", git_organization=git_organization + organization_id=organization.id, + service="github", + git_organization_id=git_organization_id, ).first() if config: - response_data["github_organization"][git_organization] = config.data + response_data["github_organization"][git_organization_id] = config.data return Response(response_data, status=200) - def put(self, request: Request, organization: Organization, git_organization: str) -> Response: + def put( + self, request: Request, organization: Organization, git_organization_id: str + ) -> Response: """ Update the Prevent AI GitHub configuration for an organization. """ @@ -62,8 +68,8 @@ def put(self, request: Request, organization: Organization, git_organization: st PreventAIConfiguration.objects.update_or_create( organization_id=organization.id, - provider="github", - git_organization=git_organization, + service="github", + git_organization_id=git_organization_id, defaults={"data": request.data}, ) @@ -71,11 +77,14 @@ def put(self, request: Request, organization: Organization, git_organization: st request=request, organization=organization, target_object=organization.id, - event=audit_log.get_event_id("ORG_EDIT"), - data={"preventAiConfigGithub": "updated"}, + event=audit_log.get_event_id("PREVENT_CONFIG_EDIT"), + data={ + "git_organization": git_organization_id, + "service": "github", + }, ) response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT) - response_data["github_organization"][git_organization] = request.data + response_data["github_organization"][git_organization_id] = request.data return Response(response_data, status=200) diff --git a/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py b/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py index 8bf0fa208a9e73..0b113f8a541226 100644 --- a/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py +++ b/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.1 on 2025-10-20 18:37 +# Generated by Django 5.2.1 on 2025-10-22 17:57 from django.db import migrations, models @@ -41,13 +41,13 @@ class Migration(CheckedMigration): "organization_id", sentry.db.models.fields.bounded.BoundedBigIntegerField(db_index=True), ), - ("provider", models.CharField(max_length=64)), - ("git_organization", models.CharField(max_length=255)), + ("service", models.CharField(max_length=64)), + ("git_organization_id", models.CharField(max_length=255)), ("data", models.JSONField(default=dict)), ], options={ "db_table": "sentry_preventaiconfiguration", - "unique_together": {("organization_id", "provider", "git_organization")}, + "unique_together": {("organization_id", "service", "git_organization_id")}, }, ), ] diff --git a/src/sentry/prevent/models.py b/src/sentry/prevent/models.py index b419d9b062b10e..f5154257e56076 100644 --- a/src/sentry/prevent/models.py +++ b/src/sentry/prevent/models.py @@ -19,11 +19,11 @@ class PreventAIConfiguration(DefaultFieldsModel): __relocation_scope__ = RelocationScope.Excluded organization_id = BoundedBigIntegerField(db_index=True) - provider = models.CharField(max_length=64) - git_organization = models.CharField(max_length=255) + service = models.CharField(max_length=64) + git_organization_id = models.CharField(max_length=255) data = models.JSONField(default=dict) class Meta: app_label = "prevent" db_table = "sentry_preventaiconfiguration" - unique_together = (("organization_id", "provider", "git_organization"),) + unique_together = (("organization_id", "service", "git_organization_id"),) diff --git a/tests/sentry/prevent/endpoints/test_pr_review_config.py b/tests/sentry/prevent/endpoints/test_pr_review_config.py index 93287679b781a6..a62fe3a8235932 100644 --- a/tests/sentry/prevent/endpoints/test_pr_review_config.py +++ b/tests/sentry/prevent/endpoints/test_pr_review_config.py @@ -50,8 +50,8 @@ def test_get_returns_default_when_no_config(self): def test_get_returns_config_when_exists(self): PreventAIConfiguration.objects.create( organization_id=self.org.id, - provider="github", - git_organization=self.git_org, + service="github", + git_organization_id=self.git_org, data=VALID_ORG_CONFIG, ) @@ -70,15 +70,15 @@ def test_put_with_valid_config_creates_entry(self): assert resp.data["github_organization"][self.git_org] == VALID_ORG_CONFIG config = PreventAIConfiguration.objects.get( - organization_id=self.org.id, provider="github", git_organization=self.git_org + organization_id=self.org.id, service="github", git_organization_id=self.git_org ) assert config.data == VALID_ORG_CONFIG def test_put_updates_existing_config(self): PreventAIConfiguration.objects.create( organization_id=self.org.id, - provider="github", - git_organization=self.git_org, + service="github", + git_organization_id=self.git_org, data={"org_defaults": {"bug_prediction": {"enabled": False}}, "repo_overrides": {}}, ) @@ -87,17 +87,19 @@ def test_put_updates_existing_config(self): assert ( PreventAIConfiguration.objects.filter( - organization_id=self.org.id, provider="github", git_organization=self.git_org + organization_id=self.org.id, service="github", git_organization_id=self.git_org ).count() == 1 ) config = PreventAIConfiguration.objects.get( - organization_id=self.org.id, provider="github", git_organization=self.git_org + organization_id=self.org.id, service="github", git_organization_id=self.git_org ) assert config.data == VALID_ORG_CONFIG - @mock.patch("sentry.api.base.create_audit_entry") + @mock.patch( + "sentry.prevent.endpoints.pr_review_config.OrganizationPreventGitHubConfigEndpoint.create_audit_entry" + ) def test_audit_entry_created(self, mock_create_audit_entry): self.client.put(self.url, data=VALID_ORG_CONFIG, format="json") assert mock_create_audit_entry.called From 160fbd664e4e4f815f6914ab3fd1e04beba23190 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Wed, 22 Oct 2025 17:09:43 -0400 Subject: [PATCH 10/11] rename to provider column --- src/sentry/prevent/endpoints/pr_review_config.py | 6 +++--- .../0001_initial_prevent_ai_configuration.py | 6 +++--- src/sentry/prevent/models.py | 4 ++-- .../sentry/prevent/endpoints/test_pr_review_config.py | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/sentry/prevent/endpoints/pr_review_config.py b/src/sentry/prevent/endpoints/pr_review_config.py index aa5d4669ccf7d3..9a3a16ab939819 100644 --- a/src/sentry/prevent/endpoints/pr_review_config.py +++ b/src/sentry/prevent/endpoints/pr_review_config.py @@ -46,7 +46,7 @@ def get( config = PreventAIConfiguration.objects.filter( organization_id=organization.id, - service="github", + provider="github", git_organization_id=git_organization_id, ).first() @@ -68,7 +68,7 @@ def put( PreventAIConfiguration.objects.update_or_create( organization_id=organization.id, - service="github", + provider="github", git_organization_id=git_organization_id, defaults={"data": request.data}, ) @@ -80,7 +80,7 @@ def put( event=audit_log.get_event_id("PREVENT_CONFIG_EDIT"), data={ "git_organization": git_organization_id, - "service": "github", + "provider": "github", }, ) diff --git a/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py b/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py index 0b113f8a541226..6cbb6bf3727fc3 100644 --- a/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py +++ b/src/sentry/prevent/migrations/0001_initial_prevent_ai_configuration.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.1 on 2025-10-22 17:57 +# Generated by Django 5.2.1 on 2025-10-22 21:03 from django.db import migrations, models @@ -41,13 +41,13 @@ class Migration(CheckedMigration): "organization_id", sentry.db.models.fields.bounded.BoundedBigIntegerField(db_index=True), ), - ("service", models.CharField(max_length=64)), + ("provider", models.CharField(max_length=64)), ("git_organization_id", models.CharField(max_length=255)), ("data", models.JSONField(default=dict)), ], options={ "db_table": "sentry_preventaiconfiguration", - "unique_together": {("organization_id", "service", "git_organization_id")}, + "unique_together": {("organization_id", "provider", "git_organization_id")}, }, ), ] diff --git a/src/sentry/prevent/models.py b/src/sentry/prevent/models.py index f5154257e56076..a1427b81697d64 100644 --- a/src/sentry/prevent/models.py +++ b/src/sentry/prevent/models.py @@ -19,11 +19,11 @@ class PreventAIConfiguration(DefaultFieldsModel): __relocation_scope__ = RelocationScope.Excluded organization_id = BoundedBigIntegerField(db_index=True) - service = models.CharField(max_length=64) + provider = models.CharField(max_length=64) git_organization_id = models.CharField(max_length=255) data = models.JSONField(default=dict) class Meta: app_label = "prevent" db_table = "sentry_preventaiconfiguration" - unique_together = (("organization_id", "service", "git_organization_id"),) + unique_together = (("organization_id", "provider", "git_organization_id"),) diff --git a/tests/sentry/prevent/endpoints/test_pr_review_config.py b/tests/sentry/prevent/endpoints/test_pr_review_config.py index a62fe3a8235932..3e20c0b38f0607 100644 --- a/tests/sentry/prevent/endpoints/test_pr_review_config.py +++ b/tests/sentry/prevent/endpoints/test_pr_review_config.py @@ -50,7 +50,7 @@ def test_get_returns_default_when_no_config(self): def test_get_returns_config_when_exists(self): PreventAIConfiguration.objects.create( organization_id=self.org.id, - service="github", + provider="github", git_organization_id=self.git_org, data=VALID_ORG_CONFIG, ) @@ -70,14 +70,14 @@ def test_put_with_valid_config_creates_entry(self): assert resp.data["github_organization"][self.git_org] == VALID_ORG_CONFIG config = PreventAIConfiguration.objects.get( - organization_id=self.org.id, service="github", git_organization_id=self.git_org + organization_id=self.org.id, provider="github", git_organization_id=self.git_org ) assert config.data == VALID_ORG_CONFIG def test_put_updates_existing_config(self): PreventAIConfiguration.objects.create( organization_id=self.org.id, - service="github", + provider="github", git_organization_id=self.git_org, data={"org_defaults": {"bug_prediction": {"enabled": False}}, "repo_overrides": {}}, ) @@ -87,13 +87,13 @@ def test_put_updates_existing_config(self): assert ( PreventAIConfiguration.objects.filter( - organization_id=self.org.id, service="github", git_organization_id=self.git_org + organization_id=self.org.id, provider="github", git_organization_id=self.git_org ).count() == 1 ) config = PreventAIConfiguration.objects.get( - organization_id=self.org.id, service="github", git_organization_id=self.git_org + organization_id=self.org.id, provider="github", git_organization_id=self.git_org ) assert config.data == VALID_ORG_CONFIG From d9ce12775f8bc5aed1210a62c4ca87b86c85eae8 Mon Sep 17 00:00:00 2001 From: Jerry Feng Date: Wed, 22 Oct 2025 17:45:42 -0400 Subject: [PATCH 11/11] fix audit event --- src/sentry/audit_log/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/audit_log/register.py b/src/sentry/audit_log/register.py index a006038e41e012..f417d9ee0b3141 100644 --- a/src/sentry/audit_log/register.py +++ b/src/sentry/audit_log/register.py @@ -632,7 +632,7 @@ event_id=203, name="PREVENT_CONFIG_EDIT", api_name="prevent.config.edit", - template="prevent_ai.config.edit: {service} {git_organization}", + template="prevent_ai.config.edit: {provider} {git_organization}", ) ) default_manager.add(