Skip to content

Commit 1b4efba

Browse files
feat(seer): Add Auto-open PR and Cursor handoff toggles for triage-signals-v0 [feature flagged] (#103932)
## PR Details + When the triage-signals-v0 feature flag is enabled, replace the "Where should Seer stop?" dropdown with two simpler toggles: + Auto-open PR: Controls whether Seer automatically opens PRs (off=code_changes, on=open_pr) + Hand off to Cursor: Enables Cursor cloud agent handoff at root cause + The toggles are mutually exclusive - enabling one disables the other. + Screenshots: + <img width="2151" height="1167" alt="Screenshot 2025-11-24 at 2 08 09 PM" src="https://github.com/user-attachments/assets/02a960f1-3963-40ff-888e-a64346ac9e13" /> + <img width="2068" height="1118" alt="Screenshot 2025-11-24 at 2 07 52 PM" src="https://github.com/user-attachments/assets/165d5bc6-cd18-4f43-a88b-f8c36185da49" /> + Will rebase after [this one](#103730) merged
1 parent 49f4da0 commit 1b4efba

File tree

2 files changed

+543
-2
lines changed

2 files changed

+543
-2
lines changed

static/app/views/settings/projectSeer/index.spec.tsx

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
within,
1313
} from 'sentry-test/reactTestingLibrary';
1414

15+
import * as indicators from 'sentry/actionCreators/indicator';
1516
import type {SeerPreferencesResponse} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences';
1617
import type {Organization} from 'sentry/types/organization';
1718
import type {Project} from 'sentry/types/project';
@@ -1224,4 +1225,385 @@ describe('ProjectSeer', () => {
12241225
).not.toBeInTheDocument();
12251226
});
12261227
});
1228+
1229+
describe('Auto-open PR and Cursor Handoff toggles with triage-signals-v0', () => {
1230+
it('shows Auto-open PR toggle when Auto-Trigger is ON', async () => {
1231+
render(<ProjectSeer />, {
1232+
organization,
1233+
outletContext: {
1234+
project: ProjectFixture({
1235+
features: ['triage-signals-v0'],
1236+
autofixAutomationTuning: 'medium',
1237+
}),
1238+
},
1239+
});
1240+
1241+
await screen.findByText(/Automation/i);
1242+
expect(screen.getByRole('checkbox', {name: /Auto-open PR/i})).toBeInTheDocument();
1243+
});
1244+
1245+
it('hides Auto-open PR toggle when Auto-Trigger is OFF', async () => {
1246+
render(<ProjectSeer />, {
1247+
organization,
1248+
outletContext: {
1249+
project: ProjectFixture({
1250+
features: ['triage-signals-v0'],
1251+
autofixAutomationTuning: 'off',
1252+
}),
1253+
},
1254+
});
1255+
1256+
await screen.findByText(/Automation/i);
1257+
expect(
1258+
screen.queryByRole('checkbox', {name: /Auto-open PR/i})
1259+
).not.toBeInTheDocument();
1260+
});
1261+
1262+
it('shows Cursor handoff toggle when Auto-Trigger is ON and Cursor integration exists', async () => {
1263+
const orgWithCursor = OrganizationFixture({
1264+
features: ['autofix-seer-preferences', 'integrations-cursor'],
1265+
});
1266+
1267+
MockApiClient.addMockResponse({
1268+
url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`,
1269+
method: 'GET',
1270+
body: {
1271+
setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true},
1272+
billing: {hasAutofixQuota: true, hasScannerQuota: true},
1273+
},
1274+
});
1275+
1276+
MockApiClient.addMockResponse({
1277+
url: `/organizations/${orgWithCursor.slug}/repos/`,
1278+
query: {status: 'active'},
1279+
method: 'GET',
1280+
body: [],
1281+
});
1282+
1283+
MockApiClient.addMockResponse({
1284+
url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`,
1285+
method: 'GET',
1286+
body: {code_mapping_repos: []},
1287+
});
1288+
1289+
MockApiClient.addMockResponse({
1290+
url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`,
1291+
method: 'GET',
1292+
body: {
1293+
integrations: [{id: '123', name: 'Cursor', provider: 'cursor'}],
1294+
},
1295+
});
1296+
1297+
render(<ProjectSeer />, {
1298+
organization: orgWithCursor,
1299+
outletContext: {
1300+
project: ProjectFixture({
1301+
features: ['triage-signals-v0'],
1302+
autofixAutomationTuning: 'medium',
1303+
}),
1304+
},
1305+
});
1306+
1307+
await screen.findByText(/Automation/i);
1308+
expect(
1309+
screen.getByRole('checkbox', {name: /Hand off to Cursor/i})
1310+
).toBeInTheDocument();
1311+
});
1312+
1313+
it('hides Cursor handoff toggle when no Cursor integration', async () => {
1314+
render(<ProjectSeer />, {
1315+
organization,
1316+
outletContext: {
1317+
project: ProjectFixture({
1318+
features: ['triage-signals-v0'],
1319+
autofixAutomationTuning: 'medium',
1320+
}),
1321+
},
1322+
});
1323+
1324+
await screen.findByText(/Automation/i);
1325+
expect(
1326+
screen.queryByRole('checkbox', {name: /Hand off to Cursor/i})
1327+
).not.toBeInTheDocument();
1328+
});
1329+
1330+
it('updates preferences when Auto-open PR toggle is changed', async () => {
1331+
MockApiClient.addMockResponse({
1332+
url: `/projects/${organization.slug}/${project.slug}/`,
1333+
method: 'PUT',
1334+
body: {},
1335+
});
1336+
1337+
const seerPreferencesPostRequest = MockApiClient.addMockResponse({
1338+
url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`,
1339+
method: 'POST',
1340+
});
1341+
1342+
render(<ProjectSeer />, {
1343+
organization,
1344+
outletContext: {
1345+
project: ProjectFixture({
1346+
features: ['triage-signals-v0'],
1347+
autofixAutomationTuning: 'medium',
1348+
}),
1349+
},
1350+
});
1351+
1352+
const toggle = await screen.findByRole('checkbox', {name: /Auto-open PR/i});
1353+
await userEvent.click(toggle);
1354+
1355+
await waitFor(() => {
1356+
expect(seerPreferencesPostRequest).toHaveBeenCalledWith(
1357+
expect.anything(),
1358+
expect.objectContaining({
1359+
data: expect.objectContaining({
1360+
automated_run_stopping_point: 'open_pr',
1361+
automation_handoff: undefined,
1362+
}),
1363+
})
1364+
);
1365+
});
1366+
});
1367+
1368+
it('updates preferences when Cursor handoff toggle is changed', async () => {
1369+
const orgWithCursor = OrganizationFixture({
1370+
features: ['autofix-seer-preferences', 'integrations-cursor'],
1371+
});
1372+
1373+
MockApiClient.addMockResponse({
1374+
url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`,
1375+
method: 'GET',
1376+
body: {
1377+
setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true},
1378+
billing: {hasAutofixQuota: true, hasScannerQuota: true},
1379+
},
1380+
});
1381+
1382+
MockApiClient.addMockResponse({
1383+
url: `/organizations/${orgWithCursor.slug}/repos/`,
1384+
query: {status: 'active'},
1385+
method: 'GET',
1386+
body: [],
1387+
});
1388+
1389+
MockApiClient.addMockResponse({
1390+
url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`,
1391+
method: 'GET',
1392+
body: {code_mapping_repos: []},
1393+
});
1394+
1395+
MockApiClient.addMockResponse({
1396+
url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`,
1397+
method: 'GET',
1398+
body: {
1399+
integrations: [{id: '123', name: 'Cursor', provider: 'cursor'}],
1400+
},
1401+
});
1402+
1403+
MockApiClient.addMockResponse({
1404+
url: `/projects/${orgWithCursor.slug}/${project.slug}/`,
1405+
method: 'PUT',
1406+
body: {},
1407+
});
1408+
1409+
const seerPreferencesPostRequest = MockApiClient.addMockResponse({
1410+
url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`,
1411+
method: 'POST',
1412+
});
1413+
1414+
render(<ProjectSeer />, {
1415+
organization: orgWithCursor,
1416+
outletContext: {
1417+
project: ProjectFixture({
1418+
features: ['triage-signals-v0'],
1419+
autofixAutomationTuning: 'medium',
1420+
}),
1421+
},
1422+
});
1423+
1424+
const toggle = await screen.findByRole('checkbox', {name: /Hand off to Cursor/i});
1425+
await userEvent.click(toggle);
1426+
1427+
await waitFor(() => {
1428+
expect(seerPreferencesPostRequest).toHaveBeenCalledWith(
1429+
expect.anything(),
1430+
expect.objectContaining({
1431+
data: expect.objectContaining({
1432+
automated_run_stopping_point: 'root_cause',
1433+
automation_handoff: {
1434+
handoff_point: 'root_cause',
1435+
target: 'cursor_background_agent',
1436+
integration_id: 123,
1437+
auto_create_pr: false,
1438+
},
1439+
}),
1440+
})
1441+
);
1442+
});
1443+
});
1444+
1445+
it('shows error when Cursor handoff fails due to missing integration', async () => {
1446+
const orgWithCursor = OrganizationFixture({
1447+
features: ['autofix-seer-preferences', 'integrations-cursor'],
1448+
});
1449+
1450+
MockApiClient.addMockResponse({
1451+
url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`,
1452+
method: 'GET',
1453+
body: {
1454+
setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true},
1455+
billing: {hasAutofixQuota: true, hasScannerQuota: true},
1456+
},
1457+
});
1458+
1459+
MockApiClient.addMockResponse({
1460+
url: `/organizations/${orgWithCursor.slug}/repos/`,
1461+
query: {status: 'active'},
1462+
method: 'GET',
1463+
body: [],
1464+
});
1465+
1466+
MockApiClient.addMockResponse({
1467+
url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`,
1468+
method: 'GET',
1469+
body: {code_mapping_repos: []},
1470+
});
1471+
1472+
// Mock integrations endpoint returning empty array (no Cursor integration)
1473+
MockApiClient.addMockResponse({
1474+
url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`,
1475+
method: 'GET',
1476+
body: {integrations: []},
1477+
});
1478+
1479+
render(<ProjectSeer />, {
1480+
organization: orgWithCursor,
1481+
outletContext: {
1482+
project: ProjectFixture({
1483+
features: ['triage-signals-v0'],
1484+
autofixAutomationTuning: 'medium',
1485+
}),
1486+
},
1487+
});
1488+
1489+
await screen.findByText(/Automation/i);
1490+
1491+
// Toggle should not be visible when no Cursor integration exists
1492+
expect(
1493+
screen.queryByRole('checkbox', {name: /Hand off to Cursor/i})
1494+
).not.toBeInTheDocument();
1495+
});
1496+
1497+
it('shows error message when Auto-open PR toggle fails', async () => {
1498+
jest.spyOn(indicators, 'addErrorMessage');
1499+
1500+
MockApiClient.addMockResponse({
1501+
url: `/projects/${organization.slug}/${project.slug}/`,
1502+
method: 'PUT',
1503+
body: {},
1504+
});
1505+
1506+
const seerPreferencesPostRequest = MockApiClient.addMockResponse({
1507+
url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`,
1508+
method: 'POST',
1509+
statusCode: 500,
1510+
body: {detail: 'Internal Server Error'},
1511+
});
1512+
1513+
render(<ProjectSeer />, {
1514+
organization,
1515+
outletContext: {
1516+
project: ProjectFixture({
1517+
features: ['triage-signals-v0'],
1518+
autofixAutomationTuning: 'medium',
1519+
}),
1520+
},
1521+
});
1522+
1523+
const toggle = await screen.findByRole('checkbox', {name: /Auto-open PR/i});
1524+
await userEvent.click(toggle);
1525+
1526+
await waitFor(() => {
1527+
expect(seerPreferencesPostRequest).toHaveBeenCalled();
1528+
});
1529+
1530+
// Should show error message
1531+
expect(indicators.addErrorMessage).toHaveBeenCalledWith(
1532+
'Failed to update auto-open PR setting'
1533+
);
1534+
});
1535+
1536+
it('shows error message when Cursor handoff toggle fails', async () => {
1537+
jest.spyOn(indicators, 'addErrorMessage');
1538+
1539+
const orgWithCursor = OrganizationFixture({
1540+
features: ['autofix-seer-preferences', 'integrations-cursor'],
1541+
});
1542+
1543+
MockApiClient.addMockResponse({
1544+
url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`,
1545+
method: 'GET',
1546+
body: {
1547+
setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true},
1548+
billing: {hasAutofixQuota: true, hasScannerQuota: true},
1549+
},
1550+
});
1551+
1552+
MockApiClient.addMockResponse({
1553+
url: `/organizations/${orgWithCursor.slug}/repos/`,
1554+
query: {status: 'active'},
1555+
method: 'GET',
1556+
body: [],
1557+
});
1558+
1559+
MockApiClient.addMockResponse({
1560+
url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`,
1561+
method: 'GET',
1562+
body: {code_mapping_repos: []},
1563+
});
1564+
1565+
MockApiClient.addMockResponse({
1566+
url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`,
1567+
method: 'GET',
1568+
body: {
1569+
integrations: [{id: '123', name: 'Cursor', provider: 'cursor'}],
1570+
},
1571+
});
1572+
1573+
MockApiClient.addMockResponse({
1574+
url: `/projects/${orgWithCursor.slug}/${project.slug}/`,
1575+
method: 'PUT',
1576+
body: {},
1577+
});
1578+
1579+
const seerPreferencesPostRequest = MockApiClient.addMockResponse({
1580+
url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`,
1581+
method: 'POST',
1582+
statusCode: 500,
1583+
body: {detail: 'Internal Server Error'},
1584+
});
1585+
1586+
render(<ProjectSeer />, {
1587+
organization: orgWithCursor,
1588+
outletContext: {
1589+
project: ProjectFixture({
1590+
features: ['triage-signals-v0'],
1591+
autofixAutomationTuning: 'medium',
1592+
}),
1593+
},
1594+
});
1595+
1596+
const toggle = await screen.findByRole('checkbox', {name: /Hand off to Cursor/i});
1597+
await userEvent.click(toggle);
1598+
1599+
await waitFor(() => {
1600+
expect(seerPreferencesPostRequest).toHaveBeenCalled();
1601+
});
1602+
1603+
// Should show error message
1604+
expect(indicators.addErrorMessage).toHaveBeenCalledWith(
1605+
'Failed to update Cursor handoff setting'
1606+
);
1607+
});
1608+
});
12271609
});

0 commit comments

Comments
 (0)