From bd5e49c5ff0d0f5e03e5f42435de00c5a0758d7e Mon Sep 17 00:00:00 2001
From: Jacob Richman <jacob314@gmail.com>
Date: Fri, 15 Aug 2025 10:54:00 -0700
Subject: fix(input) Resolve cases where escape was broken (#6304)

---
 .../cli/src/ui/components/InputPrompt.test.tsx     | 156 +++++++++++++--------
 1 file changed, 99 insertions(+), 57 deletions(-)

(limited to 'packages/cli/src/ui/components/InputPrompt.test.tsx')

diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index ec2d7441..a4aaf6e9 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import { render } from 'ink-testing-library';
+import { renderWithProviders } from '../../test-utils/render.js';
 import { waitFor } from '@testing-library/react';
 import { InputPrompt, InputPromptProps } from './InputPrompt.js';
 import type { TextBuffer } from './shared/text-buffer.js';
@@ -197,7 +197,7 @@ describe('InputPrompt', () => {
 
   it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
     props.shellModeActive = true;
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\u001B[A');
@@ -209,7 +209,7 @@ describe('InputPrompt', () => {
 
   it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
     props.shellModeActive = true;
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\u001B[B');
@@ -224,7 +224,7 @@ describe('InputPrompt', () => {
     vi.mocked(mockShellHistory.getPreviousCommand).mockReturnValue(
       'previous command',
     );
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\u001B[A');
@@ -238,7 +238,7 @@ describe('InputPrompt', () => {
   it('should call shellHistory.addCommandToHistory on submit in shell mode', async () => {
     props.shellModeActive = true;
     props.buffer.setText('ls -l');
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r');
@@ -251,7 +251,7 @@ describe('InputPrompt', () => {
 
   it('should NOT call shell history methods when not in shell mode', async () => {
     props.buffer.setText('some text');
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\u001B[A'); // Up arrow
@@ -283,7 +283,7 @@ describe('InputPrompt', () => {
 
     props.buffer.setText('/mem');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     // Test up arrow
@@ -309,7 +309,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/mem');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     // Test down arrow
@@ -331,7 +331,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('some text');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\u001B[A'); // Up arrow
@@ -363,7 +363,9 @@ describe('InputPrompt', () => {
         '/test/.gemini-clipboard/clipboard-123.png',
       );
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       // Send Ctrl+V
@@ -384,7 +386,9 @@ describe('InputPrompt', () => {
     it('should not insert anything when clipboard has no image', async () => {
       vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x16'); // Ctrl+V
@@ -400,7 +404,9 @@ describe('InputPrompt', () => {
       vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
       vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x16'); // Ctrl+V
@@ -426,7 +432,9 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['Hello world'];
       mockBuffer.replaceRangeByOffset = vi.fn();
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x16'); // Ctrl+V
@@ -454,7 +462,9 @@ describe('InputPrompt', () => {
         new Error('Clipboard error'),
       );
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x16'); // Ctrl+V
@@ -481,7 +491,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/mem');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\t'); // Press Tab
@@ -504,7 +514,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/memory ');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\t'); // Press Tab
@@ -528,7 +538,7 @@ describe('InputPrompt', () => {
     // The user has backspaced, so the query is now just '/memory'
     props.buffer.setText('/memory');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\t'); // Press Tab
@@ -549,7 +559,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/chat resume fi-');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\t'); // Press Tab
@@ -568,7 +578,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/mem');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r');
@@ -599,7 +609,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/?');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\t'); // Press Tab for autocomplete
@@ -612,7 +622,7 @@ describe('InputPrompt', () => {
   it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
     props.buffer.setText('   '); // Set buffer to whitespace
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r'); // Press Enter
@@ -630,7 +640,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/clear');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r');
@@ -648,7 +658,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('/clear');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r');
@@ -667,7 +677,7 @@ describe('InputPrompt', () => {
     });
     props.buffer.setText('@src/components/');
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r');
@@ -684,7 +694,7 @@ describe('InputPrompt', () => {
     mockBuffer.cursor = [0, 11];
     mockBuffer.lines = ['first line\\'];
 
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\r');
@@ -698,7 +708,7 @@ describe('InputPrompt', () => {
 
   it('should clear the buffer on Ctrl+C if it has text', async () => {
     props.buffer.setText('some text to clear');
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\x03'); // Ctrl+C character
@@ -712,7 +722,7 @@ describe('InputPrompt', () => {
 
   it('should NOT clear the buffer on Ctrl+C if it is empty', async () => {
     props.buffer.text = '';
-    const { stdin, unmount } = render(<InputPrompt {...props} />);
+    const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
     await wait();
 
     stdin.write('\x03'); // Ctrl+C character
@@ -735,7 +745,7 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       // Verify useCompletion was called with correct signature
@@ -763,7 +773,7 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'show', value: 'show' }],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -790,7 +800,7 @@ describe('InputPrompt', () => {
         suggestions: [],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -817,7 +827,7 @@ describe('InputPrompt', () => {
         suggestions: [],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -844,7 +854,7 @@ describe('InputPrompt', () => {
         suggestions: [],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -871,7 +881,7 @@ describe('InputPrompt', () => {
         suggestions: [],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       // Verify useCompletion was called with the buffer
@@ -899,7 +909,7 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'show', value: 'show' }],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -927,7 +937,7 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'file👍.txt', value: 'file👍.txt' }],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -955,7 +965,7 @@ describe('InputPrompt', () => {
         suggestions: [],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -983,7 +993,7 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'my file.txt', value: 'my file.txt' }],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -1011,7 +1021,7 @@ describe('InputPrompt', () => {
         suggestions: [],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -1041,7 +1051,7 @@ describe('InputPrompt', () => {
         ],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -1069,7 +1079,7 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'test-command', value: 'test-command' }],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -1099,7 +1109,7 @@ describe('InputPrompt', () => {
         ],
       });
 
-      const { unmount } = render(<InputPrompt {...props} />);
+      const { unmount } = renderWithProviders(<InputPrompt {...props} />);
       await wait();
 
       expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -1120,7 +1130,9 @@ describe('InputPrompt', () => {
     it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
       props.vimModeEnabled = true;
       props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('i');
@@ -1134,7 +1146,9 @@ describe('InputPrompt', () => {
     it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => {
       props.vimModeEnabled = true;
       props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('i');
@@ -1148,7 +1162,9 @@ describe('InputPrompt', () => {
     it('should call handleInput when vim mode is disabled', async () => {
       // Mock vimHandleInput to return false (vim didn't handle the input)
       props.vimHandleInput = vi.fn().mockReturnValue(false);
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('i');
@@ -1163,7 +1179,9 @@ describe('InputPrompt', () => {
   describe('unfocused paste', () => {
     it('should handle bracketed paste when not focused', async () => {
       props.focus = false;
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x1B[200~pasted text\x1B[201~');
@@ -1180,7 +1198,9 @@ describe('InputPrompt', () => {
 
     it('should ignore regular keypresses when not focused', async () => {
       props.focus = false;
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('a');
@@ -1197,7 +1217,9 @@ describe('InputPrompt', () => {
       props.onEscapePromptChange = onEscapePromptChange;
       props.buffer.setText('text to clear');
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x1B');
@@ -1216,7 +1238,9 @@ describe('InputPrompt', () => {
       props.onEscapePromptChange = onEscapePromptChange;
       props.buffer.setText('some text');
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
 
       stdin.write('\x1B');
 
@@ -1235,7 +1259,9 @@ describe('InputPrompt', () => {
     it('should handle ESC in shell mode by disabling shell mode', async () => {
       props.shellModeActive = true;
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x1B');
@@ -1252,7 +1278,9 @@ describe('InputPrompt', () => {
         suggestions: [{ label: 'suggestion', value: 'suggestion' }],
       });
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x1B');
@@ -1266,7 +1294,9 @@ describe('InputPrompt', () => {
       props.onEscapePromptChange = undefined;
       props.buffer.setText('some text');
 
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x1B');
@@ -1276,7 +1306,9 @@ describe('InputPrompt', () => {
     });
 
     it('should not interfere with existing keyboard shortcuts', async () => {
-      const { stdin, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x0C');
@@ -1306,7 +1338,9 @@ describe('InputPrompt', () => {
     });
 
     it('invokes reverse search on Ctrl+R', async () => {
-      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, stdout, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x12');
@@ -1322,7 +1356,9 @@ describe('InputPrompt', () => {
     });
 
     it('resets reverse search state on Escape', async () => {
-      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, stdout, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       await wait();
 
       stdin.write('\x12');
@@ -1339,7 +1375,9 @@ describe('InputPrompt', () => {
     });
 
     it('completes the highlighted entry on Tab and exits reverse-search', async () => {
-      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, stdout, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       stdin.write('\x12');
       await wait();
       stdin.write('\t');
@@ -1353,7 +1391,9 @@ describe('InputPrompt', () => {
     });
 
     it('submits the highlighted entry on Enter and exits reverse-search', async () => {
-      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, stdout, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       stdin.write('\x12');
       await wait();
       expect(stdout.lastFrame()).toContain('(r:)');
@@ -1370,7 +1410,9 @@ describe('InputPrompt', () => {
     it('text and cursor position should be restored after reverse search', async () => {
       props.buffer.setText('initial text');
       props.buffer.cursor = [0, 3];
-      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      const { stdin, stdout, unmount } = renderWithProviders(
+        <InputPrompt {...props} />,
+      );
       stdin.write('\x12');
       await wait();
       expect(stdout.lastFrame()).toContain('(r:)');
-- 
cgit v1.2.3

