Skip to content

Commit f24820a

Browse files
jennmuengjjbayer
authored andcommitted
feat(seer-autofix): remember last used rca action (#102399)
Remember a user's last used root cause card action in the user options <img width="914" height="322" alt="CleanShot 2025-10-30 at 21 18 27@2x" src="https://github.com/user-attachments/assets/bcfe843a-f707-4f39-a3eb-e020e66e66e4" /> <img width="772" height="360" alt="CleanShot 2025-10-30 at 21 16 51@2x" src="https://github.com/user-attachments/assets/862572cc-86c8-4228-9901-a341e750c138" /> Will still show [Find Solution] if you don't have any integrations configured. Paired with #102400
1 parent 6320fae commit f24820a

File tree

3 files changed

+295
-90
lines changed

3 files changed

+295
-90
lines changed
Lines changed: 178 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {AutofixRootCauseData} from 'sentry-fixture/autofixRootCauseData';
22

3-
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
3+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
44

55
import {AutofixRootCause} from 'sentry/components/events/autofix/autofixRootCause';
66
import {AutofixStatus} from 'sentry/components/events/autofix/types';
77

88
describe('AutofixRootCause', () => {
99
beforeEach(() => {
10+
localStorage.clear();
1011
MockApiClient.addMockResponse({
1112
url: '/organizations/org-slug/issues/1/autofix/update/',
1213
method: 'POST',
@@ -19,6 +20,7 @@ describe('AutofixRootCause', () => {
1920
});
2021

2122
afterEach(() => {
23+
localStorage.clear();
2224
MockApiClient.clearMockResponses();
2325
jest.clearAllTimers();
2426
});
@@ -35,35 +37,20 @@ describe('AutofixRootCause', () => {
3537
render(<AutofixRootCause {...defaultProps} />);
3638

3739
// Wait for initial render and animations
38-
await waitFor(
39-
() => {
40-
expect(screen.getByText('Root Cause')).toBeInTheDocument();
41-
},
42-
{timeout: 2000}
43-
);
40+
expect(await screen.findByText('Root Cause')).toBeInTheDocument();
4441

45-
await waitFor(
46-
() => {
47-
expect(
48-
screen.getByText(defaultProps.causes[0]!.root_cause_reproduction![0]!.title)
49-
).toBeInTheDocument();
50-
},
51-
{timeout: 2000}
52-
);
42+
expect(
43+
await screen.findByText(defaultProps.causes[0]!.root_cause_reproduction![0]!.title)
44+
).toBeInTheDocument();
5345

5446
await userEvent.click(screen.getByTestId('autofix-root-cause-timeline-item-0'));
5547

5648
// Wait for code snippet to appear with increased timeout for animation
57-
await waitFor(
58-
() => {
59-
expect(
60-
screen.getByText(
61-
defaultProps.causes[0]!.root_cause_reproduction![0]!.code_snippet_and_analysis
62-
)
63-
).toBeInTheDocument();
64-
},
65-
{timeout: 2000}
66-
);
49+
expect(
50+
await screen.findByText(
51+
defaultProps.causes[0]!.root_cause_reproduction![0]!.code_snippet_and_analysis
52+
)
53+
).toBeInTheDocument();
6754
});
6855

6956
it('shows graceful error state when there are no causes', async () => {
@@ -78,16 +65,11 @@ describe('AutofixRootCause', () => {
7865
);
7966

8067
// Wait for error state to render
81-
await waitFor(
82-
() => {
83-
expect(
84-
screen.getByText(
85-
'No root cause found. The error comes from outside the codebase.'
86-
)
87-
).toBeInTheDocument();
88-
},
89-
{timeout: 2000}
90-
);
68+
expect(
69+
await screen.findByText(
70+
'No root cause found. The error comes from outside the codebase.'
71+
)
72+
).toBeInTheDocument();
9173
});
9274

9375
it('shows selected root cause when rootCauseSelection is provided', async () => {
@@ -104,20 +86,171 @@ describe('AutofixRootCause', () => {
10486
);
10587

10688
// Wait for selected root cause to render
107-
await waitFor(
108-
() => {
109-
expect(screen.getByText('Root Cause')).toBeInTheDocument();
89+
expect(await screen.findByText('Root Cause')).toBeInTheDocument();
90+
91+
expect(
92+
await screen.findByText(selectedCause.root_cause_reproduction![0]!.title)
93+
).toBeInTheDocument();
94+
});
95+
96+
it('saves preference when clicking Find Solution with Seer', async () => {
97+
MockApiClient.addMockResponse({
98+
url: '/organizations/org-slug/integrations/coding-agents/',
99+
body: {
100+
integrations: [
101+
{
102+
id: 'cursor-integration-id',
103+
name: 'Cursor',
104+
provider: 'cursor',
105+
},
106+
],
110107
},
111-
{timeout: 2000}
108+
});
109+
110+
render(<AutofixRootCause {...defaultProps} />);
111+
112+
await userEvent.click(
113+
await screen.findByRole('button', {name: 'Find Solution with Seer'})
112114
);
113115

114-
await waitFor(
115-
() => {
116-
expect(
117-
screen.getByText(selectedCause.root_cause_reproduction![0]!.title)
118-
).toBeInTheDocument();
116+
expect(JSON.parse(localStorage.getItem('autofix:rootCauseActionPreference')!)).toBe(
117+
'seer_solution'
118+
);
119+
});
120+
121+
it('saves preference when clicking Cursor agent', async () => {
122+
MockApiClient.addMockResponse({
123+
url: '/organizations/org-slug/integrations/coding-agents/',
124+
body: {
125+
integrations: [
126+
{
127+
id: 'cursor-integration-id',
128+
name: 'Cursor',
129+
provider: 'cursor',
130+
},
131+
],
119132
},
120-
{timeout: 2000}
133+
});
134+
135+
MockApiClient.addMockResponse({
136+
url: '/organizations/org-slug/integrations/coding-agents/',
137+
method: 'POST',
138+
body: {success: true},
139+
});
140+
141+
render(<AutofixRootCause {...defaultProps} />);
142+
143+
// Find and open the dropdown
144+
const dropdownTrigger = await screen.findByRole('button', {
145+
name: 'More solution options',
146+
});
147+
await userEvent.click(dropdownTrigger);
148+
149+
// Click the Cursor option in the dropdown
150+
await userEvent.click(await screen.findByText('Send to Cursor Background Agent'));
151+
152+
expect(JSON.parse(localStorage.getItem('autofix:rootCauseActionPreference')!)).toBe(
153+
'cursor_background_agent'
121154
);
122155
});
156+
157+
it('shows Seer as primary button by default', async () => {
158+
render(<AutofixRootCause {...defaultProps} />);
159+
160+
expect(
161+
await screen.findByRole('button', {name: 'Find Solution'})
162+
).toBeInTheDocument();
163+
});
164+
165+
it('shows Seer as primary when preference is seer', async () => {
166+
MockApiClient.addMockResponse({
167+
url: '/organizations/org-slug/integrations/coding-agents/',
168+
body: {
169+
integrations: [
170+
{
171+
id: 'cursor-integration-id',
172+
name: 'Cursor',
173+
provider: 'cursor',
174+
},
175+
],
176+
},
177+
});
178+
179+
localStorage.setItem(
180+
'autofix:rootCauseActionPreference',
181+
JSON.stringify('seer_solution')
182+
);
183+
184+
render(<AutofixRootCause {...defaultProps} />);
185+
186+
expect(
187+
await screen.findByRole('button', {name: 'Find Solution with Seer'})
188+
).toBeInTheDocument();
189+
});
190+
191+
it('shows Cursor as primary when preference is cursor', async () => {
192+
MockApiClient.addMockResponse({
193+
url: '/organizations/org-slug/integrations/coding-agents/',
194+
body: {
195+
integrations: [
196+
{
197+
id: 'cursor-integration-id',
198+
name: 'Cursor',
199+
provider: 'cursor',
200+
},
201+
],
202+
},
203+
});
204+
205+
localStorage.setItem(
206+
'autofix:rootCauseActionPreference',
207+
JSON.stringify('cursor_background_agent')
208+
);
209+
210+
render(<AutofixRootCause {...defaultProps} />);
211+
212+
expect(
213+
await screen.findByRole('button', {name: 'Send to Cursor Background Agent'})
214+
).toBeInTheDocument();
215+
216+
// Verify Seer option is in the dropdown
217+
const dropdownTrigger = await screen.findByRole('button', {
218+
name: 'More solution options',
219+
});
220+
await userEvent.click(dropdownTrigger);
221+
222+
expect(await screen.findByText('Find Solution with Seer')).toBeInTheDocument();
223+
});
224+
225+
it('both options accessible in dropdown', async () => {
226+
MockApiClient.addMockResponse({
227+
url: '/organizations/org-slug/integrations/coding-agents/',
228+
body: {
229+
integrations: [
230+
{
231+
id: 'cursor-integration-id',
232+
name: 'Cursor',
233+
provider: 'cursor',
234+
},
235+
],
236+
},
237+
});
238+
239+
render(<AutofixRootCause {...defaultProps} />);
240+
241+
// Primary button is Seer (when cursor integration exists, show "with Seer" to distinguish)
242+
expect(
243+
await screen.findByRole('button', {name: 'Find Solution with Seer'})
244+
).toBeInTheDocument();
245+
246+
// Open dropdown to find Cursor option
247+
const dropdownTrigger = await screen.findByRole('button', {
248+
name: 'More solution options',
249+
});
250+
await userEvent.click(dropdownTrigger);
251+
252+
expect(
253+
await screen.findByText('Send to Cursor Background Agent')
254+
).toBeInTheDocument();
255+
});
123256
});

0 commit comments

Comments
 (0)