mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 16:10:59 -07:00
710 lines
22 KiB
TypeScript
710 lines
22 KiB
TypeScript
/**
|
|
* @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 type { Config, FileSearch } from '@google/gemini-cli-core';
|
|
import {
|
|
FileSearchFactory,
|
|
FileDiscoveryService,
|
|
escapePath,
|
|
} from '@google/gemini-cli-core';
|
|
import type { FileSystemStructure } from '@google/gemini-cli-test-utils';
|
|
import { createTmpDir, cleanupTmpDir } 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 } = 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 } = 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 } = 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 } = 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 } = 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({});
|
|
|
|
// Mock FileSearch to be slow to catch the loading state
|
|
const mockFileSearch = {
|
|
initialize: vi.fn().mockImplementation(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}),
|
|
search: vi.fn().mockResolvedValue([]),
|
|
};
|
|
vi.spyOn(FileSearchFactory, 'create').mockReturnValue(
|
|
mockFileSearch as unknown as FileSearch,
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTestHarnessForAtCompletion(true, '', mockConfig, testRootDir),
|
|
);
|
|
|
|
// It's initially true because the effect runs synchronously.
|
|
await waitFor(() => {
|
|
expect(result.current.isLoadingSuggestions).toBe(true);
|
|
});
|
|
|
|
// 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 } = 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
|
|
const mockFileSearch: FileSearch = {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
search: vi
|
|
.fn()
|
|
.mockImplementation(async (pattern, options) =>
|
|
realFileSearch.search(pattern, options),
|
|
),
|
|
};
|
|
vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch);
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ pattern }) =>
|
|
useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir),
|
|
{ initialProps: { pattern: 'a' } },
|
|
);
|
|
|
|
// 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');
|
|
const mockFileSearch: FileSearch = {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
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 } = renderHook(
|
|
({ pattern }) =>
|
|
useTestHarnessForAtCompletion(true, pattern, mockConfig, testRootDir),
|
|
{ initialProps: { pattern: 'a' } },
|
|
);
|
|
|
|
// 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 } = 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({});
|
|
|
|
// Force an error during initialization
|
|
const mockFileSearch: FileSearch = {
|
|
initialize: vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('Initialization failed')),
|
|
search: vi.fn(),
|
|
};
|
|
vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch);
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ enabled }) =>
|
|
useTestHarnessForAtCompletion(enabled, '', mockConfig, testRootDir),
|
|
{ initialProps: { enabled: true } },
|
|
);
|
|
|
|
// 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 } = 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 } = 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 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 } = 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;
|
|
|
|
const { result } = renderHook(() =>
|
|
useTestHarnessForAtCompletion(
|
|
true,
|
|
'',
|
|
nonRecursiveConfig,
|
|
testRootDir,
|
|
),
|
|
);
|
|
|
|
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 = {
|
|
...mockConfig,
|
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
|
getDirectories: () => [cwdDir, addedDir],
|
|
onDirectoriesChanged: vi.fn(() => () => {}),
|
|
}),
|
|
} as unknown as Config;
|
|
|
|
const { result } = 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 = {
|
|
...mockConfig,
|
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
|
getDirectories: () => [...directories],
|
|
onDirectoriesChanged: vi.fn((listener: () => void) => {
|
|
dirChangeListener = listener;
|
|
return () => {
|
|
dirChangeListener = null;
|
|
};
|
|
}),
|
|
}),
|
|
} as unknown as Config;
|
|
|
|
const { result } = 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 = {
|
|
...mockConfig,
|
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
|
getDirectories: () => [dir1, dir2],
|
|
onDirectoriesChanged: vi.fn(() => () => {}),
|
|
}),
|
|
} as unknown as Config;
|
|
|
|
const { result } = 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')),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|