diff --git a/static/app/views/settings/projectSeer/index.spec.tsx b/static/app/views/settings/projectSeer/index.spec.tsx index 4578e19a11597a..d0a8159a57fabb 100644 --- a/static/app/views/settings/projectSeer/index.spec.tsx +++ b/static/app/views/settings/projectSeer/index.spec.tsx @@ -583,4 +583,118 @@ describe('ProjectSeer', () => { }); expect(toggle).toBeInTheDocument(); }); + + describe('Auto-Trigger Fixes with triage-signals-v0', () => { + it('shows as toggle when flag enabled, dropdown when disabled', async () => { + const projectWithFlag = ProjectFixture({ + features: ['triage-signals-v0'], + seerScannerAutomation: true, + autofixAutomationTuning: 'off', + }); + + const {unmount} = render(, { + organization, + outletContext: {project: projectWithFlag}, + }); + + await screen.findByText(/Automation/i); + expect( + screen.getByRole('checkbox', {name: /Auto-Trigger Fixes/i}) + ).toBeInTheDocument(); + expect( + screen.queryByRole('textbox', {name: /Auto-Trigger Fixes/i}) + ).not.toBeInTheDocument(); + + unmount(); + + render(, { + organization, + outletContext: { + project: ProjectFixture({ + seerScannerAutomation: true, + autofixAutomationTuning: 'high', + }), + }, + }); + + await screen.findByText(/Automation/i); + expect( + screen.getByRole('textbox', {name: /Auto-Trigger Fixes/i}) + ).toBeInTheDocument(); + expect( + screen.queryByRole('checkbox', {name: /Auto-Trigger Fixes/i}) + ).not.toBeInTheDocument(); + }); + + it('maps values correctly: off=unchecked, others=checked', async () => { + const {unmount} = render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + seerScannerAutomation: true, + autofixAutomationTuning: 'off', + }), + }, + }); + + expect( + await screen.findByRole('checkbox', {name: /Auto-Trigger Fixes/i}) + ).not.toBeChecked(); + unmount(); + + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + seerScannerAutomation: true, + autofixAutomationTuning: 'high', + }), + }, + }); + + expect( + await screen.findByRole('checkbox', {name: /Auto-Trigger Fixes/i}) + ).toBeChecked(); + }); + + it('saves "always" when toggled ON, "off" when toggled OFF', async () => { + const projectPutRequest = MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/`, + method: 'PUT', + body: {}, + }); + + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + seerScannerAutomation: true, + autofixAutomationTuning: 'off', + }), + }, + }); + + const toggle = await screen.findByRole('checkbox', {name: /Auto-Trigger Fixes/i}); + await userEvent.click(toggle); + + await waitFor(() => { + expect(projectPutRequest).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({data: {autofixAutomationTuning: 'always'}}) + ); + }); + + await userEvent.click(toggle); + + await waitFor(() => { + expect(projectPutRequest).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({data: {autofixAutomationTuning: 'off'}}) + ); + }); + }); + }); }); diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx index 7a482282c13f3e..e440068764c0c2 100644 --- a/static/app/views/settings/projectSeer/index.tsx +++ b/static/app/views/settings/projectSeer/index.tsx @@ -84,6 +84,21 @@ export const autofixAutomatingTuningField = { visible: ({model}) => model?.getValue('seerScannerAutomation') === true, } satisfies FieldObject; +const autofixAutomationToggleField = { + name: 'autofixAutomationTuning', + label: t('Auto-Trigger Fixes'), + help: () => + t( + 'When enabled, Seer will automatically analyze actionable issues in the background.' + ), + type: 'boolean', + saveOnBlur: true, + saveMessage: t('Automatic Seer settings updated'), + getData: (data: Record) => ({ + autofixAutomationTuning: data.autofixAutomationTuning ? 'always' : 'off', + }), +} satisfies FieldObject; + function ProjectSeerGeneralForm({project}: {project: Project}) { const organization = useOrganization(); const queryClient = useQueryClient(); @@ -91,6 +106,7 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { const {mutate: updateProjectSeerPreferences} = useUpdateProjectSeerPreferences(project); const {data: codingAgentIntegrations} = useCodingAgentIntegrations(); + const isTriageSignalsFeatureOn = project.features.includes('triage-signals-v0'); const canWriteProject = hasEveryAccess(['project:read'], {organization, project}); const cursorIntegration = codingAgentIntegrations?.integrations.find( @@ -190,12 +206,22 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { saveOnBlur: true, saveMessage: t('Stopping point updated'), onChange: handleStoppingPointChange, - visible: ({model}) => - model?.getValue('seerScannerAutomation') === true && - model?.getValue('autofixAutomationTuning') !== 'off', - } satisfies FieldObject; + visible: ({model}) => { + const tuningValue = model?.getValue('autofixAutomationTuning'); + // Handle both boolean (toggle) and string (dropdown) values + const automationEnabled = + typeof tuningValue === 'boolean' ? tuningValue : tuningValue !== 'off'; - const isTriageSignalsFeatureOn = project.features.includes('triage-signals-v0'); + // When feature flag is ON (toggle mode): only check automation + // When feature flag is OFF (dropdown mode): check both scanner and automation + if (isTriageSignalsFeatureOn) { + return automationEnabled; + } + + const scannerEnabled = model?.getValue('seerScannerAutomation') === true; + return scannerEnabled && automationEnabled; + }, + } satisfies FieldObject; const seerFormGroups: JsonFormObject[] = [ { @@ -222,7 +248,9 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { ), fields: [ ...(isTriageSignalsFeatureOn ? [] : [seerScannerAutomationField]), - autofixAutomatingTuningField, + isTriageSignalsFeatureOn + ? autofixAutomationToggleField + : autofixAutomatingTuningField, automatedRunStoppingPointField, ], }, @@ -235,14 +263,17 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { preference?.automation_handoff ? 'cursor_handoff' : (preference?.automated_run_stopping_point ?? 'root_cause') - }`} + }-${isTriageSignalsFeatureOn}`} saveOnBlur apiMethod="PUT" apiEndpoint={`/projects/${organization.slug}/${project.slug}/`} allowUndo initialData={{ seerScannerAutomation: project.seerScannerAutomation ?? false, - autofixAutomationTuning: project.autofixAutomationTuning ?? 'off', + // Same DB field, different UI: toggle (boolean) vs dropdown (string) + autofixAutomationTuning: isTriageSignalsFeatureOn + ? (project.autofixAutomationTuning ?? 'off') !== 'off' + : (project.autofixAutomationTuning ?? 'off'), automated_run_stopping_point: preference?.automation_handoff ? 'cursor_handoff' : (preference?.automated_run_stopping_point ?? 'root_cause'),