Fixed auto question timer unmount (#5368)

* fixed bug

* expanded tests and made better mock

* code review: refactor & race conditions

* code review, some refactor and reset timer on task switch

* rename to onCancelAutoApproval
This commit is contained in:
Will Li 2025-07-08 17:19:22 -07:00 committed by GitHub
parent 917b2cc882
commit 15586d3846
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 514 additions and 18 deletions

View file

@ -53,6 +53,7 @@ interface ChatRowProps {
onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void
onBatchFileResponse?: (response: { [key: string]: boolean }) => void
onFollowUpUnmount?: () => void
isFollowUpAnswered?: boolean
editable?: boolean
}
@ -104,6 +105,7 @@ export const ChatRowContent = ({
onSuggestionClick,
onFollowUpUnmount,
onBatchFileResponse,
isFollowUpAnswered,
editable,
}: ChatRowContentProps) => {
const { t } = useTranslation()
@ -1298,7 +1300,8 @@ export const ChatRowContent = ({
suggestions={followUpData?.suggest}
onSuggestionClick={onSuggestionClick}
ts={message?.ts}
onUnmount={onFollowUpUnmount}
onCancelAutoApproval={onFollowUpUnmount}
isAnswered={isFollowUpAnswered}
/>
</>
)

View file

@ -169,6 +169,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}),
)
const autoApproveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const userRespondedRef = useRef<boolean>(false)
const [currentFollowUpTs, setCurrentFollowUpTs] = useState<number | null>(null)
const clineAskRef = useRef(clineAsk)
useEffect(() => {
@ -415,6 +417,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
useEffect(() => {
setExpandedRows({})
everVisibleMessagesTsRef.current.clear() // Clear for new task
setCurrentFollowUpTs(null) // Clear follow-up answered state for new task
// Clear any pending auto-approval timeout from previous task
if (autoApproveTimeoutRef.current) {
clearTimeout(autoApproveTimeoutRef.current)
autoApproveTimeoutRef.current = null
}
// Reset user response flag for new task
userRespondedRef.current = false
}, [task?.ts])
useEffect(() => {
@ -486,7 +497,22 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return false
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
const markFollowUpAsAnswered = useCallback(() => {
const lastFollowUpMessage = messagesRef.current.findLast((msg) => msg.ask === "followup")
if (lastFollowUpMessage) {
setCurrentFollowUpTs(lastFollowUpMessage.ts)
}
}, [])
const handleChatReset = useCallback(() => {
// Clear any pending auto-approval timeout
if (autoApproveTimeoutRef.current) {
clearTimeout(autoApproveTimeoutRef.current)
autoApproveTimeoutRef.current = null
}
// Reset user response flag for new message
userRespondedRef.current = false
// Only reset message-specific state, preserving mode.
setInputValue("")
setSendingDisabled(true)
@ -504,9 +530,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
text = text.trim()
if (text || images.length > 0) {
// Mark that user has responded - this prevents any pending auto-approvals
userRespondedRef.current = true
if (messagesRef.current.length === 0) {
vscode.postMessage({ type: "newTask", text, images })
} else if (clineAskRef.current) {
if (clineAskRef.current === "followup") {
markFollowUpAsAnswered()
}
// Use clineAskRef.current
switch (
clineAskRef.current // Use clineAskRef.current
@ -530,7 +563,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
handleChatReset()
}
},
[handleChatReset], // messagesRef and clineAskRef are stable
[handleChatReset, markFollowUpAsAnswered], // messagesRef and clineAskRef are stable
)
const handleSetChatBoxMessage = useCallback(
@ -555,6 +588,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// extension.
const handlePrimaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
// Mark that user has responded
userRespondedRef.current = true
const trimmedInput = text?.trim()
switch (clineAsk) {
@ -599,6 +635,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const handleSecondaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
// Mark that user has responded
userRespondedRef.current = true
const trimmedInput = text?.trim()
if (isStreaming) {
@ -1219,6 +1258,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const handleSuggestionClickInRow = useCallback(
(suggestion: SuggestionItem, event?: React.MouseEvent) => {
// Mark that user has responded if this is a manual click (not auto-approval)
if (event) {
userRespondedRef.current = true
}
// Mark the current follow-up question as answered when a suggestion is clicked
if (clineAsk === "followup" && !event?.shiftKey) {
markFollowUpAsAnswered()
}
// Check if we need to switch modes
if (suggestion.mode) {
// Only switch modes if it's a manual click (event exists) or auto-approval is allowed
@ -1238,7 +1287,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
handleSendMessage(suggestion.answer, [])
}
},
[handleSendMessage, setInputValue, switchToMode, alwaysAllowModeSwitch],
[handleSendMessage, setInputValue, switchToMode, alwaysAllowModeSwitch, clineAsk, markFollowUpAsAnswered],
)
const handleBatchFileResponse = useCallback((response: { [key: string]: boolean }) => {
@ -1248,11 +1297,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// Handler for when FollowUpSuggest component unmounts
const handleFollowUpUnmount = useCallback(() => {
// Clear the auto-approve timeout to prevent race conditions
if (autoApproveTimeoutRef.current) {
clearTimeout(autoApproveTimeoutRef.current)
autoApproveTimeoutRef.current = null
}
// Mark that user has responded
userRespondedRef.current = true
}, [])
const itemContent = useCallback(
@ -1291,6 +1337,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
onBatchFileResponse={handleBatchFileResponse}
onFollowUpUnmount={handleFollowUpUnmount}
isFollowUpAnswered={messageOrGroup.ts === currentFollowUpTs}
editable={
messageOrGroup.type === "ask" &&
messageOrGroup.ask === "tool" &&
@ -1322,6 +1369,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
handleSuggestionClickInRow,
handleBatchFileResponse,
handleFollowUpUnmount,
currentFollowUpTs,
alwaysAllowUpdateTodoList,
enableButtons,
primaryButtonText,
@ -1338,6 +1386,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return
}
// Exit early if user has already responded
if (userRespondedRef.current) {
return
}
const autoApprove = async () => {
if (lastMessage?.ask && isAutoApproved(lastMessage)) {
// Special handling for follow-up questions
@ -1354,9 +1407,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
if (followUpData && followUpData.suggest && followUpData.suggest.length > 0) {
// Wait for the configured timeout before auto-selecting the first suggestion
await new Promise<void>((resolve) => {
autoApproveTimeoutRef.current = setTimeout(resolve, followupAutoApproveTimeoutMs)
autoApproveTimeoutRef.current = setTimeout(() => {
autoApproveTimeoutRef.current = null
resolve()
}, followupAutoApproveTimeoutMs)
})
// Check if user responded manually
if (userRespondedRef.current) {
return
}
// Get the first suggestion
const firstSuggestion = followUpData.suggest[0]
@ -1366,7 +1427,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
} else if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) {
await new Promise<void>((resolve) => {
autoApproveTimeoutRef.current = setTimeout(resolve, writeDelayMs)
autoApproveTimeoutRef.current = setTimeout(() => {
autoApproveTimeoutRef.current = null
resolve()
}, writeDelayMs)
})
}

View file

@ -14,10 +14,17 @@ interface FollowUpSuggestProps {
suggestions?: SuggestionItem[]
onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void
ts: number
onUnmount?: () => void
onCancelAutoApproval?: () => void
isAnswered?: boolean
}
export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, onUnmount }: FollowUpSuggestProps) => {
export const FollowUpSuggest = ({
suggestions = [],
onSuggestionClick,
ts = 1,
onCancelAutoApproval,
isAnswered = false,
}: FollowUpSuggestProps) => {
const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState()
const [countdown, setCountdown] = useState<number | null>(null)
const [suggestionSelected, setSuggestionSelected] = useState(false)
@ -26,7 +33,14 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, o
// Start countdown timer when auto-approval is enabled for follow-up questions
useEffect(() => {
// Only start countdown if auto-approval is enabled for follow-up questions and no suggestion has been selected
if (autoApprovalEnabled && alwaysAllowFollowupQuestions && suggestions.length > 0 && !suggestionSelected) {
// Also stop countdown if the question has been answered
if (
autoApprovalEnabled &&
alwaysAllowFollowupQuestions &&
suggestions.length > 0 &&
!suggestionSelected &&
!isAnswered
) {
// Start with the configured timeout in seconds
const timeoutMs =
typeof followupAutoApproveTimeoutMs === "number" && !isNaN(followupAutoApproveTimeoutMs)
@ -52,7 +66,7 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, o
clearInterval(intervalId)
// Notify parent component that this component is unmounting
// so it can clear any related timeouts
onUnmount?.()
onCancelAutoApproval?.()
}
} else {
setCountdown(null)
@ -63,7 +77,8 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, o
suggestions,
followupAutoApproveTimeoutMs,
suggestionSelected,
onUnmount,
onCancelAutoApproval,
isAnswered,
])
const handleSuggestionClick = useCallback(
(suggestion: SuggestionItem, event: React.MouseEvent) => {
@ -72,14 +87,14 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, o
setSuggestionSelected(true)
// Also notify parent component to cancel auto-approval timeout
// This prevents race conditions between visual countdown and actual timeout
onUnmount?.()
onCancelAutoApproval?.()
}
// Pass the suggestion object to the parent component
// The parent component will handle mode switching if needed
onSuggestionClick?.(suggestion, event)
},
[onSuggestionClick, onUnmount],
[onSuggestionClick, onCancelAutoApproval],
)
// Don't render if there are no suggestions or no click handler.
@ -100,7 +115,7 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, o
onClick={(event) => handleSuggestionClick(suggestion, event)}
aria-label={suggestion.answer}>
{suggestion.answer}
{isFirstSuggestion && countdown !== null && !suggestionSelected && (
{isFirstSuggestion && countdown !== null && !suggestionSelected && !isAnswered && (
<span
className="ml-2 px-1.5 py-0.5 text-xs rounded-full bg-vscode-badge-background text-vscode-badge-foreground"
title={t("chat:followUpSuggest.autoSelectCountdown", { count: countdown })}>

View file

@ -0,0 +1,414 @@
import React, { createContext, useContext } from "react"
import { render, screen, act } from "@testing-library/react"
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { FollowUpSuggest } from "../FollowUpSuggest"
import { TooltipProvider } from "@radix-ui/react-tooltip"
// Mock the translation hook
vi.mock("@src/i18n/TranslationContext", () => ({
TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
useAppTranslation: () => ({
t: (key: string, options?: any) => {
if (key === "chat:followUpSuggest.countdownDisplay" && options?.count !== undefined) {
return `${options.count}s`
}
if (key === "chat:followUpSuggest.autoSelectCountdown" && options?.count !== undefined) {
return `Auto-selecting in ${options.count} seconds`
}
if (key === "chat:followUpSuggest.copyToInput") {
return "Copy to input"
}
return key
},
}),
}))
// Test-specific extension state context that only provides the values needed by FollowUpSuggest
interface TestExtensionState {
autoApprovalEnabled: boolean
alwaysAllowFollowupQuestions: boolean
followupAutoApproveTimeoutMs: number
}
const TestExtensionStateContext = createContext<TestExtensionState | undefined>(undefined)
// Mock the useExtensionState hook to use our test context
vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: () => {
const context = useContext(TestExtensionStateContext)
if (!context) {
throw new Error("useExtensionState must be used within TestExtensionStateProvider")
}
return context
},
}))
// Test provider that only provides the specific values needed by FollowUpSuggest
const TestExtensionStateProvider: React.FC<{
children: React.ReactNode
value: TestExtensionState
}> = ({ children, value }) => {
return <TestExtensionStateContext.Provider value={value}>{children}</TestExtensionStateContext.Provider>
}
// Helper function to render component with test providers
const renderWithTestProviders = (component: React.ReactElement, extensionState: TestExtensionState) => {
return render(
<TestExtensionStateProvider value={extensionState}>
<TooltipProvider>{component}</TooltipProvider>
</TestExtensionStateProvider>,
)
}
describe("FollowUpSuggest", () => {
const mockSuggestions = [{ answer: "First suggestion" }, { answer: "Second suggestion" }]
const mockOnSuggestionClick = vi.fn()
const mockOnCancelAutoApproval = vi.fn()
// Default test state with auto-approval enabled
const defaultTestState: TestExtensionState = {
autoApprovalEnabled: true,
alwaysAllowFollowupQuestions: true,
followupAutoApproveTimeoutMs: 3000, // 3 seconds for testing
}
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it("should display countdown timer when auto-approval is enabled", () => {
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
defaultTestState,
)
// Should show initial countdown (3 seconds)
expect(screen.getByText(/3s/)).toBeInTheDocument()
})
it("should not display countdown timer when isAnswered is true", () => {
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={true}
/>,
defaultTestState,
)
// Should not show countdown
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})
it("should clear interval and call onCancelAutoApproval when component unmounts", () => {
const { unmount } = renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
defaultTestState,
)
// Unmount the component
unmount()
// onCancelAutoApproval should have been called
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
})
it("should not show countdown when auto-approval is disabled", () => {
const testState: TestExtensionState = {
...defaultTestState,
autoApprovalEnabled: false,
}
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
testState,
)
// Should not show countdown
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})
it("should not show countdown when alwaysAllowFollowupQuestions is false", () => {
const testState: TestExtensionState = {
...defaultTestState,
alwaysAllowFollowupQuestions: false,
}
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
testState,
)
// Should not show countdown
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})
it("should use custom timeout value from extension state", () => {
const testState: TestExtensionState = {
...defaultTestState,
followupAutoApproveTimeoutMs: 5000, // 5 seconds
}
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
testState,
)
// Should show initial countdown (5 seconds)
expect(screen.getByText(/5s/)).toBeInTheDocument()
})
it("should render suggestions without countdown when both auto-approval settings are disabled", () => {
const testState: TestExtensionState = {
autoApprovalEnabled: false,
alwaysAllowFollowupQuestions: false,
followupAutoApproveTimeoutMs: 3000,
}
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
testState,
)
// Should render suggestions
expect(screen.getByText("First suggestion")).toBeInTheDocument()
expect(screen.getByText("Second suggestion")).toBeInTheDocument()
// Should not show countdown
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})
it("should not render when no suggestions are provided", () => {
const { container } = renderWithTestProviders(
<FollowUpSuggest
suggestions={[]}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
defaultTestState,
)
// Component should not render anything
expect(container.firstChild).toBeNull()
})
it("should not render when onSuggestionClick is not provided", () => {
const { container } = renderWithTestProviders(
<FollowUpSuggest suggestions={mockSuggestions} ts={123} onCancelAutoApproval={mockOnCancelAutoApproval} />,
defaultTestState,
)
// Component should not render anything
expect(container.firstChild).toBeNull()
})
it("should stop countdown when user manually responds (isAnswered becomes true)", () => {
const { rerender } = renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={false}
/>,
defaultTestState,
)
// Initially should show countdown
expect(screen.getByText(/3s/)).toBeInTheDocument()
// Simulate user manually responding by setting isAnswered to true
rerender(
<TestExtensionStateProvider value={defaultTestState}>
<TooltipProvider>
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={true}
/>
</TooltipProvider>
</TestExtensionStateProvider>,
)
// Countdown should no longer be visible immediately after isAnswered becomes true
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
// Advance timer to ensure countdown doesn't restart or continue
vi.advanceTimersByTime(5000)
// onSuggestionClick should not have been called (auto-selection stopped)
expect(mockOnSuggestionClick).not.toHaveBeenCalled()
// Countdown should still not be visible
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
// Verify onCancelAutoApproval was called when the countdown was stopped
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
})
it("should handle race condition when timeout fires but user has already responded", () => {
// This test simulates the scenario where:
// 1. Auto-approval countdown starts
// 2. User manually responds (isAnswered becomes true)
// 3. The timeout still fires (because it was already scheduled)
// 4. The auto-selection should NOT happen because user already responded
const { rerender } = renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={false}
/>,
defaultTestState,
)
// Initially should show countdown
expect(screen.getByText(/3s/)).toBeInTheDocument()
// Advance timer to just before timeout completes (2.5 seconds)
vi.advanceTimersByTime(2500)
// User manually responds before timeout completes
rerender(
<TestExtensionStateProvider value={defaultTestState}>
<TooltipProvider>
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={true}
/>
</TooltipProvider>
</TestExtensionStateProvider>,
)
// Countdown should be hidden immediately
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
// Now advance timer past the original timeout duration
vi.advanceTimersByTime(1000) // Total: 3.5 seconds
// onSuggestionClick should NOT have been called
// This verifies the fix for the race condition
expect(mockOnSuggestionClick).not.toHaveBeenCalled()
})
it("should update countdown display as time progresses", async () => {
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={false}
/>,
defaultTestState,
)
// Initially should show 3s
expect(screen.getByText(/3s/)).toBeInTheDocument()
// Advance timer by 1 second and wait for React to update
await act(async () => {
vi.advanceTimersByTime(1000)
})
// Check countdown updated to 2s
expect(screen.getByText(/2s/)).toBeInTheDocument()
// Advance timer by another second
await act(async () => {
vi.advanceTimersByTime(1000)
})
// Check countdown updated to 1s
expect(screen.getByText(/1s/)).toBeInTheDocument()
// Advance timer to completion - countdown should disappear
await act(async () => {
vi.advanceTimersByTime(1000)
})
// Countdown should no longer be visible after reaching 0
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
// The component itself doesn't trigger auto-selection, that's handled by ChatView
expect(mockOnSuggestionClick).not.toHaveBeenCalled()
})
it("should handle component unmounting during countdown", () => {
const { unmount } = renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
isAnswered={false}
/>,
defaultTestState,
)
// Initially should show countdown
expect(screen.getByText(/3s/)).toBeInTheDocument()
// Advance timer partially
vi.advanceTimersByTime(1500)
// Unmount component before countdown completes
unmount()
// onCancelAutoApproval should have been called
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
// Advance timer past the original timeout
vi.advanceTimersByTime(2000)
// onSuggestionClick should NOT have been called (component doesn't auto-select)
expect(mockOnSuggestionClick).not.toHaveBeenCalled()
})
})