diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 0fe0a17eea..688f9a8538 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -773,393 +773,96 @@ describe('InputPrompt', () => { }); describe('cursor-based completion trigger', () => { - it('should trigger completion when cursor is after @ without spaces', async () => { - // Set up buffer state - mockBuffer.text = '@src/components'; - mockBuffer.lines = ['@src/components']; - mockBuffer.cursor = [0, 15]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + it.each([ + { + name: 'should trigger completion when cursor is after @ without spaces', + text: '@src/components', + cursor: [0, 15], showSuggestions: true, - suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - // Verify useCompletion was called with correct signature - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should trigger completion when cursor is after / without spaces', async () => { - mockBuffer.text = '/memory'; - mockBuffer.lines = ['/memory']; - mockBuffer.cursor = [0, 7]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should trigger completion when cursor is after / without spaces', + text: '/memory', + cursor: [0, 7], showSuggestions: true, - suggestions: [{ label: 'show', value: 'show' }], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should NOT trigger completion when cursor is after space following @', async () => { - mockBuffer.text = '@src/file.ts hello'; - mockBuffer.lines = ['@src/file.ts hello']; - mockBuffer.cursor = [0, 18]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should NOT trigger completion when cursor is after space following @', + text: '@src/file.ts hello', + cursor: [0, 18], showSuggestions: false, - suggestions: [], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should NOT trigger completion when cursor is after space following /', async () => { - mockBuffer.text = '/memory add'; - mockBuffer.lines = ['/memory add']; - mockBuffer.cursor = [0, 11]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should NOT trigger completion when cursor is after space following /', + text: '/memory add', + cursor: [0, 11], showSuggestions: false, - suggestions: [], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should NOT trigger completion when cursor is not after @ or /', async () => { - mockBuffer.text = 'hello world'; - mockBuffer.lines = ['hello world']; - mockBuffer.cursor = [0, 5]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should NOT trigger completion when cursor is not after @ or /', + text: 'hello world', + cursor: [0, 5], showSuggestions: false, - suggestions: [], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle multiline text correctly', async () => { - mockBuffer.text = 'first line\n/memory'; - mockBuffer.lines = ['first line', '/memory']; - mockBuffer.cursor = [1, 7]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should handle multiline text correctly', + text: 'first line\n/memory', + cursor: [1, 7], showSuggestions: false, - suggestions: [], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - // Verify useCompletion was called with the buffer - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle single line slash command correctly', async () => { - mockBuffer.text = '/memory'; - mockBuffer.lines = ['/memory']; - mockBuffer.cursor = [0, 7]; - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should handle Unicode characters (emojis) correctly in paths', + text: '@src/fileπŸ‘.txt', + cursor: [0, 14], showSuggestions: true, - suggestions: [{ label: 'show', value: 'show' }], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle Unicode characters (emojis) correctly in paths', async () => { - // Test with emoji in path after @ - mockBuffer.text = '@src/fileπŸ‘.txt'; - mockBuffer.lines = ['@src/fileπŸ‘.txt']; - mockBuffer.cursor = [0, 14]; // After the emoji character - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, - suggestions: [{ label: 'fileπŸ‘.txt', value: 'fileπŸ‘.txt' }], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle Unicode characters with spaces after them', async () => { - // Test with emoji followed by space - should NOT trigger completion - mockBuffer.text = '@src/fileπŸ‘.txt hello'; - mockBuffer.lines = ['@src/fileπŸ‘.txt hello']; - mockBuffer.cursor = [0, 20]; // After the space - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should handle Unicode characters with spaces after them', + text: '@src/fileπŸ‘.txt hello', + cursor: [0, 20], showSuggestions: false, - suggestions: [], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle escaped spaces in paths correctly', async () => { - // Test with escaped space in path - should trigger completion - mockBuffer.text = '@src/my\\ file.txt'; - mockBuffer.lines = ['@src/my\\ file.txt']; - mockBuffer.cursor = [0, 16]; // After the escaped space and filename - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should handle escaped spaces in paths correctly', + text: '@src/my\\ file.txt', + cursor: [0, 16], showSuggestions: true, - suggestions: [{ label: 'my file.txt', value: 'my file.txt' }], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should NOT trigger completion after unescaped space following escaped space', async () => { - // Test: @path/my\ file.txt hello (unescaped space after escaped space) - mockBuffer.text = '@path/my\\ file.txt hello'; - mockBuffer.lines = ['@path/my\\ file.txt hello']; - mockBuffer.cursor = [0, 24]; // After "hello" - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, + }, + { + name: 'should NOT trigger completion after unescaped space following escaped space', + text: '@path/my\\ file.txt hello', + cursor: [0, 24], showSuggestions: false, - suggestions: [], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle multiple escaped spaces in paths', async () => { - // Test with multiple escaped spaces - mockBuffer.text = '@docs/my\\ long\\ file\\ name.md'; - mockBuffer.lines = ['@docs/my\\ long\\ file\\ name.md']; - mockBuffer.cursor = [0, 29]; // At the end + }, + { + name: 'should handle multiple escaped spaces in paths', + text: '@docs/my\\ long\\ file\\ name.md', + cursor: [0, 29], + showSuggestions: true, + }, + { + name: 'should handle escaped spaces in slash commands', + text: '/memory\\ test', + cursor: [0, 13], + showSuggestions: true, + }, + { + name: 'should handle Unicode characters with escaped spaces', + text: `@${path.join('files', 'emoji\\ πŸ‘\\ test.txt')}`, + cursor: [0, 25], + showSuggestions: true, + }, + ])('$name', async ({ text, cursor, showSuggestions }) => { + mockBuffer.text = text; + mockBuffer.lines = text.split('\n'); + mockBuffer.cursor = cursor as [number, number]; mockedUseCommandCompletion.mockReturnValue({ ...mockCommandCompletion, - showSuggestions: true, - suggestions: [ - { label: 'my long file name.md', value: 'my long file name.md' }, - ], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle escaped spaces in slash commands', async () => { - // Test escaped spaces with slash commands (though less common) - mockBuffer.text = '/memory\\ test'; - mockBuffer.lines = ['/memory\\ test']; - mockBuffer.cursor = [0, 13]; // At the end - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, - suggestions: [{ label: 'test-command', value: 'test-command' }], - }); - - const { unmount } = renderWithProviders(); - await wait(); - - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); - - unmount(); - }); - - it('should handle Unicode characters with escaped spaces', async () => { - // Test combining Unicode and escaped spaces - mockBuffer.text = '@' + path.join('files', 'emoji\\ πŸ‘\\ test.txt'); - mockBuffer.lines = ['@' + path.join('files', 'emoji\\ πŸ‘\\ test.txt')]; - mockBuffer.cursor = [0, 25]; // After the escaped space and emoji - - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, - suggestions: [ - { label: 'emoji πŸ‘ test.txt', value: 'emoji πŸ‘ test.txt' }, - ], + showSuggestions, + suggestions: showSuggestions + ? [{ label: 'suggestion', value: 'suggestion' }] + : [], }); const { unmount } = renderWithProviders(); @@ -1264,226 +967,156 @@ describe('InputPrompt', () => { }); describe('Highlighting and Cursor Display', () => { - it('should display cursor mid-word by highlighting the character', async () => { - mockBuffer.text = 'hello world'; - mockBuffer.lines = ['hello world']; - mockBuffer.viewportVisualLines = ['hello world']; - mockBuffer.visualCursor = [0, 3]; // cursor on the second 'l' + describe('single-line scenarios', () => { + it.each([ + { + name: 'mid-word', + text: 'hello world', + visualCursor: [0, 3], + expected: `hel${chalk.inverse('l')}o world`, + }, + { + name: 'at the beginning of the line', + text: 'hello', + visualCursor: [0, 0], + expected: `${chalk.inverse('h')}ello`, + }, + { + name: 'at the end of the line', + text: 'hello', + visualCursor: [0, 5], + expected: `hello${chalk.inverse(' ')}`, + }, + { + name: 'on a highlighted token', + text: 'run @path/to/file', + visualCursor: [0, 9], + expected: `@path/${chalk.inverse('t')}o/file`, + }, + { + name: 'for multi-byte unicode characters', + text: 'hello πŸ‘ world', + visualCursor: [0, 6], + expected: `hello ${chalk.inverse('πŸ‘')} world`, + }, + { + name: 'at the end of a line with unicode characters', + text: 'hello πŸ‘', + visualCursor: [0, 8], + expected: `hello πŸ‘${chalk.inverse(' ')}`, + }, + { + name: 'on an empty line', + text: '', + visualCursor: [0, 0], + expected: chalk.inverse(' '), + }, + { + name: 'on a space between words', + text: 'hello world', + visualCursor: [0, 5], + expected: `hello${chalk.inverse(' ')}world`, + }, + ])( + 'should display cursor correctly $name', + async ({ text, visualCursor, expected }) => { + mockBuffer.text = text; + mockBuffer.lines = [text]; + mockBuffer.viewportVisualLines = [text]; + mockBuffer.visualCursor = visualCursor as [number, number]; - const { stdout, unmount } = renderWithProviders( - , + const { stdout, unmount } = renderWithProviders( + , + ); + await wait(); + + const frame = stdout.lastFrame(); + expect(frame).toContain(expected); + unmount(); + }, ); - await wait(); - - const frame = stdout.lastFrame(); - // The component will render the text with the character at the cursor inverted. - expect(frame).toContain(`hel${chalk.inverse('l')}o world`); - unmount(); }); - it('should display cursor at the beginning of the line', async () => { - mockBuffer.text = 'hello'; - mockBuffer.lines = ['hello']; - mockBuffer.viewportVisualLines = ['hello']; - mockBuffer.visualCursor = [0, 0]; // cursor on 'h' + describe('multi-line scenarios', () => { + it.each([ + { + name: 'in the middle of a line', + text: 'first line\nsecond line\nthird line', + visualCursor: [1, 3], + visualToLogicalMap: [ + [0, 0], + [1, 0], + [2, 0], + ], + expected: `sec${chalk.inverse('o')}nd line`, + }, + { + name: 'at the beginning of a line', + text: 'first line\nsecond line', + visualCursor: [1, 0], + visualToLogicalMap: [ + [0, 0], + [1, 0], + ], + expected: `${chalk.inverse('s')}econd line`, + }, + { + name: 'at the end of a line', + text: 'first line\nsecond line', + visualCursor: [0, 10], + visualToLogicalMap: [ + [0, 0], + [1, 0], + ], + expected: `first line${chalk.inverse(' ')}`, + }, + ])( + 'should display cursor correctly $name in a multiline block', + async ({ text, visualCursor, expected, visualToLogicalMap }) => { + mockBuffer.text = text; + mockBuffer.lines = text.split('\n'); + mockBuffer.viewportVisualLines = text.split('\n'); + mockBuffer.visualCursor = visualCursor as [number, number]; + mockBuffer.visualToLogicalMap = visualToLogicalMap as Array< + [number, number] + >; - const { stdout, unmount } = renderWithProviders( - , + const { stdout, unmount } = renderWithProviders( + , + ); + await wait(); + + const frame = stdout.lastFrame(); + expect(frame).toContain(expected); + unmount(); + }, ); - await wait(); - const frame = stdout.lastFrame(); - expect(frame).toContain(`${chalk.inverse('h')}ello`); - unmount(); - }); + it('should display cursor on a blank line in a multiline block', async () => { + const text = 'first line\n\nthird line'; + mockBuffer.text = text; + mockBuffer.lines = text.split('\n'); + mockBuffer.viewportVisualLines = text.split('\n'); + mockBuffer.visualCursor = [1, 0]; // cursor on the blank line + mockBuffer.visualToLogicalMap = [ + [0, 0], + [1, 0], + [2, 0], + ]; - it('should display cursor at the end of the line as an inverted space', async () => { - mockBuffer.text = 'hello'; - mockBuffer.lines = ['hello']; - mockBuffer.viewportVisualLines = ['hello']; - mockBuffer.visualCursor = [0, 5]; // cursor after 'o' + const { stdout, unmount } = renderWithProviders( + , + ); + await wait(); - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`hello${chalk.inverse(' ')}`); - unmount(); - }); - - it('should display cursor correctly on a highlighted token', async () => { - mockBuffer.text = 'run @path/to/file'; - mockBuffer.lines = ['run @path/to/file']; - mockBuffer.viewportVisualLines = ['run @path/to/file']; - mockBuffer.visualCursor = [0, 9]; // cursor on 't' in 'to' - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - // The token '@path/to/file' is colored, and the cursor highlights one char inside it. - expect(frame).toContain(`@path/${chalk.inverse('t')}o/file`); - unmount(); - }); - - it('should display cursor correctly for multi-byte unicode characters', async () => { - const text = 'hello πŸ‘ world'; - mockBuffer.text = text; - mockBuffer.lines = [text]; - mockBuffer.viewportVisualLines = [text]; - mockBuffer.visualCursor = [0, 6]; // cursor on 'πŸ‘' - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`hello ${chalk.inverse('πŸ‘')} world`); - unmount(); - }); - - it('should display cursor at the end of a line with unicode characters', async () => { - const text = 'hello πŸ‘'; - mockBuffer.text = text; - mockBuffer.lines = [text]; - mockBuffer.viewportVisualLines = [text]; - mockBuffer.visualCursor = [0, 8]; // cursor after 'πŸ‘' (length is 6 + 2 for emoji) - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`hello πŸ‘${chalk.inverse(' ')}`); - unmount(); - }); - - it('should display cursor on an empty line', async () => { - mockBuffer.text = ''; - mockBuffer.lines = ['']; - mockBuffer.viewportVisualLines = ['']; - mockBuffer.visualCursor = [0, 0]; - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(chalk.inverse(' ')); - unmount(); - }); - - it('should display cursor on a space between words', async () => { - mockBuffer.text = 'hello world'; - mockBuffer.lines = ['hello world']; - mockBuffer.viewportVisualLines = ['hello world']; - mockBuffer.visualCursor = [0, 5]; // cursor on the space - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`hello${chalk.inverse(' ')}world`); - unmount(); - }); - - it('should display cursor in the middle of a line in a multiline block', async () => { - const text = 'first line\nsecond line\nthird line'; - mockBuffer.text = text; - mockBuffer.lines = text.split('\n'); - mockBuffer.viewportVisualLines = text.split('\n'); - mockBuffer.visualCursor = [1, 3]; // cursor on 'o' in 'second' - mockBuffer.visualToLogicalMap = [ - [0, 0], - [1, 0], - [2, 0], - ]; - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`sec${chalk.inverse('o')}nd line`); - unmount(); - }); - - it('should display cursor at the beginning of a line in a multiline block', async () => { - const text = 'first line\nsecond line'; - mockBuffer.text = text; - mockBuffer.lines = text.split('\n'); - mockBuffer.viewportVisualLines = text.split('\n'); - mockBuffer.visualCursor = [1, 0]; // cursor on 's' in 'second' - mockBuffer.visualToLogicalMap = [ - [0, 0], - [1, 0], - ]; - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`${chalk.inverse('s')}econd line`); - unmount(); - }); - - it('should display cursor at the end of a line in a multiline block', async () => { - const text = 'first line\nsecond line'; - mockBuffer.text = text; - mockBuffer.lines = text.split('\n'); - mockBuffer.viewportVisualLines = text.split('\n'); - mockBuffer.visualCursor = [0, 10]; // cursor after 'first line' - mockBuffer.visualToLogicalMap = [ - [0, 0], - [1, 0], - ]; - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - expect(frame).toContain(`first line${chalk.inverse(' ')}`); - unmount(); - }); - - it('should display cursor on a blank line in a multiline block', async () => { - const text = 'first line\n\nthird line'; - mockBuffer.text = text; - mockBuffer.lines = text.split('\n'); - mockBuffer.viewportVisualLines = text.split('\n'); - mockBuffer.visualCursor = [1, 0]; // cursor on the blank line - mockBuffer.visualToLogicalMap = [ - [0, 0], - [1, 0], - [2, 0], - ]; - - const { stdout, unmount } = renderWithProviders( - , - ); - await wait(); - - const frame = stdout.lastFrame(); - const lines = frame!.split('\n'); - // The line with the cursor should just be an inverted space inside the box border - expect( - lines.find((l) => l.includes(chalk.inverse(' '))), - ).not.toBeUndefined(); - unmount(); + const frame = stdout.lastFrame(); + const lines = frame!.split('\n'); + // The line with the cursor should just be an inverted space inside the box border + expect( + lines.find((l) => l.includes(chalk.inverse(' '))), + ).not.toBeUndefined(); + unmount(); + }); }); }); @@ -2403,55 +2036,58 @@ describe('InputPrompt', () => { expect(mockBuffer.handleInput).toHaveBeenCalled(); unmount(); }); - it('should prevent slash commands from being queued while streaming', async () => { - props.onSubmit = vi.fn(); - props.buffer.text = '/help'; - props.setQueueErrorMessage = vi.fn(); - props.streamingState = StreamingState.Responding; - const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('/help'); - stdin.write('\r'); - await wait(); + describe('command queuing while streaming', () => { + beforeEach(() => { + props.streamingState = StreamingState.Responding; + props.setQueueErrorMessage = vi.fn(); + props.onSubmit = vi.fn(); + }); - expect(props.onSubmit).not.toHaveBeenCalled(); - expect(props.setQueueErrorMessage).toHaveBeenCalledWith( - 'Slash commands cannot be queued', + it.each([ + { + name: 'should prevent slash commands', + bufferText: '/help', + shellMode: false, + shouldSubmit: false, + errorMessage: 'Slash commands cannot be queued', + }, + { + name: 'should prevent shell commands', + bufferText: 'ls', + shellMode: true, + shouldSubmit: false, + errorMessage: 'Shell commands cannot be queued', + }, + { + name: 'should allow regular messages', + bufferText: 'regular message', + shellMode: false, + shouldSubmit: true, + errorMessage: null, + }, + ])( + '$name', + async ({ bufferText, shellMode, shouldSubmit, errorMessage }) => { + props.buffer.text = bufferText; + props.shellModeActive = shellMode; + + const { stdin, unmount } = renderWithProviders( + , + ); + await wait(); + stdin.write('\r'); + await wait(); + + if (shouldSubmit) { + expect(props.onSubmit).toHaveBeenCalledWith(bufferText); + expect(props.setQueueErrorMessage).not.toHaveBeenCalled(); + } else { + expect(props.onSubmit).not.toHaveBeenCalled(); + expect(props.setQueueErrorMessage).toHaveBeenCalledWith(errorMessage); + } + unmount(); + }, ); - unmount(); - }); - it('should prevent shell commands from being queued while streaming', async () => { - props.onSubmit = vi.fn(); - props.buffer.text = 'ls'; - props.setQueueErrorMessage = vi.fn(); - props.streamingState = StreamingState.Responding; - props.shellModeActive = true; - const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('ls'); - stdin.write('\r'); - await wait(); - - expect(props.onSubmit).not.toHaveBeenCalled(); - expect(props.setQueueErrorMessage).toHaveBeenCalledWith( - 'Shell commands cannot be queued', - ); - unmount(); - }); - it('should allow regular messages to be queued while streaming', async () => { - props.onSubmit = vi.fn(); - props.buffer.text = 'regular message'; - props.setQueueErrorMessage = vi.fn(); - props.streamingState = StreamingState.Responding; - const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('regular message'); - stdin.write('\r'); - await wait(); - - expect(props.onSubmit).toHaveBeenCalledWith('regular message'); - expect(props.setQueueErrorMessage).not.toHaveBeenCalled(); - unmount(); }); });