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