Download packages/cli/src/ui/hooks/useAtCompletion.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 25.7 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/hooks/useAtCompletion.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/hooks/useAtCompletion.test.ts
-
curl -L -o useAtCompletion.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/hooks/useAtCompletion.test.ts
25.7 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; | |
| import { act, useState } from 'react'; | |
| import * as path from 'node:path'; | |
| import { renderHook } from '../../test-utils/render.js'; | |
| import { waitFor } from '../../test-utils/async.js'; | |
| import { useAtCompletion } from './useAtCompletion.js'; | |
| import { | |
| FileSearchFactory, | |
| FileDiscoveryService, | |
| escapePath, | |
| type Config, | |
| type FileSearch, | |
| } from '@google/gemini-cli-core'; | |
| import { | |
| createTmpDir, | |
| cleanupTmpDir, | |
| type FileSystemStructure, | |
| } from '@google/gemini-cli-test-utils'; | |
| import type { Suggestion } from '../components/SuggestionsDisplay.js'; | |
| // Test harness to capture the state from the hook's callbacks. | |
| function useTestHarnessForAtCompletion( | |
| enabled: boolean, | |
| pattern: string, | |
| config: Config | undefined, | |
| cwd: string, | |
| ) { | |
| const [suggestions, setSuggestions] = useState<Suggestion[]>([]); | |
| const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); | |
| useAtCompletion({ | |
| enabled, | |
| pattern, | |
| config, | |
| cwd, | |
| setSuggestions, | |
| setIsLoadingSuggestions, | |
| }); | |
| return { suggestions, isLoadingSuggestions }; | |
| } | |
| describe('useAtCompletion', () => { | |
| let testRootDir: string; | |
| let mockConfig: Config; | |
| beforeEach(() => { | |
| mockConfig = { | |
| getFileFilteringOptions: vi.fn(() => ({ | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: true, | |
| })), | |
| getEnableRecursiveFileSearch: () => true, | |
| getFileFilteringEnableFuzzySearch: () => true, | |
| getResourceRegistry: vi.fn().mockReturnValue({ | |
| getAllResources: () => [], | |
| }), | |
| } as unknown as Config; | |
| vi.clearAllMocks(); | |
| }); | |
| afterEach(async () => { | |
| if (testRootDir) { | |
| await cleanupTmpDir(testRootDir); | |
| } | |
| vi.restoreAllMocks(); | |
| }); | |
| describe('File Search Logic', () => { | |
| it('should perform a recursive search for an empty pattern', async () => { | |
| const structure: FileSystemStructure = { | |
| 'file.txt': '', | |
| src: { | |
| 'index.js': '', | |
| components: ['Button.tsx', 'Button with spaces.tsx'], | |
| }, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir), | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(5); | |
| }); | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'src/', | |
| 'src/components/', | |
| 'file.txt', | |
| `${escapePath('src/components/Button with spaces.tsx')}`, | |
| 'src/components/Button.tsx', | |
| 'src/index.js', | |
| ]); | |
| }); | |
| it('should correctly filter the recursive list based on a pattern', async () => { | |
| const structure: FileSystemStructure = { | |
| 'file.txt': '', | |
| src: { | |
| 'index.js': '', | |
| components: { | |
| 'Button.tsx': '', | |
| }, | |
| }, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, 'src/', mockConfig, testRootDir), | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| }); | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'src/', | |
| 'src/index.js', | |
| 'src/components/', | |
| 'src/components/Button.tsx', | |
| ]); | |
| }); | |
| it('should append a trailing slash to directory paths in suggestions', async () => { | |
| const structure: FileSystemStructure = { | |
| 'file.txt': '', | |
| dir: {}, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir), | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| }); | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'dir/', | |
| 'file.txt', | |
| ]); | |
| }); | |
| it('should perform a case-insensitive search by lowercasing the pattern', async () => { | |
| testRootDir = await createTmpDir({ 'cRaZycAsE.txt': '' }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: testRootDir, | |
| ignoreDirs: [], | |
| fileDiscoveryService: new FileDiscoveryService(testRootDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| vi.spyOn(FileSearchFactory, 'create').mockReturnValue(fileSearch); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion( | |
| true, | |
| 'CrAzYCaSe', | |
| mockConfig, | |
| testRootDir, | |
| ), | |
| ); | |
| // The hook should find 'cRaZycAsE.txt' even though the pattern is 'CrAzYCaSe'. | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'cRaZycAsE.txt', | |
| ]); | |
| }); | |
| }); | |
| }); | |
| describe('MCP resource suggestions', () => { | |
| it('should include MCP resources in the suggestion list using fuzzy matching', async () => { | |
| mockConfig.getResourceRegistry = vi.fn().mockReturnValue({ | |
| getAllResources: () => [ | |
| { | |
| serverName: 'server-1', | |
| uri: 'file:///tmp/server-1/logs.txt', | |
| name: 'logs', | |
| discoveredAt: Date.now(), | |
| }, | |
| ], | |
| }); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, 'logs', mockConfig, '/tmp'), | |
| ); | |
| await waitFor(() => { | |
| expect( | |
| result.current.suggestions.some( | |
| (suggestion) => | |
| suggestion.value === 'server-1:file:///tmp/server-1/logs.txt', | |
| ), | |
| ).toBe(true); | |
| }); | |
| }); | |
| }); | |
| describe('UI State and Loading Behavior', () => { | |
| it('should be in a loading state during initial file system crawl', async () => { | |
| testRootDir = await createTmpDir({}); | |
| let deferredInit: { resolve: (value?: unknown) => void }; | |
| // Mock FileSearch to control when initialization finishes | |
| const mockFileSearch = { | |
| initialize: vi.fn().mockImplementation( | |
| () => | |
| new Promise((resolve) => { | |
| deferredInit = { resolve }; | |
| }), | |
| ), | |
| search: vi.fn().mockResolvedValue([]), | |
| }; | |
| vi.spyOn(FileSearchFactory, 'create').mockReturnValue( | |
| mockFileSearch as unknown as FileSearch, | |
| ); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir), | |
| ); | |
| // It's true because the promise hasn't resolved yet | |
| expect(result.current.isLoadingSuggestions).toBe(true); | |
| // Resolve the initialization | |
| await act(async () => { | |
| deferredInit.resolve(); | |
| }); | |
| // Wait for the loading to complete. | |
| await waitFor(() => { | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| }); | |
| }); | |
| it('should NOT show a loading indicator for subsequent searches that complete under 200ms', async () => { | |
| const structure: FileSystemStructure = { 'a.txt': '', 'b.txt': '' }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result, rerender } = await renderHook( | |
| ({ pattern }) => | |
| useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir), | |
| { initialProps: { pattern: 'a' } }, | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'a.txt', | |
| ]); | |
| }); | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| rerender({ pattern: 'b' }); | |
| // Wait for the final result | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'b.txt', | |
| ]); | |
| }); | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| }); | |
| it('should show a loading indicator and clear old suggestions for subsequent searches that take longer than 200ms', async () => { | |
| const structure: FileSystemStructure = { 'a.txt': '', 'b.txt': '' }; | |
| testRootDir = await createTmpDir(structure); | |
| const realFileSearch = FileSearchFactory.create({ | |
| projectRoot: testRootDir, | |
| ignoreDirs: [], | |
| fileDiscoveryService: new FileDiscoveryService(testRootDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: true, | |
| }), | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await realFileSearch.initialize(); | |
| // Mock that returns results immediately but we'll control timing with fake timers | |
| let deferredInit: { | |
| resolve: (value?: unknown) => void; | |
| reject: (e: Error) => void; | |
| }; | |
| const mockFileSearch: FileSearch = { | |
| initialize: vi.fn().mockImplementation( | |
| () => | |
| new Promise((resolve, reject) => { | |
| deferredInit = { resolve, reject }; | |
| }), | |
| ), | |
| search: vi | |
| .fn() | |
| .mockImplementation(async (pattern, options) => | |
| realFileSearch.search(pattern, options), | |
| ), | |
| }; | |
| vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch); | |
| const { result, rerender } = await renderHook( | |
| ({ pattern }) => | |
| useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir), | |
| { initialProps: { pattern: 'a' } }, | |
| ); | |
| await act(async () => { | |
| deferredInit.resolve(); | |
| }); | |
| // Wait for the initial search to complete (using real timers) | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'a.txt', | |
| ]); | |
| }); | |
| // Now switch to fake timers for precise control of the loading behavior | |
| vi.useFakeTimers(); | |
| // Trigger the second search | |
| act(() => { | |
| rerender({ pattern: 'b' }); | |
| }); | |
| // Initially, loading should be false (before 200ms timer) | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| // Advance time by exactly 200ms to trigger the loading state | |
| act(() => { | |
| vi.advanceTimersByTime(200); | |
| }); | |
| // Now loading should be true and suggestions should be cleared | |
| expect(result.current.isLoadingSuggestions).toBe(true); | |
| expect(result.current.suggestions).toEqual([]); | |
| // Switch back to real timers for the final waitFor | |
| vi.useRealTimers(); | |
| // Wait for the search results to be processed | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'b.txt', | |
| ]); | |
| }); | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| }); | |
| it('should abort the previous search when a new one starts', async () => { | |
| const structure: FileSystemStructure = { 'a.txt': '', 'b.txt': '' }; | |
| testRootDir = await createTmpDir(structure); | |
| const abortSpy = vi.spyOn(AbortController.prototype, 'abort'); | |
| let deferredInit: { | |
| resolve: (value?: unknown) => void; | |
| reject: (e: Error) => void; | |
| }; | |
| const mockFileSearch: FileSearch = { | |
| initialize: vi.fn().mockImplementation( | |
| () => | |
| new Promise((resolve, reject) => { | |
| deferredInit = { resolve, reject }; | |
| }), | |
| ), | |
| search: vi.fn().mockImplementation(async (pattern: string) => { | |
| const delay = pattern === 'a' ? 500 : 50; | |
| await new Promise((resolve) => setTimeout(resolve, delay)); | |
| return [pattern]; | |
| }), | |
| }; | |
| vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch); | |
| const { result, rerender } = await renderHook( | |
| ({ pattern }) => | |
| useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir), | |
| { initialProps: { pattern: 'a' } }, | |
| ); | |
| await act(async () => { | |
| deferredInit.resolve(); | |
| }); | |
| // Wait for the hook to be ready (initialization is complete) | |
| await waitFor(() => { | |
| expect(mockFileSearch.search).toHaveBeenCalledWith( | |
| 'a', | |
| expect.any(Object), | |
| ); | |
| }); | |
| // Now that the first search is in-flight, trigger the second one. | |
| act(() => { | |
| rerender({ pattern: 'b' }); | |
| }); | |
| // The abort should have been called for the first search. | |
| expect(abortSpy).toHaveBeenCalledTimes(1); | |
| // Wait for the final result, which should be from the second, faster search. | |
| await waitFor( | |
| () => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual(['b']); | |
| }, | |
| { timeout: 1000 }, | |
| ); | |
| // The search spy should have been called for both patterns. | |
| expect(mockFileSearch.search).toHaveBeenCalledWith( | |
| 'b', | |
| expect.any(Object), | |
| ); | |
| }); | |
| }); | |
| describe('State Management', () => { | |
| it('should reset the state when disabled after being in a READY state', async () => { | |
| const structure: FileSystemStructure = { 'a.txt': '' }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result, rerender } = await renderHook( | |
| ({ enabled }) => | |
| useTestHarnessForAtCompletion(enabled, 'a', mockConfig, testRootDir), | |
| { initialProps: { enabled: true } }, | |
| ); | |
| // Wait for the hook to be ready and have suggestions | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'a.txt', | |
| ]); | |
| }); | |
| // Now, disable the hook | |
| rerender({ enabled: false }); | |
| // The suggestions should be cleared immediately because of the RESET action | |
| expect(result.current.suggestions).toEqual([]); | |
| }); | |
| it('should reset the state when disabled after being in an ERROR state', async () => { | |
| testRootDir = await createTmpDir({}); | |
| let deferredInit: { | |
| resolve: (value?: unknown) => void; | |
| reject: (e: Error) => void; | |
| }; | |
| // Force an error during initialization | |
| const mockFileSearch: FileSearch = { | |
| initialize: vi.fn().mockImplementation( | |
| () => | |
| new Promise((resolve, reject) => { | |
| deferredInit = { resolve, reject }; | |
| }), | |
| ), | |
| search: vi.fn(), | |
| }; | |
| vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch); | |
| const { result, rerender } = await renderHook( | |
| ({ enabled }) => | |
| useTestHarnessForAtCompletion(enabled, '', mockConfig, testRootDir), | |
| { initialProps: { enabled: true } }, | |
| ); | |
| await act(async () => { | |
| deferredInit.reject(new Error('Initialization failed')); | |
| }); | |
| // Wait for the hook to enter the error state | |
| await waitFor(() => { | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| }); | |
| expect(result.current.suggestions).toEqual([]); // No suggestions on error | |
| // Now, disable the hook | |
| rerender({ enabled: false }); | |
| // The state should still be reset (though visually it's the same) | |
| // We can't directly inspect the internal state, but we can ensure it doesn't crash | |
| // and the suggestions remain empty. | |
| expect(result.current.suggestions).toEqual([]); | |
| }); | |
| }); | |
| describe('Filtering and Configuration', () => { | |
| it('should respect .gitignore files', async () => { | |
| const gitignoreContent = ['dist/', '*.log'].join('\n'); | |
| const structure: FileSystemStructure = { | |
| '.git': {}, | |
| '.gitignore': gitignoreContent, | |
| dist: {}, | |
| 'test.log': '', | |
| src: {}, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir), | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| }); | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'src/', | |
| '.gitignore', | |
| ]); | |
| }); | |
| it('should work correctly when config is undefined', async () => { | |
| const structure: FileSystemStructure = { | |
| node_modules: {}, | |
| src: {}, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', undefined, testRootDir), | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| }); | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'node_modules/', | |
| 'src/', | |
| ]); | |
| }); | |
| it('should pass enableFileWatcher flag into FileSearchFactory options', async () => { | |
| const structure: FileSystemStructure = { | |
| src: { | |
| 'index.ts': '', | |
| }, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const createSpy = vi.spyOn(FileSearchFactory, 'create'); | |
| const configWithWatcher = { | |
| getFileFilteringOptions: vi.fn(() => ({ | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: true, | |
| enableFileWatcher: true, | |
| })), | |
| getEnableRecursiveFileSearch: () => true, | |
| getFileFilteringEnableFuzzySearch: () => true, | |
| } as unknown as Config; | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', configWithWatcher, testRootDir), | |
| ); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| }); | |
| expect(createSpy).toHaveBeenCalled(); | |
| const firstCallArg = createSpy.mock.calls[0]?.[0]; | |
| expect(firstCallArg?.enableFileWatcher).toBe(true); | |
| }); | |
| it('should reset and re-initialize when the cwd changes', async () => { | |
| const structure1: FileSystemStructure = { 'file1.txt': '' }; | |
| const rootDir1 = await createTmpDir(structure1); | |
| const structure2: FileSystemStructure = { 'file2.txt': '' }; | |
| const rootDir2 = await createTmpDir(structure2); | |
| const { result, rerender } = await renderHook( | |
| ({ cwd, pattern }) => | |
| useTestHarnessForAtCompletion(true, pattern, mockConfig, cwd), | |
| { | |
| initialProps: { | |
| cwd: rootDir1, | |
| pattern: 'file', | |
| }, | |
| }, | |
| ); | |
| // Wait for initial suggestions from the first directory | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'file1.txt', | |
| ]); | |
| }); | |
| // Change the CWD | |
| act(() => { | |
| rerender({ cwd: rootDir2, pattern: 'file' }); | |
| }); | |
| // After CWD changes, suggestions should be cleared and it should load again. | |
| await waitFor(() => { | |
| expect(result.current.isLoadingSuggestions).toBe(true); | |
| expect(result.current.suggestions).toEqual([]); | |
| }); | |
| // Wait for the new suggestions from the second directory | |
| await waitFor(() => { | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'file2.txt', | |
| ]); | |
| }); | |
| expect(result.current.isLoadingSuggestions).toBe(false); | |
| await cleanupTmpDir(rootDir1); | |
| await cleanupTmpDir(rootDir2); | |
| }); | |
| it('should perform a non-recursive search when enableRecursiveFileSearch is false', async () => { | |
| const structure: FileSystemStructure = { | |
| 'file.txt': '', | |
| src: { | |
| 'index.js': '', | |
| }, | |
| }; | |
| testRootDir = await createTmpDir(structure); | |
| const nonRecursiveConfig = { | |
| getEnableRecursiveFileSearch: () => false, | |
| getFileFilteringOptions: vi.fn(() => ({ | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: true, | |
| })), | |
| getFileFilteringEnableFuzzySearch: () => true, | |
| } as unknown as Config; | |
| let deferredInit: { resolve: (value?: unknown) => void }; | |
| const mockFileSearch: FileSearch = { | |
| initialize: vi.fn().mockImplementation( | |
| () => | |
| new Promise((resolve) => { | |
| deferredInit = { resolve }; | |
| }), | |
| ), | |
| search: vi.fn().mockResolvedValue(['src/', 'file.txt']), | |
| }; | |
| vi.spyOn(FileSearchFactory, 'create').mockReturnValue( | |
| mockFileSearch as unknown as FileSearch, | |
| ); | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion( | |
| true, | |
| '', | |
| nonRecursiveConfig, | |
| testRootDir, | |
| ), | |
| ); | |
| await act(async () => { | |
| deferredInit.resolve(); | |
| }); | |
| await waitFor(() => { | |
| expect(result.current.suggestions.length).toBeGreaterThan(0); | |
| }); | |
| // Should only contain top-level items | |
| expect(result.current.suggestions.map((s) => s.value)).toEqual([ | |
| 'src/', | |
| 'file.txt', | |
| ]); | |
| }); | |
| }); | |
| describe('Multi-directory workspace support', () => { | |
| const multiDirTmpDirs: string[] = []; | |
| afterEach(async () => { | |
| await Promise.all(multiDirTmpDirs.map((dir) => cleanupTmpDir(dir))); | |
| multiDirTmpDirs.length = 0; | |
| }); | |
| it('should include files from workspace directories beyond cwd', async () => { | |
| const cwdStructure: FileSystemStructure = { 'main.txt': '' }; | |
| const addedDirStructure: FileSystemStructure = { 'added-file.txt': '' }; | |
| const cwdDir = await createTmpDir(cwdStructure); | |
| multiDirTmpDirs.push(cwdDir); | |
| const addedDir = await createTmpDir(addedDirStructure); | |
| multiDirTmpDirs.push(addedDir); | |
| const multiDirConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-misused-spread | |
| ...mockConfig, | |
| getWorkspaceContext: vi.fn().mockReturnValue({ | |
| getDirectories: () => [cwdDir, addedDir], | |
| onDirectoriesChanged: vi.fn(() => () => {}), | |
| }), | |
| } as unknown as Config; | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', multiDirConfig, cwdDir), | |
| ); | |
| await waitFor(() => { | |
| const values = result.current.suggestions.map((s) => s.value); | |
| expect(values).toContain('main.txt'); | |
| expect(values).toContain( | |
| escapePath(path.join(addedDir, 'added-file.txt')), | |
| ); | |
| }); | |
| }); | |
| it('should pick up newly added directories via onDirectoriesChanged', async () => { | |
| const cwdStructure: FileSystemStructure = { 'original.txt': '' }; | |
| const addedStructure: FileSystemStructure = { 'new-file.txt': '' }; | |
| const cwdDir = await createTmpDir(cwdStructure); | |
| multiDirTmpDirs.push(cwdDir); | |
| const addedDir = await createTmpDir(addedStructure); | |
| multiDirTmpDirs.push(addedDir); | |
| let dirChangeListener: (() => void) | null = null; | |
| const directories = [cwdDir]; | |
| const dynamicConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-misused-spread | |
| ...mockConfig, | |
| getWorkspaceContext: vi.fn().mockReturnValue({ | |
| getDirectories: () => [...directories], | |
| onDirectoriesChanged: vi.fn((listener: () => void) => { | |
| dirChangeListener = listener; | |
| return () => { | |
| dirChangeListener = null; | |
| }; | |
| }), | |
| }), | |
| } as unknown as Config; | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, '', dynamicConfig, cwdDir), | |
| ); | |
| await waitFor(() => { | |
| const values = result.current.suggestions.map((s) => s.value); | |
| expect(values).toContain('original.txt'); | |
| expect(values.every((v) => !v.includes('new-file.txt'))).toBe(true); | |
| }); | |
| directories.push(addedDir); | |
| act(() => { | |
| dirChangeListener?.(); | |
| }); | |
| await waitFor(() => { | |
| const values = result.current.suggestions.map((s) => s.value); | |
| expect(values).toContain( | |
| escapePath(path.join(addedDir, 'new-file.txt')), | |
| ); | |
| }); | |
| }); | |
| it('should show same-named files from different directories without false deduplication', async () => { | |
| const dir1Structure: FileSystemStructure = { 'readme.md': '' }; | |
| const dir2Structure: FileSystemStructure = { 'readme.md': '' }; | |
| const dir1 = await createTmpDir(dir1Structure); | |
| multiDirTmpDirs.push(dir1); | |
| const dir2 = await createTmpDir(dir2Structure); | |
| multiDirTmpDirs.push(dir2); | |
| const multiDirConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-misused-spread | |
| ...mockConfig, | |
| getWorkspaceContext: vi.fn().mockReturnValue({ | |
| getDirectories: () => [dir1, dir2], | |
| onDirectoriesChanged: vi.fn(() => () => {}), | |
| }), | |
| } as unknown as Config; | |
| const { result } = await renderHook(() => | |
| useTestHarnessForAtCompletion(true, 'readme', multiDirConfig, dir1), | |
| ); | |
| await waitFor(() => { | |
| const values = result.current.suggestions.map((s) => s.value); | |
| const readmeEntries = values.filter((v) => v.includes('readme.md')); | |
| expect(readmeEntries.length).toBe(2); | |
| expect(readmeEntries).toContain('readme.md'); | |
| expect(readmeEntries).toContain( | |
| escapePath(path.join(dir2, 'readme.md')), | |
| ); | |
| }); | |
| }); | |
| }); | |
| }); | |