fix(cli): prevent race condition when restoring prompt after context overflow (#13473)

This commit is contained in:
Sandy Tao
2025-11-20 13:54:16 +08:00
committed by GitHub
parent 98cdaa01b8
commit b1258dd52c
2 changed files with 82 additions and 1 deletions

View File

@@ -1834,5 +1834,62 @@ describe('AppContainer State Management', () => {
unmount();
});
it('correctly restores prompt even if userMessages is stale (race condition fix)', async () => {
// Setup initial history with one message
const initialHistory = [{ type: 'user', text: 'Previous Prompt' }];
mockedUseHistory.mockReturnValue({
history: initialHistory,
addItem: vi.fn(),
updateItem: vi.fn(),
clearItems: vi.fn(),
loadHistory: vi.fn(),
});
// Mock logger to resolve so userMessages gets populated
mockedUseLogger.mockReturnValue({
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
});
const { unmount, rerender } = renderAppContainer();
// Wait for userMessages to be populated with 'Previous Prompt'
await waitFor(() =>
expect(capturedUIState.userMessages).toContain('Previous Prompt'),
);
// Simulate a new prompt being added (e.g., user sent it, but it overflowed)
const newPrompt = 'Current Prompt that Overflowed';
const newHistory = [...initialHistory, { type: 'user', text: newPrompt }];
mockedUseHistory.mockReturnValue({
history: newHistory,
addItem: vi.fn(),
updateItem: vi.fn(),
clearItems: vi.fn(),
loadHistory: vi.fn(),
});
// Rerender to reflect the history change.
// This triggers the effect to update userMessages, but it's async.
rerender(getAppContainer());
const { onCancelSubmit } = extractUseGeminiStreamArgs(
mockedUseGeminiStream.mock.lastCall!,
);
// Call onCancelSubmit immediately (simulating the race condition where
// the overflow event comes in before the effect updates userMessages)
act(() => {
onCancelSubmit(true);
});
// With the fix, it should wait for userMessages to update and then set the new prompt
await waitFor(() => {
expect(mockSetText).toHaveBeenCalledWith(newPrompt);
});
unmount();
});
});
});

View File

@@ -170,6 +170,7 @@ export const AppContainer = (props: AppContainerProps) => {
null,
);
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false);
const [shellModeActive, setShellModeActive] = useState(false);
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
@@ -666,9 +667,32 @@ Logging in with Google... Please restart Gemini CLI to continue.
);
const onCancelSubmit = useCallback((shouldRestorePrompt?: boolean) => {
cancelHandlerRef.current(shouldRestorePrompt);
if (shouldRestorePrompt) {
setPendingRestorePrompt(true);
} else {
setPendingRestorePrompt(false);
cancelHandlerRef.current(false);
}
}, []);
useEffect(() => {
if (pendingRestorePrompt) {
const lastHistoryUserMsg = historyManager.history.findLast(
(h) => h.type === 'user',
);
const lastUserMsg = userMessages.at(-1);
if (
!lastHistoryUserMsg ||
(typeof lastHistoryUserMsg.text === 'string' &&
lastHistoryUserMsg.text === lastUserMsg)
) {
cancelHandlerRef.current(true);
setPendingRestorePrompt(false);
}
}
}, [pendingRestorePrompt, userMessages, historyManager.history]);
const {
streamingState,
submitQuery,