diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 8372358a09..54cce991ae 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -559,94 +559,52 @@ describe('InputPrompt', () => { }); }); - it('should complete a partial parent command', async () => { - // SCENARIO: /mem -> Tab - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, + it.each([ + { + name: 'should complete a partial parent command', + bufferText: '/mem', suggestions: [{ label: 'memory', value: 'memory', description: '...' }], - activeSuggestionIndex: 0, - }); - props.buffer.setText('/mem'); - - const { stdin, unmount } = renderWithProviders(); - - await act(async () => { - stdin.write('\t'); // Press Tab - }); - await waitFor(() => - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), - ); - unmount(); - }); - - it('should append a sub-command when the parent command is already complete', async () => { - // SCENARIO: /memory -> Tab (to accept 'add') - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, + activeIndex: 0, + }, + { + name: 'should append a sub-command when parent command is complete', + bufferText: '/memory ', suggestions: [ { label: 'show', value: 'show' }, { label: 'add', value: 'add' }, ], - activeSuggestionIndex: 1, // 'add' is highlighted - }); - props.buffer.setText('/memory '); - - const { stdin, unmount } = renderWithProviders(); - - await act(async () => { - stdin.write('\t'); // Press Tab - }); - await waitFor(() => - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1), - ); - unmount(); - }); - - it('should handle the "backspace" edge case correctly', async () => { - // SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show') - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, + activeIndex: 1, + }, + { + name: 'should handle the backspace edge case correctly', + bufferText: '/memory', suggestions: [ { label: 'show', value: 'show' }, { label: 'add', value: 'add' }, ], - activeSuggestionIndex: 0, // 'show' is highlighted - }); - // The user has backspaced, so the query is now just '/memory' - props.buffer.setText('/memory'); - - const { stdin, unmount } = renderWithProviders(); - - await act(async () => { - stdin.write('\t'); // Press Tab - }); - await waitFor(() => - // It should NOT become '/show'. It should correctly become '/memory show'. - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), - ); - unmount(); - }); - - it('should complete a partial argument for a command', async () => { - // SCENARIO: /chat resume fi- -> Tab - mockedUseCommandCompletion.mockReturnValue({ - ...mockCommandCompletion, - showSuggestions: true, + activeIndex: 0, + }, + { + name: 'should complete a partial argument for a command', + bufferText: '/chat resume fi-', suggestions: [{ label: 'fix-foo', value: 'fix-foo' }], - activeSuggestionIndex: 0, + activeIndex: 0, + }, + ])('$name', async ({ bufferText, suggestions, activeIndex }) => { + mockedUseCommandCompletion.mockReturnValue({ + ...mockCommandCompletion, + showSuggestions: true, + suggestions, + activeSuggestionIndex: activeIndex, }); - props.buffer.setText('/chat resume fi-'); - + props.buffer.setText(bufferText); const { stdin, unmount } = renderWithProviders(); - await act(async () => { - stdin.write('\t'); // Press Tab - }); + await act(async () => stdin.write('\t')); await waitFor(() => - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith( + activeIndex, + ), ); unmount(); }); @@ -937,51 +895,36 @@ describe('InputPrompt', () => { }); describe('vim mode', () => { - it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => { - props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it. + it.each([ + { + name: 'should not call buffer.handleInput when vim handles input', + vimHandled: true, + expectBufferHandleInput: false, + }, + { + name: 'should call buffer.handleInput when vim does not handle input', + vimHandled: false, + expectBufferHandleInput: true, + }, + { + name: 'should call handleInput when vim mode is disabled', + vimHandled: false, + expectBufferHandleInput: true, + }, + ])('$name', async ({ vimHandled, expectBufferHandleInput }) => { + props.vimHandleInput = vi.fn().mockReturnValue(vimHandled); const { stdin, unmount } = renderWithProviders( , ); - await act(async () => { - stdin.write('i'); - }); + await act(async () => stdin.write('i')); await waitFor(() => { expect(props.vimHandleInput).toHaveBeenCalled(); - }); - expect(mockBuffer.handleInput).not.toHaveBeenCalled(); - unmount(); - }); - - it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => { - props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it. - const { stdin, unmount } = renderWithProviders( - , - ); - - await act(async () => { - stdin.write('i'); - }); - await waitFor(() => { - expect(props.vimHandleInput).toHaveBeenCalled(); - expect(mockBuffer.handleInput).toHaveBeenCalled(); - }); - unmount(); - }); - - 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 } = renderWithProviders( - , - ); - - await act(async () => { - stdin.write('i'); - }); - await waitFor(() => { - expect(props.vimHandleInput).toHaveBeenCalled(); - expect(mockBuffer.handleInput).toHaveBeenCalled(); + if (expectBufferHandleInput) { + expect(mockBuffer.handleInput).toHaveBeenCalled(); + } else { + expect(mockBuffer.handleInput).not.toHaveBeenCalled(); + } }); unmount(); }); @@ -2226,8 +2169,6 @@ describe('InputPrompt', () => { ); await waitFor(() => { expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`); - // This snapshot is good to make sure there was an input prompt but does - // not show the inverted cursor because snapshots do not show colors. expect(stdout.lastFrame()).toMatchSnapshot(); }); unmount(); diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index 49117528f5..e0015e1051 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -97,61 +97,75 @@ describe('fileUtils', () => { }); describe('isWithinRoot', () => { - const root = path.resolve('/project/root'); + const defaultRoot = path.resolve('/project/root'); - it('should return true for paths directly within the root', () => { - expect(isWithinRoot(path.join(root, 'file.txt'), root)).toBe(true); - expect(isWithinRoot(path.join(root, 'subdir', 'file.txt'), root)).toBe( - true, - ); - }); - - it('should return true for the root path itself', () => { - expect(isWithinRoot(root, root)).toBe(true); - }); - - it('should return false for paths outside the root', () => { - expect( - isWithinRoot(path.resolve('/project/other', 'file.txt'), root), - ).toBe(false); - expect(isWithinRoot(path.resolve('/unrelated', 'file.txt'), root)).toBe( - false, - ); - }); - - it('should return false for paths that only partially match the root prefix', () => { - expect( - isWithinRoot( - path.resolve('/project/root-but-actually-different'), - root, - ), - ).toBe(false); - }); - - it('should handle paths with trailing slashes correctly', () => { - expect(isWithinRoot(path.join(root, 'file.txt') + path.sep, root)).toBe( - true, - ); - expect(isWithinRoot(root + path.sep, root)).toBe(true); - }); - - it('should handle different path separators (POSIX vs Windows)', () => { - const posixRoot = '/project/root'; - const posixPathInside = '/project/root/file.txt'; - const posixPathOutside = '/project/other/file.txt'; - expect(isWithinRoot(posixPathInside, posixRoot)).toBe(true); - expect(isWithinRoot(posixPathOutside, posixRoot)).toBe(false); - }); - - it('should return false for a root path that is a sub-path of the path to check', () => { - const pathToCheck = path.resolve('/project/root/sub'); - const rootSub = path.resolve('/project/root'); - expect(isWithinRoot(pathToCheck, rootSub)).toBe(true); - - const pathToCheckSuper = path.resolve('/project/root'); - const rootSuper = path.resolve('/project/root/sub'); - expect(isWithinRoot(pathToCheckSuper, rootSuper)).toBe(false); - }); + it.each([ + { + name: 'a path directly within the root', + path: path.join(defaultRoot, 'file.txt'), + expected: true, + }, + { + name: 'a path in a subdirectory within the root', + path: path.join(defaultRoot, 'subdir', 'file.txt'), + expected: true, + }, + { name: 'the root path itself', path: defaultRoot, expected: true }, + { + name: 'a path with a trailing slash', + path: path.join(defaultRoot, 'file.txt') + path.sep, + expected: true, + }, + { + name: 'the root path with a trailing slash', + path: defaultRoot + path.sep, + expected: true, + }, + { + name: 'a sub-path of the path to check', + path: path.resolve('/project/root/sub'), + root: path.resolve('/project/root'), + expected: true, + }, + { + name: 'a path outside the root', + path: path.resolve('/project/other', 'file.txt'), + expected: false, + }, + { + name: 'an unrelated path', + path: path.resolve('/unrelated', 'file.txt'), + expected: false, + }, + { + name: 'a path that only partially matches the root prefix', + path: path.resolve('/project/root-but-actually-different'), + expected: false, + }, + { + name: 'a root path that is a sub-path of the path to check', + path: path.resolve('/project/root'), + root: path.resolve('/project/root/sub'), + expected: false, + }, + { + name: 'a POSIX path inside', + path: '/project/root/file.txt', + root: '/project/root', + expected: true, + }, + { + name: 'a POSIX path outside', + path: '/project/other/file.txt', + root: '/project/root', + expected: false, + }, + ])( + 'should return $expected for $name', + ({ path: testPath, root, expected }) => { + expect(isWithinRoot(testPath, root || defaultRoot)).toBe(expected); + }, + ); }); describe('fileExists', () => { @@ -610,45 +624,27 @@ describe('fileUtils', () => { expect(await detectFileType('component.tsx')).toBe('text'); }); - it('should detect image type by extension (png)', async () => { - mockMimeGetType.mockReturnValueOnce('image/png'); - expect(await detectFileType('file.png')).toBe('image'); - }); - - it('should detect image type by extension (jpeg)', async () => { - mockMimeGetType.mockReturnValueOnce('image/jpeg'); - expect(await detectFileType('file.jpg')).toBe('image'); - }); + it.each([ + { type: 'image', file: 'file.png', mime: 'image/png' }, + { type: 'image', file: 'file.jpg', mime: 'image/jpeg' }, + { type: 'pdf', file: 'file.pdf', mime: 'application/pdf' }, + { type: 'audio', file: 'song.mp3', mime: 'audio/mpeg' }, + { type: 'video', file: 'movie.mp4', mime: 'video/mp4' }, + { type: 'binary', file: 'archive.zip', mime: 'application/zip' }, + { type: 'binary', file: 'app.exe', mime: 'application/octet-stream' }, + ])( + 'should detect $type type for $file by extension', + async ({ file, mime, type }) => { + mockMimeGetType.mockReturnValueOnce(mime); + expect(await detectFileType(file)).toBe(type); + }, + ); it('should detect svg type by extension', async () => { expect(await detectFileType('image.svg')).toBe('svg'); expect(await detectFileType('image.icon.svg')).toBe('svg'); }); - it('should detect pdf type by extension', async () => { - mockMimeGetType.mockReturnValueOnce('application/pdf'); - expect(await detectFileType('file.pdf')).toBe('pdf'); - }); - - it('should detect audio type by extension', async () => { - mockMimeGetType.mockReturnValueOnce('audio/mpeg'); - expect(await detectFileType('song.mp3')).toBe('audio'); - }); - - it('should detect video type by extension', async () => { - mockMimeGetType.mockReturnValueOnce('video/mp4'); - expect(await detectFileType('movie.mp4')).toBe('video'); - }); - - it('should detect known binary extensions as binary (e.g. .zip)', async () => { - mockMimeGetType.mockReturnValueOnce('application/zip'); - expect(await detectFileType('archive.zip')).toBe('binary'); - }); - it('should detect known binary extensions as binary (e.g. .exe)', async () => { - mockMimeGetType.mockReturnValueOnce('application/octet-stream'); // Common for .exe - expect(await detectFileType('app.exe')).toBe('binary'); - }); - it('should use isBinaryFile for unknown extensions and detect as binary', async () => { mockMimeGetType.mockReturnValueOnce(false); // Unknown mime type // Create a file that isBinaryFile will identify as binary