mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 17:11:04 -07:00
812 lines
21 KiB
TypeScript
812 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import * as fs from 'fs/promises';
|
|
import * as path from 'path';
|
|
import * as cache from './crawlCache.js';
|
|
import { FileSearch, AbortError, filter } from './fileSearch.js';
|
|
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
|
|
|
|
type FileSearchWithPrivateMethods = FileSearch & {
|
|
performCrawl: () => Promise<void>;
|
|
};
|
|
|
|
describe('FileSearch', () => {
|
|
let tmpDir: string;
|
|
afterEach(async () => {
|
|
if (tmpDir) {
|
|
await cleanupTmpDir(tmpDir);
|
|
}
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should use .geminiignore rules', async () => {
|
|
tmpDir = await createTmpDir({
|
|
'.geminiignore': 'dist/',
|
|
dist: ['ignored.js'],
|
|
src: ['not-ignored.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: true,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual(['src/', '.geminiignore', 'src/not-ignored.js']);
|
|
});
|
|
|
|
it('should combine .gitignore and .geminiignore rules', async () => {
|
|
tmpDir = await createTmpDir({
|
|
'.gitignore': 'dist/',
|
|
'.geminiignore': 'build/',
|
|
dist: ['ignored-by-git.js'],
|
|
build: ['ignored-by-gemini.js'],
|
|
src: ['not-ignored.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: true,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual([
|
|
'src/',
|
|
'.geminiignore',
|
|
'.gitignore',
|
|
'src/not-ignored.js',
|
|
]);
|
|
});
|
|
|
|
it('should use ignoreDirs option', async () => {
|
|
tmpDir = await createTmpDir({
|
|
logs: ['some.log'],
|
|
src: ['main.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: ['logs'],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual(['src/', 'src/main.js']);
|
|
});
|
|
|
|
it('should handle negated directories', async () => {
|
|
tmpDir = await createTmpDir({
|
|
'.gitignore': ['build/**', '!build/public', '!build/public/**'].join(
|
|
'\n',
|
|
),
|
|
build: {
|
|
'private.js': '',
|
|
public: ['index.html'],
|
|
},
|
|
src: ['main.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 filter results with a search pattern', async () => {
|
|
tmpDir = await createTmpDir({
|
|
src: {
|
|
'main.js': '',
|
|
'util.ts': '',
|
|
'style.css': '',
|
|
},
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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({
|
|
'.gitignore': ['*.mk', '!Foo.mk'].join('\n'),
|
|
'bar.mk': '',
|
|
'Foo.mk': '',
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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({
|
|
'.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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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({
|
|
'.gitignore': ['dist/**', '!dist/keep.js'].join('\n'),
|
|
dist: ['ignore.js', 'keep.js'],
|
|
src: ['main.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: true,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
// 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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('sst');
|
|
|
|
expect(results).toEqual(['src/style.css']);
|
|
});
|
|
|
|
it('should return empty array when no matches are found', async () => {
|
|
tmpDir = await createTmpDir({
|
|
src: ['file1.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
describe('with in-memory cache', () => {
|
|
beforeEach(() => {
|
|
cache.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should throw an error if search is called before initialization', async () => {
|
|
tmpDir = await createTmpDir({});
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
await expect(fileSearch.search('')).rejects.toThrow(
|
|
'Engine not initialized. Call initialize() first.',
|
|
);
|
|
});
|
|
|
|
it('should hit the cache for subsequent searches', async () => {
|
|
tmpDir = await createTmpDir({ 'file1.js': '' });
|
|
const getOptions = () => ({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: true,
|
|
cacheTtl: 10,
|
|
});
|
|
|
|
const fs1 = new FileSearch(getOptions());
|
|
const crawlSpy1 = vi.spyOn(
|
|
fs1 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs1.initialize();
|
|
expect(crawlSpy1).toHaveBeenCalledTimes(1);
|
|
|
|
// Second search should hit the cache because the options are identical
|
|
const fs2 = new FileSearch(getOptions());
|
|
const crawlSpy2 = vi.spyOn(
|
|
fs2 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs2.initialize();
|
|
expect(crawlSpy2).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should miss the cache when ignore rules change', async () => {
|
|
tmpDir = await createTmpDir({
|
|
'.gitignore': 'a.txt',
|
|
'a.txt': '',
|
|
'b.txt': '',
|
|
});
|
|
const options = {
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: true,
|
|
cacheTtl: 10000,
|
|
};
|
|
|
|
// Initial search to populate the cache
|
|
const fs1 = new FileSearch(options);
|
|
const crawlSpy1 = vi.spyOn(
|
|
fs1 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs1.initialize();
|
|
const results1 = await fs1.search('');
|
|
expect(crawlSpy1).toHaveBeenCalledTimes(1);
|
|
expect(results1).toEqual(['.gitignore', 'b.txt']);
|
|
|
|
// Modify the ignore file
|
|
await fs.writeFile(path.join(tmpDir, '.gitignore'), 'b.txt');
|
|
|
|
// Second search should miss the cache and trigger a recrawl
|
|
const fs2 = new FileSearch(options);
|
|
const crawlSpy2 = vi.spyOn(
|
|
fs2 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs2.initialize();
|
|
const results2 = await fs2.search('');
|
|
expect(crawlSpy2).toHaveBeenCalledTimes(1);
|
|
expect(results2).toEqual(['.gitignore', 'a.txt']);
|
|
});
|
|
|
|
it('should miss the cache after TTL expires', async () => {
|
|
vi.useFakeTimers();
|
|
tmpDir = await createTmpDir({ 'file1.js': '' });
|
|
const options = {
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: true,
|
|
cacheTtl: 10, // 10 seconds
|
|
};
|
|
|
|
// Initial search to populate the cache
|
|
const fs1 = new FileSearch(options);
|
|
await fs1.initialize();
|
|
|
|
// Advance time past the TTL
|
|
await vi.advanceTimersByTimeAsync(11000);
|
|
|
|
// Second search should miss the cache and trigger a recrawl
|
|
const fs2 = new FileSearch(options);
|
|
const crawlSpy = vi.spyOn(
|
|
fs2 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs2.initialize();
|
|
|
|
expect(crawlSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should miss the cache when maxDepth changes', async () => {
|
|
tmpDir = await createTmpDir({ 'file1.js': '' });
|
|
const getOptions = (maxDepth?: number) => ({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: true,
|
|
cacheTtl: 10000,
|
|
maxDepth,
|
|
});
|
|
|
|
// 1. First search with maxDepth: 1, should trigger a crawl.
|
|
const fs1 = new FileSearch(getOptions(1));
|
|
const crawlSpy1 = vi.spyOn(
|
|
fs1 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs1.initialize();
|
|
expect(crawlSpy1).toHaveBeenCalledTimes(1);
|
|
|
|
// 2. Second search with maxDepth: 2, should be a cache miss and trigger a crawl.
|
|
const fs2 = new FileSearch(getOptions(2));
|
|
const crawlSpy2 = vi.spyOn(
|
|
fs2 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs2.initialize();
|
|
expect(crawlSpy2).toHaveBeenCalledTimes(1);
|
|
|
|
// 3. Third search with maxDepth: 1 again, should be a cache hit.
|
|
const fs3 = new FileSearch(getOptions(1));
|
|
const crawlSpy3 = vi.spyOn(
|
|
fs3 as FileSearchWithPrivateMethods,
|
|
'performCrawl',
|
|
);
|
|
await fs3.initialize();
|
|
expect(crawlSpy3).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('should handle empty or commented-only ignore files', async () => {
|
|
tmpDir = await createTmpDir({
|
|
'.gitignore': '# This is a comment\n\n \n',
|
|
src: ['main.js'],
|
|
});
|
|
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: true,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false, // Explicitly disable .gitignore to isolate this rule
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual(['src/', 'src/main.js']);
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: true, // Enable caching for this test
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
});
|
|
|
|
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 = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: true, // Ensure caching is enabled
|
|
cacheTtl: 10000,
|
|
});
|
|
|
|
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']);
|
|
});
|
|
|
|
describe('with maxDepth', () => {
|
|
beforeEach(async () => {
|
|
tmpDir = await createTmpDir({
|
|
'file-root.txt': '',
|
|
level1: {
|
|
'file-level1.txt': '',
|
|
level2: {
|
|
'file-level2.txt': '',
|
|
level3: {
|
|
'file-level3.txt': '',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should only search top-level files when maxDepth is 0', async () => {
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
maxDepth: 0,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual(['level1/', 'file-root.txt']);
|
|
});
|
|
|
|
it('should search one level deep when maxDepth is 1', async () => {
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
maxDepth: 1,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual([
|
|
'level1/',
|
|
'level1/level2/',
|
|
'file-root.txt',
|
|
'level1/file-level1.txt',
|
|
]);
|
|
});
|
|
|
|
it('should search two levels deep when maxDepth is 2', async () => {
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
maxDepth: 2,
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual([
|
|
'level1/',
|
|
'level1/level2/',
|
|
'level1/level2/level3/',
|
|
'file-root.txt',
|
|
'level1/file-level1.txt',
|
|
'level1/level2/file-level2.txt',
|
|
]);
|
|
});
|
|
|
|
it('should perform a full recursive search when maxDepth is undefined', async () => {
|
|
const fileSearch = new FileSearch({
|
|
projectRoot: tmpDir,
|
|
useGitignore: false,
|
|
useGeminiignore: false,
|
|
ignoreDirs: [],
|
|
cache: false,
|
|
cacheTtl: 0,
|
|
maxDepth: undefined, // Explicitly undefined
|
|
});
|
|
|
|
await fileSearch.initialize();
|
|
const results = await fileSearch.search('');
|
|
|
|
expect(results).toEqual([
|
|
'level1/',
|
|
'level1/level2/',
|
|
'level1/level2/level3/',
|
|
'file-root.txt',
|
|
'level1/file-level1.txt',
|
|
'level1/level2/file-level2.txt',
|
|
'level1/level2/level3/file-level3.txt',
|
|
]);
|
|
});
|
|
});
|
|
});
|