@@ -20,13 +20,16 @@ import type { UseCommandCompletionReturn } from '../hooks/useCommandCompletion.j
2020import { useCommandCompletion } from '../hooks/useCommandCompletion.js' ;
2121import type { UseInputHistoryReturn } from '../hooks/useInputHistory.js' ;
2222import { useInputHistory } from '../hooks/useInputHistory.js' ;
23+ import type { UseReverseSearchCompletionReturn } from '../hooks/useReverseSearchCompletion.js' ;
24+ import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js' ;
2325import * as clipboardUtils from '../utils/clipboardUtils.js' ;
2426import { createMockCommandContext } from '../../test-utils/mockCommandContext.js' ;
2527import chalk from 'chalk' ;
2628
2729vi . mock ( '../hooks/useShellHistory.js' ) ;
2830vi . mock ( '../hooks/useCommandCompletion.js' ) ;
2931vi . mock ( '../hooks/useInputHistory.js' ) ;
32+ vi . mock ( '../hooks/useReverseSearchCompletion.js' ) ;
3033vi . mock ( '../utils/clipboardUtils.js' ) ;
3134
3235const mockSlashCommands : SlashCommand [ ] = [
@@ -82,12 +85,16 @@ describe('InputPrompt', () => {
8285 let mockShellHistory : UseShellHistoryReturn ;
8386 let mockCommandCompletion : UseCommandCompletionReturn ;
8487 let mockInputHistory : UseInputHistoryReturn ;
88+ let mockReverseSearchCompletion : UseReverseSearchCompletionReturn ;
8589 let mockBuffer : TextBuffer ;
8690 let mockCommandContext : CommandContext ;
8791
8892 const mockedUseShellHistory = vi . mocked ( useShellHistory ) ;
8993 const mockedUseCommandCompletion = vi . mocked ( useCommandCompletion ) ;
9094 const mockedUseInputHistory = vi . mocked ( useInputHistory ) ;
95+ const mockedUseReverseSearchCompletion = vi . mocked (
96+ useReverseSearchCompletion ,
97+ ) ;
9198
9299 beforeEach ( ( ) => {
93100 vi . resetAllMocks ( ) ;
@@ -168,6 +175,21 @@ describe('InputPrompt', () => {
168175 } ;
169176 mockedUseInputHistory . mockReturnValue ( mockInputHistory ) ;
170177
178+ mockReverseSearchCompletion = {
179+ suggestions : [ ] ,
180+ activeSuggestionIndex : - 1 ,
181+ visibleStartIndex : 0 ,
182+ showSuggestions : false ,
183+ isLoadingSuggestions : false ,
184+ navigateUp : vi . fn ( ) ,
185+ navigateDown : vi . fn ( ) ,
186+ handleAutocomplete : vi . fn ( ) ,
187+ resetCompletionState : vi . fn ( ) ,
188+ } ;
189+ mockedUseReverseSearchCompletion . mockReturnValue (
190+ mockReverseSearchCompletion ,
191+ ) ;
192+
171193 props = {
172194 buffer : mockBuffer ,
173195 onSubmit : vi . fn ( ) ,
@@ -1375,6 +1397,77 @@ describe('InputPrompt', () => {
13751397 } ) ;
13761398 } ) ;
13771399
1400+ describe ( 'paste auto-submission protection' , ( ) => {
1401+ it ( 'should prevent auto-submission immediately after paste with newlines' , async ( ) => {
1402+ const { stdin, unmount } = renderWithProviders (
1403+ < InputPrompt { ...props } /> ,
1404+ ) ;
1405+ await wait ( ) ;
1406+
1407+ // First type some text manually
1408+ stdin . write ( 'test command' ) ;
1409+ await wait ( ) ;
1410+
1411+ // Simulate a paste operation (this should set the paste protection)
1412+ stdin . write ( `\x1b[200~\npasted content\x1b[201~` ) ;
1413+ await wait ( ) ;
1414+
1415+ // Simulate an Enter key press immediately after paste
1416+ stdin . write ( '\r' ) ;
1417+ await wait ( ) ;
1418+
1419+ // Verify that onSubmit was NOT called due to recent paste protection
1420+ expect ( props . onSubmit ) . not . toHaveBeenCalled ( ) ;
1421+
1422+ unmount ( ) ;
1423+ } ) ;
1424+
1425+ it ( 'should allow submission after paste protection timeout' , async ( ) => {
1426+ // Set up buffer with text for submission
1427+ props . buffer . text = 'test command' ;
1428+
1429+ const { stdin, unmount } = renderWithProviders (
1430+ < InputPrompt { ...props } /> ,
1431+ ) ;
1432+ await wait ( ) ;
1433+
1434+ // Simulate a paste operation (this sets the protection)
1435+ stdin . write ( `\x1b[200~\npasted\x1b[201~` ) ;
1436+ await wait ( ) ;
1437+
1438+ // Wait for the protection timeout to naturally expire
1439+ await new Promise ( ( resolve ) => setTimeout ( resolve , 600 ) ) ;
1440+
1441+ // Now Enter should work normally
1442+ stdin . write ( '\r' ) ;
1443+ await wait ( ) ;
1444+
1445+ // Verify that onSubmit was called after the timeout
1446+ expect ( props . onSubmit ) . toHaveBeenCalledWith ( 'test command' ) ;
1447+
1448+ unmount ( ) ;
1449+ } ) ;
1450+
1451+ it ( 'should not interfere with normal Enter key submission when no recent paste' , async ( ) => {
1452+ // Set up buffer with text before rendering to ensure submission works
1453+ props . buffer . text = 'normal command' ;
1454+
1455+ const { stdin, unmount } = renderWithProviders (
1456+ < InputPrompt { ...props } /> ,
1457+ ) ;
1458+ await wait ( ) ;
1459+
1460+ // Press Enter without any recent paste
1461+ stdin . write ( '\r' ) ;
1462+ await wait ( ) ;
1463+
1464+ // Verify that onSubmit was called normally
1465+ expect ( props . onSubmit ) . toHaveBeenCalledWith ( 'normal command' ) ;
1466+
1467+ unmount ( ) ;
1468+ } ) ;
1469+ } ) ;
1470+
13781471 describe ( 'enhanced input UX - double ESC clear functionality' , ( ) => {
13791472 it ( 'should clear buffer on second ESC press' , async ( ) => {
13801473 const onEscapePromptChange = vi . fn ( ) ;
@@ -1502,12 +1595,27 @@ describe('InputPrompt', () => {
15021595 } ) ;
15031596
15041597 it ( 'invokes reverse search on Ctrl+R' , async ( ) => {
1598+ // Mock the reverse search completion to return suggestions
1599+ mockedUseReverseSearchCompletion . mockReturnValue ( {
1600+ ...mockReverseSearchCompletion ,
1601+ suggestions : [
1602+ { label : 'echo hello' , value : 'echo hello' } ,
1603+ { label : 'echo world' , value : 'echo world' } ,
1604+ { label : 'ls' , value : 'ls' } ,
1605+ ] ,
1606+ showSuggestions : true ,
1607+ activeSuggestionIndex : 0 ,
1608+ } ) ;
1609+
15051610 const { stdin, stdout, unmount } = renderWithProviders (
15061611 < InputPrompt { ...props } /> ,
15071612 ) ;
15081613 await wait ( ) ;
15091614
1510- stdin . write ( '\x12' ) ;
1615+ // Trigger reverse search with Ctrl+R
1616+ act ( ( ) => {
1617+ stdin . write ( '\x12' ) ;
1618+ } ) ;
15111619 await wait ( ) ;
15121620
15131621 const frame = stdout . lastFrame ( ) ;
@@ -1539,6 +1647,27 @@ describe('InputPrompt', () => {
15391647 } ) ;
15401648
15411649 it ( 'completes the highlighted entry on Tab and exits reverse-search' , async ( ) => {
1650+ // Mock the reverse search completion
1651+ const mockHandleAutocomplete = vi . fn ( ( ) => {
1652+ props . buffer . setText ( 'echo hello' ) ;
1653+ } ) ;
1654+
1655+ mockedUseReverseSearchCompletion . mockImplementation (
1656+ ( buffer , shellHistory , reverseSearchActive ) => ( {
1657+ ...mockReverseSearchCompletion ,
1658+ suggestions : reverseSearchActive
1659+ ? [
1660+ { label : 'echo hello' , value : 'echo hello' } ,
1661+ { label : 'echo world' , value : 'echo world' } ,
1662+ { label : 'ls' , value : 'ls' } ,
1663+ ]
1664+ : [ ] ,
1665+ showSuggestions : reverseSearchActive ,
1666+ activeSuggestionIndex : reverseSearchActive ? 0 : - 1 ,
1667+ handleAutocomplete : mockHandleAutocomplete ,
1668+ } ) ,
1669+ ) ;
1670+
15421671 const { stdin, stdout, unmount } = renderWithProviders (
15431672 < InputPrompt { ...props } /> ,
15441673 ) ;
@@ -1556,19 +1685,26 @@ describe('InputPrompt', () => {
15561685 act ( ( ) => {
15571686 stdin . write ( '\t' ) ;
15581687 } ) ;
1688+ await wait ( ) ;
15591689
1560- await waitFor (
1561- ( ) => {
1562- expect ( stdout . lastFrame ( ) ) . not . toContain ( '(r:)' ) ;
1563- } ,
1564- { timeout : 5000 } ,
1565- ) ; // Increase timeout
1566-
1690+ expect ( mockHandleAutocomplete ) . toHaveBeenCalledWith ( 0 ) ;
15671691 expect ( props . buffer . setText ) . toHaveBeenCalledWith ( 'echo hello' ) ;
15681692 unmount ( ) ;
1569- } ) ;
1693+ } , 15000 ) ;
15701694
15711695 it ( 'submits the highlighted entry on Enter and exits reverse-search' , async ( ) => {
1696+ // Mock the reverse search completion to return suggestions
1697+ mockedUseReverseSearchCompletion . mockReturnValue ( {
1698+ ...mockReverseSearchCompletion ,
1699+ suggestions : [
1700+ { label : 'echo hello' , value : 'echo hello' } ,
1701+ { label : 'echo world' , value : 'echo world' } ,
1702+ { label : 'ls' , value : 'ls' } ,
1703+ ] ,
1704+ showSuggestions : true ,
1705+ activeSuggestionIndex : 0 ,
1706+ } ) ;
1707+
15721708 const { stdin, stdout, unmount } = renderWithProviders (
15731709 < InputPrompt { ...props } /> ,
15741710 ) ;
0 commit comments