Download packages/core/src/utils/filesearch/fileSearch.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 27.3 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/utils/filesearch/fileSearch.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/utils/filesearch/fileSearch.test.ts
-
curl -L -o fileSearch.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/utils/filesearch/fileSearch.test.ts
27.3 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, afterEach, vi } from 'vitest'; | |
| import path from 'node:path'; | |
| import fs from 'node:fs/promises'; | |
| import { FileSearchFactory, AbortError, filter } from './fileSearch.js'; | |
| import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils'; | |
| import * as crawler from './crawler.js'; | |
| import { GEMINI_IGNORE_FILE_NAME } from '../../config/constants.js'; | |
| import { FileDiscoveryService } from '../../services/fileDiscoveryService.js'; | |
| import { escapePath } from '../paths.js'; | |
| describe('FileSearch', () => { | |
| let tmpDir: string; | |
| afterEach(async () => { | |
| if (tmpDir) { | |
| await cleanupTmpDir(tmpDir); | |
| } | |
| vi.restoreAllMocks(); | |
| }); | |
| it('should use .geminiignore rules', async () => { | |
| tmpDir = await createTmpDir({ | |
| [GEMINI_IGNORE_FILE_NAME]: 'dist/', | |
| dist: ['ignored.js'], | |
| src: ['not-ignored.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: true, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual([ | |
| 'src/', | |
| GEMINI_IGNORE_FILE_NAME, | |
| 'src/not-ignored.js', | |
| ]); | |
| }); | |
| it('should combine .gitignore and .geminiignore rules', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': 'dist/', | |
| [GEMINI_IGNORE_FILE_NAME]: 'build/', | |
| dist: ['ignored-by-git.js'], | |
| build: ['ignored-by-gemini.js'], | |
| src: ['not-ignored.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: true, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual([ | |
| 'src/', | |
| GEMINI_IGNORE_FILE_NAME, | |
| '.gitignore', | |
| 'src/not-ignored.js', | |
| ]); | |
| }); | |
| it('should use ignoreDirs option', async () => { | |
| tmpDir = await createTmpDir({ | |
| logs: ['some.log'], | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: ['logs'], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual(['src/', 'src/main.js']); | |
| }); | |
| it('should handle negated directories', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': ['build/**', '!build/public', '!build/public/**'].join( | |
| '\n', | |
| ), | |
| build: { | |
| 'private.js': '', | |
| public: ['index.html'], | |
| }, | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual([ | |
| 'build/', | |
| 'build/public/', | |
| 'src/', | |
| '.gitignore', | |
| 'build/public/index.html', | |
| 'src/main.js', | |
| ]); | |
| }); | |
| it('should include newly created directory when watcher is enabled', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableFileWatcher: true, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| await new Promise((resolve) => setTimeout(resolve, 300)); | |
| await fs.mkdir(path.join(tmpDir, 'new-folder')); | |
| await new Promise((resolve) => setTimeout(resolve, 1200)); | |
| const results = await fileSearch.search('new-folder'); | |
| expect(results).toContain('new-folder/'); | |
| }); | |
| it('should include newly created file and remove it after deletion when watcher is enabled', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableFileWatcher: true, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| await new Promise((resolve) => setTimeout(resolve, 300)); | |
| const filePath = path.join(tmpDir, 'watcher-file.txt'); | |
| await fs.writeFile(filePath, 'hello'); | |
| await new Promise((resolve) => setTimeout(resolve, 1200)); | |
| let results = await fileSearch.search('watcher-file'); | |
| expect(results).toContain('watcher-file.txt'); | |
| await fs.rm(filePath, { force: true }); | |
| await new Promise((resolve) => setTimeout(resolve, 1200)); | |
| results = await fileSearch.search('watcher-file'); | |
| expect(results).not.toContain('watcher-file.txt'); | |
| }); | |
| it('should filter results with a search pattern', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'main.js': '', | |
| 'util.ts': '', | |
| 'style.css': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('**/*.js'); | |
| expect(results).toEqual(['src/main.js']); | |
| }); | |
| it('should handle root-level file negation', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': ['*.mk', '!Foo.mk'].join('\n'), | |
| 'bar.mk': '', | |
| 'Foo.mk': '', | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual(['.gitignore', 'Foo.mk']); | |
| }); | |
| it('should handle directory negation with glob', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': [ | |
| 'third_party/**', | |
| '!third_party/foo', | |
| '!third_party/foo/bar', | |
| '!third_party/foo/bar/baz_buffer', | |
| ].join('\n'), | |
| third_party: { | |
| foo: { | |
| bar: { | |
| baz_buffer: '', | |
| }, | |
| }, | |
| ignore_this: '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual([ | |
| 'third_party/', | |
| 'third_party/foo/', | |
| 'third_party/foo/bar/', | |
| '.gitignore', | |
| 'third_party/foo/bar/baz_buffer', | |
| ]); | |
| }); | |
| it('should correctly handle negated patterns in .gitignore', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': ['dist/**', '!dist/keep.js'].join('\n'), | |
| dist: ['ignore.js', 'keep.js'], | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual([ | |
| 'dist/', | |
| 'src/', | |
| '.gitignore', | |
| 'dist/keep.js', | |
| 'src/main.js', | |
| ]); | |
| }); | |
| // New test cases start here | |
| it('should initialize correctly when ignore files are missing', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: ['file1.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: true, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| // Expect no errors to be thrown during initialization | |
| await expect(fileSearch.initialize()).resolves.toBeUndefined(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual(['src/', 'src/file1.js']); | |
| }); | |
| it('should respect maxResults option in search', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'file1.js': '', | |
| 'file2.js': '', | |
| 'file3.js': '', | |
| 'file4.js': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('**/*.js', { maxResults: 2 }); | |
| expect(results).toEqual(['src/file1.js', 'src/file2.js']); // Assuming alphabetical sort | |
| }); | |
| it('should use fzf for fuzzy matching when pattern does not contain wildcards', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'main.js': '', | |
| 'util.ts': '', | |
| 'style.css': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('sst'); | |
| expect(results).toEqual(['src/style.css']); | |
| }); | |
| it('should not use fzf for fuzzy matching when enableFuzzySearch is false', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'file1.js': '', | |
| 'flexible.js': '', | |
| 'other.ts': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: false, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('fle'); | |
| expect(results).toEqual(['src/flexible.js']); | |
| }); | |
| it('should use fzf for fuzzy matching when enableFuzzySearch is true', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'file1.js': '', | |
| 'flexible.js': '', | |
| 'other.ts': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('fle'); | |
| expect(results).toEqual( | |
| expect.arrayContaining(['src/file1.js', 'src/flexible.js']), | |
| ); | |
| }); | |
| it('should prioritize filenames closer to the end of the path and shorter paths', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'hooks.ts': '', | |
| hooks: { | |
| 'index.ts': '', | |
| }, | |
| utils: { | |
| 'hooks.tsx': '', | |
| }, | |
| 'hooks-dev': { | |
| 'test.ts': '', | |
| }, | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('hooks'); | |
| // The order should prioritize matches closer to the end and shorter strings. | |
| // FZF matches right-to-left. | |
| expect(results[0]).toBe('src/hooks/'); | |
| expect(results[1]).toBe('src/hooks.ts'); | |
| expect(results[2]).toBe('src/utils/hooks.tsx'); | |
| expect(results[3]).toBe('src/hooks-dev/'); | |
| expect(results[4]).toBe('src/hooks/index.ts'); | |
| expect(results[5]).toBe('src/hooks-dev/test.ts'); | |
| }); | |
| it('should return empty array when no matches are found', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: ['file1.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('nonexistent-file.xyz'); | |
| expect(results).toEqual([]); | |
| }); | |
| it('should throw AbortError when filter is aborted', async () => { | |
| const controller = new AbortController(); | |
| const dummyPaths = Array.from({ length: 5000 }, (_, i) => `file${i}.js`); // Large array to ensure yielding | |
| const filterPromise = filter(dummyPaths, '*.js', controller.signal); | |
| // Abort after a short delay to ensure filter has started | |
| setTimeout(() => controller.abort(), 1); | |
| await expect(filterPromise).rejects.toThrow(AbortError); | |
| }); | |
| it('should throw an error if search is called before initialization', async () => { | |
| tmpDir = await createTmpDir({}); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await expect(fileSearch.search('')).rejects.toThrow( | |
| 'Engine not initialized. Call initialize() first.', | |
| ); | |
| }); | |
| it('should handle empty or commented-only ignore files', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': '# This is a comment\n\n \n', | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual(['src/', '.gitignore', 'src/main.js']); | |
| }); | |
| it('should always ignore the .git directory', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': ['config', 'HEAD'], | |
| src: ['main.js'], | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, // Explicitly disable .gitignore to isolate this rule | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search(''); | |
| expect(results).toEqual(['src/', 'src/main.js']); | |
| }); | |
| it('should respect default maxFiles budget of 20000 in RecursiveFileSearch', async () => { | |
| const crawlSpy = vi.spyOn(crawler, 'crawl'); | |
| tmpDir = await createTmpDir({ | |
| 'file1.js': '', | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| expect(crawlSpy).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| maxFiles: 20000, | |
| }), | |
| ); | |
| }); | |
| it('should be cancellable via AbortSignal', async () => { | |
| const largeDir: Record<string, string> = {}; | |
| for (let i = 0; i < 100; i++) { | |
| largeDir[`file${i}.js`] = ''; | |
| } | |
| tmpDir = await createTmpDir(largeDir); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const controller = new AbortController(); | |
| const searchPromise = fileSearch.search('**/*.js', { | |
| signal: controller.signal, | |
| }); | |
| // Yield to allow the search to start before aborting. | |
| await new Promise((resolve) => setImmediate(resolve)); | |
| controller.abort(); | |
| await expect(searchPromise).rejects.toThrow(AbortError); | |
| }); | |
| it('should leverage ResultCache for bestBaseQuery optimization', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'foo.js': '', | |
| 'bar.ts': '', | |
| nested: { | |
| 'baz.js': '', | |
| }, | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: true, // Enable caching for this test | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| // Perform a broad search to prime the cache | |
| const broadResults = await fileSearch.search('src/**'); | |
| expect(broadResults).toEqual([ | |
| 'src/', | |
| 'src/nested/', | |
| 'src/bar.ts', | |
| 'src/foo.js', | |
| 'src/nested/baz.js', | |
| ]); | |
| // Perform a more specific search that should leverage the broad search's cached results | |
| const specificResults = await fileSearch.search('src/**/*.js'); | |
| expect(specificResults).toEqual(['src/foo.js', 'src/nested/baz.js']); | |
| // Although we can't directly inspect ResultCache.hits/misses from here, | |
| // the correctness of specificResults after a broad search implicitly | |
| // verifies that the caching mechanism, including bestBaseQuery, is working. | |
| }); | |
| it('should be case-insensitive by default', async () => { | |
| tmpDir = await createTmpDir({ | |
| 'File1.Js': '', | |
| 'file2.js': '', | |
| 'FILE3.JS': '', | |
| 'other.txt': '', | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| // Search with a lowercase pattern | |
| let results = await fileSearch.search('file*.js'); | |
| expect(results).toHaveLength(3); | |
| expect(results).toEqual( | |
| expect.arrayContaining(['File1.Js', 'file2.js', 'FILE3.JS']), | |
| ); | |
| // Search with an uppercase pattern | |
| results = await fileSearch.search('FILE*.JS'); | |
| expect(results).toHaveLength(3); | |
| expect(results).toEqual( | |
| expect.arrayContaining(['File1.Js', 'file2.js', 'FILE3.JS']), | |
| ); | |
| // Search with a mixed-case pattern | |
| results = await fileSearch.search('FiLe*.Js'); | |
| expect(results).toHaveLength(3); | |
| expect(results).toEqual( | |
| expect.arrayContaining(['File1.Js', 'file2.js', 'FILE3.JS']), | |
| ); | |
| }); | |
| it('should respect maxResults even when the cache returns an exact match', async () => { | |
| tmpDir = await createTmpDir({ | |
| 'file1.js': '', | |
| 'file2.js': '', | |
| 'file3.js': '', | |
| 'file4.js': '', | |
| 'file5.js': '', | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: true, // Ensure caching is enabled | |
| cacheTtl: 10000, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| // 1. Perform a broad search to populate the cache with an exact match. | |
| const initialResults = await fileSearch.search('*.js'); | |
| expect(initialResults).toEqual([ | |
| 'file1.js', | |
| 'file2.js', | |
| 'file3.js', | |
| 'file4.js', | |
| 'file5.js', | |
| ]); | |
| // 2. Perform the same search again, but this time with a maxResults limit. | |
| const limitedResults = await fileSearch.search('*.js', { maxResults: 2 }); | |
| // 3. Assert that the maxResults limit was respected, even with a cache hit. | |
| expect(limitedResults).toEqual(['file1.js', 'file2.js']); | |
| }); | |
| it('should handle file paths with special characters that need escaping', async () => { | |
| tmpDir = await createTmpDir({ | |
| src: { | |
| 'file with (special) chars.txt': '', | |
| 'another-file.txt': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: true, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| // Search for the file using a pattern that contains special characters. | |
| // The `unescapePath` function should handle the escaped path correctly. | |
| const searchPattern = escapePath('src/file with (special) chars.txt'); | |
| const results = await fileSearch.search(searchPattern); | |
| expect(results.map((r) => path.normalize(r))).toEqual([ | |
| path.normalize('src/file with (special) chars.txt'), | |
| ]); | |
| }); | |
| describe('DirectoryFileSearch', () => { | |
| it('should search for files in the current directory', async () => { | |
| tmpDir = await createTmpDir({ | |
| 'file1.js': '', | |
| 'file2.ts': '', | |
| 'file3.js': '', | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: false, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('*.js'); | |
| expect(results).toEqual(['file1.js', 'file3.js']); | |
| }); | |
| it('should search for files in a subdirectory', async () => { | |
| tmpDir = await createTmpDir({ | |
| 'file1.js': '', | |
| src: { | |
| 'file2.js': '', | |
| 'file3.ts': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: false, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('src/*.js'); | |
| expect(results).toEqual(['src/file2.js']); | |
| }); | |
| it('should list all files in a directory', async () => { | |
| tmpDir = await createTmpDir({ | |
| 'file1.js': '', | |
| src: { | |
| 'file2.js': '', | |
| 'file3.ts': '', | |
| }, | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: false, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('src/'); | |
| expect(results).toEqual(['src/file2.js', 'src/file3.ts']); | |
| }); | |
| it('should respect ignore rules', async () => { | |
| tmpDir = await createTmpDir({ | |
| '.git': {}, | |
| '.gitignore': '*.js', | |
| 'file1.js': '', | |
| 'file2.ts': '', | |
| }); | |
| const fileSearch = FileSearchFactory.create({ | |
| projectRoot: tmpDir, | |
| fileDiscoveryService: new FileDiscoveryService(tmpDir, { | |
| respectGitIgnore: true, | |
| respectGeminiIgnore: false, | |
| }), | |
| ignoreDirs: [], | |
| cache: false, | |
| cacheTtl: 0, | |
| enableRecursiveFileSearch: false, | |
| enableFuzzySearch: true, | |
| }); | |
| await fileSearch.initialize(); | |
| const results = await fileSearch.search('*'); | |
| expect(results).toEqual(['.gitignore', 'file2.ts']); | |
| }); | |
| }); | |
| }); | |