Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions static/app/views/settings/projectSeer/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ProjectSeer />, {
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(<ProjectSeer />, {
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(<ProjectSeer />, {
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(<ProjectSeer />, {
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(<ProjectSeer />, {
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'}})
);
});
});
});
});
47 changes: 39 additions & 8 deletions static/app/views/settings/projectSeer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,29 @@ 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<PropertyKey, unknown>) => ({
autofixAutomationTuning: data.autofixAutomationTuning ? 'always' : 'off',
}),
} satisfies FieldObject;

function ProjectSeerGeneralForm({project}: {project: Project}) {
const organization = useOrganization();
const queryClient = useQueryClient();
const {preference} = useProjectSeerPreferences(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(
Expand Down Expand Up @@ -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[] = [
{
Expand All @@ -222,7 +248,9 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
),
fields: [
...(isTriageSignalsFeatureOn ? [] : [seerScannerAutomationField]),
autofixAutomatingTuningField,
isTriageSignalsFeatureOn
? autofixAutomationToggleField
: autofixAutomatingTuningField,
automatedRunStoppingPointField,
],
},
Expand All @@ -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'),
Expand Down
Loading