Skip to content

Commit e10ca7f

Browse files
fix(config): Hook up to new prevent ai config apis
1 parent dd6f8c2 commit e10ca7f

File tree

9 files changed

+227
-86
lines changed

9 files changed

+227
-86
lines changed

static/app/types/organization.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {WidgetType} from 'sentry/views/dashboards/types';
99
import type {Actor, Avatar, ObjectStatus, Scope} from './core';
1010
import type {ExternalTeam} from './integrations';
1111
import type {OnboardingTaskStatus} from './onboarding';
12-
import type {PreventAIConfig} from './prevent';
1312
import type {Project} from './project';
1413
import type {Relay} from './relay';
1514
import type {User} from './user';
@@ -114,7 +113,6 @@ export interface Organization extends OrganizationSummary {
114113
};
115114
orgRole?: string;
116115
planSampleRate?: number | null;
117-
preventAiConfigGithub?: PreventAIConfig;
118116
}
119117

120118
export interface Team {

static/app/types/prevent.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,8 @@ export interface PreventAIFeatureConfigsByName {
1717
vanilla: PreventAIFeatureConfig; // "vanilla" basic ai pr review
1818
}
1919

20-
export interface PreventAIOrgConfig {
20+
export interface PreventAIConfig {
2121
org_defaults: PreventAIFeatureConfigsByName;
2222
repo_overrides: Record<string, PreventAIFeatureConfigsByName>;
23-
}
24-
25-
export interface PreventAIConfig {
26-
default_org_config: PreventAIOrgConfig;
27-
github_organizations: Record<string, PreventAIOrgConfig>;
2823
schema_version: string;
2924
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
3+
import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
4+
5+
import {usePreventAIGitHubConfig} from './usePreventAIConfig';
6+
7+
describe('usePreventAIGitHubConfig', () => {
8+
const org = OrganizationFixture({slug: 'test-org'});
9+
const gitOrgName = 'octo-corp';
10+
11+
beforeEach(() => {
12+
MockApiClient.clearMockResponses();
13+
});
14+
15+
it('fetches config from API correctly', async () => {
16+
const configResponse = {
17+
default_org_config: {
18+
org_defaults: {
19+
vanilla: {enabled: true, sensitivity: 'medium'},
20+
test_generation: {enabled: false, sensitivity: 'low'},
21+
bug_prediction: {enabled: false, sensitivity: 'high'},
22+
},
23+
repo_overrides: {
24+
'repo-123': {
25+
vanilla: {enabled: false, sensitivity: 'high'},
26+
test_generation: {enabled: true, sensitivity: 'critical'},
27+
bug_prediction: {enabled: false, sensitivity: 'medium'},
28+
},
29+
},
30+
schema_version: 'v1',
31+
},
32+
organization: {
33+
'octo-corp': {
34+
org_defaults: {
35+
vanilla: {enabled: false, sensitivity: 'low'},
36+
test_generation: {enabled: true, sensitivity: 'medium'},
37+
bug_prediction: {enabled: true, sensitivity: 'high'},
38+
},
39+
repo_overrides: {},
40+
schema_version: 'v1',
41+
},
42+
},
43+
schema_version: 'v1',
44+
};
45+
46+
MockApiClient.addMockResponse({
47+
url: `/organizations/${org.slug}/prevent/ai/github/config/${gitOrgName}/`,
48+
body: configResponse,
49+
});
50+
51+
const {result} = renderHookWithProviders(
52+
() => usePreventAIGitHubConfig({gitOrgName}),
53+
{
54+
organization: org,
55+
}
56+
);
57+
58+
await waitFor(() => {
59+
expect(result.current.isPending).toBe(false);
60+
});
61+
62+
expect(result.current.data).toMatchObject(configResponse);
63+
expect(result.current.isError).toBeFalsy();
64+
});
65+
66+
it('sets isError when API fails', async () => {
67+
MockApiClient.addMockResponse({
68+
url: `/organizations/${org.slug}/prevent/ai/github/config/${gitOrgName}/`,
69+
statusCode: 500,
70+
});
71+
72+
const {result} = renderHookWithProviders(
73+
() => usePreventAIGitHubConfig({gitOrgName}),
74+
{
75+
organization: org,
76+
}
77+
);
78+
79+
await waitFor(() => {
80+
expect(result.current.isPending).toBe(false);
81+
});
82+
83+
expect(result.current.isError).toBe(true);
84+
expect(result.current.data).toBeUndefined();
85+
});
86+
87+
it('uses correct API URL for different org', async () => {
88+
const diffOrg = OrganizationFixture({slug: 'diff-org'});
89+
90+
MockApiClient.addMockResponse({
91+
url: `/organizations/diff-org/prevent/ai/github/config/${gitOrgName}/`,
92+
body: {},
93+
});
94+
95+
const {result} = renderHookWithProviders(
96+
() => usePreventAIGitHubConfig({gitOrgName}),
97+
{
98+
organization: diffOrg,
99+
}
100+
);
101+
102+
await waitFor(() => {
103+
expect(result.current.isPending).toBe(false);
104+
});
105+
106+
expect(result.current.isError).toBeFalsy();
107+
});
108+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type {PreventAIConfig} from 'sentry/types/prevent';
2+
import {useApiQuery} from 'sentry/utils/queryClient';
3+
import useOrganization from 'sentry/utils/useOrganization';
4+
5+
interface UsePreventAIGitHubConfigOptions {
6+
gitOrgName: string;
7+
}
8+
9+
export function usePreventAIGitHubConfig({gitOrgName}: UsePreventAIGitHubConfigOptions) {
10+
const organization = useOrganization();
11+
12+
return useApiQuery<{
13+
default_org_config: PreventAIConfig;
14+
organization: Record<string, PreventAIConfig>;
15+
schema_version: string;
16+
}>([`/organizations/${organization.slug}/prevent/ai/github/config/${gitOrgName}/`], {
17+
staleTime: 30000,
18+
});
19+
}

static/app/views/prevent/preventAI/hooks/usePreventAIOrgRepos.spec.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {OrganizationFixture} from 'sentry-fixture/organization';
22
import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations';
3-
import {PreventAIConfigFixture} from 'sentry-fixture/prevent';
43

54
import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
65

@@ -9,9 +8,7 @@ import type {OrganizationIntegration} from 'sentry/types/integrations';
98
import {usePreventAIOrgs} from './usePreventAIOrgRepos';
109

1110
describe('usePreventAIOrgRepos', () => {
12-
const mockOrg = OrganizationFixture({
13-
preventAiConfigGithub: PreventAIConfigFixture(),
14-
});
11+
const mockOrg = OrganizationFixture();
1512

1613
const mockResponse: OrganizationIntegration[] = [
1714
OrganizationIntegrationsFixture({
Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import {updateOrganization} from 'sentry/actionCreators/organizations';
2-
import type {Organization} from 'sentry/types/organization';
31
import type {
42
PreventAIConfig,
53
PreventAIFeatureTriggers,
64
Sensitivity,
75
} from 'sentry/types/prevent';
8-
import {fetchMutation, useMutation} from 'sentry/utils/queryClient';
6+
import {fetchMutation, useMutation, useQueryClient} from 'sentry/utils/queryClient';
97
import useOrganization from 'sentry/utils/useOrganization';
108

119
interface UpdatePreventAIFeatureParams {
1210
enabled: boolean;
1311
// 'use_org_defaults' is a special case that will remove the entire repo override
1412
feature: 'vanilla' | 'test_generation' | 'bug_prediction' | 'use_org_defaults';
15-
orgId: string;
13+
gitOrgName: string;
14+
originalConfig: PreventAIConfig;
1615
// if repo is provided, edit repo_overrides for that repo, otherwise edit org_defaults
1716
repoId?: string;
1817
sensitivity?: Sensitivity;
@@ -21,20 +20,28 @@ interface UpdatePreventAIFeatureParams {
2120

2221
export function useUpdatePreventAIFeature() {
2322
const organization = useOrganization();
23+
const queryClient = useQueryClient();
24+
2425
const {mutateAsync, isPending, error} = useMutation({
2526
mutationFn: async (params: UpdatePreventAIFeatureParams) => {
26-
if (!organization.preventAiConfigGithub) {
27-
throw new Error('Organization has no AI Code Review config');
28-
}
29-
const newConfig = makePreventAIConfig(organization.preventAiConfigGithub, params);
27+
const newOrgConfig = makePreventAIOrgConfig(params.originalConfig, params);
3028

31-
return fetchMutation<Partial<Organization>>({
29+
return fetchMutation<{
30+
default_org_config: PreventAIConfig;
31+
organization: Record<string, PreventAIConfig>;
32+
}>({
3233
method: 'PUT',
33-
url: `/organizations/${organization.slug}/`,
34-
data: {preventAiConfigGithub: newConfig},
34+
url: `/organizations/${organization.slug}/prevent/ai/github/config/${params.gitOrgName}/`,
35+
data: newOrgConfig as unknown as Record<string, PreventAIConfig>,
36+
});
37+
},
38+
onSuccess: (_data, variables) => {
39+
queryClient.invalidateQueries({
40+
queryKey: [
41+
`/organizations/${organization.slug}/prevent/ai/github/config/${variables.gitOrgName}/`,
42+
],
3543
});
3644
},
37-
onSuccess: updateOrganization,
3845
});
3946

4047
return {
@@ -45,46 +52,49 @@ export function useUpdatePreventAIFeature() {
4552
}
4653

4754
/**
48-
* Makes a new PreventAIConfig object with feature settings applied for the specified org and/or repo
55+
* Makes a new PreventAIOrgConfig object with feature settings applied for the specified repo or org defaults
4956
* 1. Deep clones the original config to prevent mutation
50-
* 2. Get the org config for the specified org or create it from default_org_config template if not exists
51-
* 3. If editing repo, get the repo override for the specified repo or create it from org_defaults template if not exists
52-
* 4. Modifies the specified feature's settings, preserves any unspecified settings.
53-
* 5. Special case: 'use_org_defaults' feature type will remove the entire repo override
57+
* 2. If editing repo, get the repo override for the specified repo or create it from org_defaults template if not exists
58+
* 3. Modifies the specified feature's settings, preserves any unspecified settings.
59+
* 4. Special case: 'use_org_defaults' feature type will remove the entire repo override
5460
*
55-
* @param originalConfig Original PreventAIConfig object (will not be mutated)
61+
* @param originalConfig Original PreventAIOrgConfig object (will not be mutated)
5662
* @param params Parameters to update
57-
* @returns New (copy of) PreventAIConfig object with updates applied
63+
* @returns New (copy of) PreventAIOrgConfig object with updates applied
5864
*/
59-
export function makePreventAIConfig(
65+
export function makePreventAIOrgConfig(
6066
originalConfig: PreventAIConfig,
6167
params: UpdatePreventAIFeatureParams
62-
) {
63-
const updatedConfig = structuredClone(originalConfig);
68+
): PreventAIConfig {
69+
const updatedOrgConfig = structuredClone(originalConfig);
70+
if (!updatedOrgConfig.schema_version) {
71+
updatedOrgConfig.schema_version = 'v1';
72+
}
6473

65-
const orgConfig =
66-
updatedConfig.github_organizations[params.orgId] ??
67-
structuredClone(updatedConfig.default_org_config);
68-
updatedConfig.github_organizations[params.orgId] = orgConfig;
74+
if (!updatedOrgConfig.repo_overrides) {
75+
updatedOrgConfig.repo_overrides = {};
76+
}
6977

7078
if (params.feature === 'use_org_defaults') {
7179
if (!params.repoId) {
7280
throw new Error('Repo name is required when feature is use_org_defaults');
7381
}
7482
if (params.enabled) {
75-
delete orgConfig.repo_overrides[params.repoId];
83+
delete updatedOrgConfig.repo_overrides[params.repoId];
7684
} else {
77-
orgConfig.repo_overrides[params.repoId] = structuredClone(orgConfig.org_defaults);
85+
updatedOrgConfig.repo_overrides[params.repoId] = structuredClone(
86+
updatedOrgConfig.org_defaults
87+
);
7888
}
79-
return updatedConfig;
89+
return updatedOrgConfig;
8090
}
8191

82-
let featureConfig = orgConfig.org_defaults;
92+
let featureConfig = updatedOrgConfig.org_defaults;
8393
if (params.repoId) {
84-
let repoOverride = orgConfig.repo_overrides[params.repoId];
94+
let repoOverride = updatedOrgConfig.repo_overrides[params.repoId];
8595
if (!repoOverride) {
86-
repoOverride = structuredClone(orgConfig.org_defaults);
87-
orgConfig.repo_overrides[params.repoId] = repoOverride;
96+
repoOverride = structuredClone(updatedOrgConfig.org_defaults);
97+
updatedOrgConfig.repo_overrides[params.repoId] = repoOverride;
8898
}
8999
featureConfig = repoOverride;
90100
}
@@ -95,5 +105,5 @@ export function makePreventAIConfig(
95105
sensitivity: params.sensitivity ?? featureConfig[params.feature].sensitivity,
96106
};
97107

98-
return updatedConfig;
108+
return updatedOrgConfig;
99109
}

static/app/views/prevent/preventAI/manageRepos.spec.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {OrganizationFixture} from 'sentry-fixture/organization';
22
import {OrganizationIntegrationsFixture} from 'sentry-fixture/organizationIntegrations';
3-
import {PreventAIConfigFixture} from 'sentry-fixture/prevent';
43

54
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
65

@@ -24,9 +23,7 @@ describe('PreventAIManageRepos', () => {
2423
}),
2524
];
2625

27-
const organization = OrganizationFixture({
28-
preventAiConfigGithub: PreventAIConfigFixture(),
29-
});
26+
const organization = OrganizationFixture();
3027

3128
beforeEach(() => {
3229
MockApiClient.addMockResponse({

0 commit comments

Comments
 (0)