diff --git a/packages/core/src/utils/bfsFileSearch.test.ts b/packages/core/src/utils/bfsFileSearch.test.ts index 3ce452de..63198a8d 100644 --- a/packages/core/src/utils/bfsFileSearch.test.ts +++ b/packages/core/src/utils/bfsFileSearch.test.ts @@ -4,184 +4,189 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs'; -import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fsPromises from 'fs/promises'; -import * as gitUtils from './gitUtils.js'; +import * as path from 'path'; +import * as os from 'os'; import { bfsFileSearch } from './bfsFileSearch.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; -vi.mock('fs'); -vi.mock('fs/promises'); -vi.mock('./gitUtils.js'); - -const createMockDirent = (name: string, isFile: boolean): fs.Dirent => { - const dirent = new fs.Dirent(); - dirent.name = name; - dirent.isFile = () => isFile; - dirent.isDirectory = () => !isFile; - return dirent; -}; - -// Type for the specific overload we're using -type ReaddirWithFileTypes = ( - path: fs.PathLike, - options: { withFileTypes: true }, -) => Promise; - describe('bfsFileSearch', () => { - beforeEach(() => { - vi.resetAllMocks(); + let testRootDir: string; + + async function createEmptyDir(...pathSegments: string[]) { + const fullPath = path.join(testRootDir, ...pathSegments); + await fsPromises.mkdir(fullPath, { recursive: true }); + return fullPath; + } + + async function createTestFile(content: string, ...pathSegments: string[]) { + const fullPath = path.join(testRootDir, ...pathSegments); + await fsPromises.mkdir(path.dirname(fullPath), { recursive: true }); + await fsPromises.writeFile(fullPath, content); + return fullPath; + } + + beforeEach(async () => { + testRootDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), 'bfs-file-search-test-'), + ); + }); + + afterEach(async () => { + await fsPromises.rm(testRootDir, { recursive: true, force: true }); }); it('should find a file in the root directory', async () => { - const mockFs = vi.mocked(fsPromises); - const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes; - vi.mocked(mockReaddir).mockResolvedValue([ - createMockDirent('file1.txt', true), - createMockDirent('file2.txt', true), - ]); - - const result = await bfsFileSearch('/test', { fileName: 'file1.txt' }); - expect(result).toEqual(['/test/file1.txt']); + const targetFilePath = await createTestFile('content', 'target.txt'); + const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); + expect(result).toEqual([targetFilePath]); }); - it('should find a file in a subdirectory', async () => { - const mockFs = vi.mocked(fsPromises); - const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes; - vi.mocked(mockReaddir).mockImplementation(async (dir) => { - if (dir === '/test') { - return [createMockDirent('subdir', false)]; - } - if (dir === '/test/subdir') { - return [createMockDirent('file1.txt', true)]; - } - return []; - }); - - const result = await bfsFileSearch('/test', { fileName: 'file1.txt' }); - expect(result).toEqual(['/test/subdir/file1.txt']); + it('should find a file in a nested directory', async () => { + const targetFilePath = await createTestFile( + 'content', + 'a', + 'b', + 'target.txt', + ); + const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); + expect(result).toEqual([targetFilePath]); }); - it('should ignore specified directories', async () => { - const mockFs = vi.mocked(fsPromises); - const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes; - vi.mocked(mockReaddir).mockImplementation(async (dir) => { - if (dir === '/test') { - return [ - createMockDirent('subdir1', false), - createMockDirent('subdir2', false), - ]; - } - if (dir === '/test/subdir1') { - return [createMockDirent('file1.txt', true)]; - } - if (dir === '/test/subdir2') { - return [createMockDirent('file1.txt', true)]; - } - return []; - }); - - const result = await bfsFileSearch('/test', { - fileName: 'file1.txt', - ignoreDirs: ['subdir2'], - }); - expect(result).toEqual(['/test/subdir1/file1.txt']); + it('should find multiple files with the same name', async () => { + const targetFilePath1 = await createTestFile('content1', 'a', 'target.txt'); + const targetFilePath2 = await createTestFile('content2', 'b', 'target.txt'); + const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); + result.sort(); + expect(result).toEqual([targetFilePath1, targetFilePath2].sort()); }); - it('should respect maxDirs limit', async () => { - const mockFs = vi.mocked(fsPromises); - const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes; - vi.mocked(mockReaddir).mockImplementation(async (dir) => { - if (dir === '/test') { - return [ - createMockDirent('subdir1', false), - createMockDirent('subdir2', false), - ]; - } - if (dir === '/test/subdir1') { - return [createMockDirent('file1.txt', true)]; - } - if (dir === '/test/subdir2') { - return [createMockDirent('file1.txt', true)]; - } - return []; - }); - - const result = await bfsFileSearch('/test', { - fileName: 'file1.txt', - maxDirs: 2, - }); - expect(result).toEqual(['/test/subdir1/file1.txt']); + it('should return an empty array if no file is found', async () => { + await createTestFile('content', 'other.txt'); + const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); + expect(result).toEqual([]); }); - it('should respect .gitignore files', async () => { - const mockFs = vi.mocked(fsPromises); - const mockGitUtils = vi.mocked(gitUtils); - mockGitUtils.isGitRepository.mockReturnValue(true); - const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes; - vi.mocked(mockReaddir).mockImplementation(async (dir) => { - if (dir === '/test') { - return [ - createMockDirent('.gitignore', true), - createMockDirent('subdir1', false), - createMockDirent('subdir2', false), - ]; - } - if (dir === '/test/subdir1') { - return [createMockDirent('file1.txt', true)]; - } - if (dir === '/test/subdir2') { - return [createMockDirent('file1.txt', true)]; - } - return []; + it('should ignore directories specified in ignoreDirs', async () => { + await createTestFile('content', 'ignored', 'target.txt'); + const targetFilePath = await createTestFile( + 'content', + 'not-ignored', + 'target.txt', + ); + const result = await bfsFileSearch(testRootDir, { + fileName: 'target.txt', + ignoreDirs: ['ignored'], }); - vi.mocked(fs).readFileSync.mockReturnValue('subdir2'); - - const fileService = new FileDiscoveryService('/test'); - const result = await bfsFileSearch('/test', { - fileName: 'file1.txt', - fileService, - }); - expect(result).toEqual(['/test/subdir1/file1.txt']); + expect(result).toEqual([targetFilePath]); }); - it('should respect .geminiignore files', async () => { - const mockFs = vi.mocked(fsPromises); - const mockGitUtils = vi.mocked(gitUtils); + it('should respect the maxDirs limit and not find the file', async () => { + await createTestFile('content', 'a', 'b', 'c', 'target.txt'); + const result = await bfsFileSearch(testRootDir, { + fileName: 'target.txt', + maxDirs: 3, + }); + expect(result).toEqual([]); + }); - mockGitUtils.isGitRepository.mockReturnValue(false); + it('should respect the maxDirs limit and find the file', async () => { + const targetFilePath = await createTestFile( + 'content', + 'a', + 'b', + 'c', + 'target.txt', + ); + const result = await bfsFileSearch(testRootDir, { + fileName: 'target.txt', + maxDirs: 4, + }); + expect(result).toEqual([targetFilePath]); + }); - const mockReaddir = mockFs.readdir as unknown as ReaddirWithFileTypes; - vi.mocked(mockReaddir).mockImplementation(async (dir) => { - if (dir === '/test') { - return [ - createMockDirent('.geminiignore', true), - createMockDirent('subdir1', false), - createMockDirent('subdir2', false), - ]; - } - if (dir === '/test/subdir1') { - return [createMockDirent('file1.txt', true)]; - } - if (dir === '/test/subdir2') { - return [createMockDirent('file1.txt', true)]; - } - return []; + describe('with FileDiscoveryService', () => { + let projectRoot: string; + + beforeEach(async () => { + projectRoot = await createEmptyDir('project'); }); - vi.mocked(fs).readFileSync.mockReturnValue('subdir2'); + it('should ignore gitignored files', async () => { + await createEmptyDir('project', '.git'); + await createTestFile('node_modules/', 'project', '.gitignore'); + await createTestFile('content', 'project', 'node_modules', 'target.txt'); + const targetFilePath = await createTestFile( + 'content', + 'project', + 'not-ignored', + 'target.txt', + ); - const fileService = new FileDiscoveryService('/test'); - const result = await bfsFileSearch('/test', { - fileName: 'file1.txt', - fileService, - fileFilteringOptions: { - respectGitIgnore: true, - respectGeminiIgnore: true, - }, + const fileService = new FileDiscoveryService(projectRoot); + const result = await bfsFileSearch(projectRoot, { + fileName: 'target.txt', + fileService, + fileFilteringOptions: { + respectGitIgnore: true, + respectGeminiIgnore: true, + }, + }); + + expect(result).toEqual([targetFilePath]); }); - expect(result).toEqual(['/test/subdir1/file1.txt']); + it('should ignore geminiignored files', async () => { + await createTestFile('node_modules/', 'project', '.geminiignore'); + await createTestFile('content', 'project', 'node_modules', 'target.txt'); + const targetFilePath = await createTestFile( + 'content', + 'project', + 'not-ignored', + 'target.txt', + ); + + const fileService = new FileDiscoveryService(projectRoot); + const result = await bfsFileSearch(projectRoot, { + fileName: 'target.txt', + fileService, + fileFilteringOptions: { + respectGitIgnore: false, + respectGeminiIgnore: true, + }, + }); + + expect(result).toEqual([targetFilePath]); + }); + + it('should not ignore files if respect flags are false', async () => { + await createEmptyDir('project', '.git'); + await createTestFile('node_modules/', 'project', '.gitignore'); + const target1 = await createTestFile( + 'content', + 'project', + 'node_modules', + 'target.txt', + ); + const target2 = await createTestFile( + 'content', + 'project', + 'not-ignored', + 'target.txt', + ); + + const fileService = new FileDiscoveryService(projectRoot); + const result = await bfsFileSearch(projectRoot, { + fileName: 'target.txt', + fileService, + fileFilteringOptions: { + respectGitIgnore: false, + respectGeminiIgnore: false, + }, + }); + + expect(result.sort()).toEqual([target1, target2].sort()); + }); }); }); diff --git a/packages/core/src/utils/getFolderStructure.test.ts b/packages/core/src/utils/getFolderStructure.test.ts index 3f1b4534..f7b67ae4 100644 --- a/packages/core/src/utils/getFolderStructure.test.ts +++ b/packages/core/src/utils/getFolderStructure.test.ts @@ -309,7 +309,7 @@ ${testRootDir}${path.sep} fileService, }); expect(structure).not.toContain('ignored.txt'); - expect(structure).toContain('node_modules/...'); + expect(structure).toContain(`node_modules${path.sep}...`); expect(structure).not.toContain('logs.json'); }); @@ -334,7 +334,7 @@ ${testRootDir}${path.sep} }); expect(structure).toContain('ignored.txt'); // node_modules is still ignored by default - expect(structure).toContain('node_modules/...'); + expect(structure).toContain(`node_modules${path.sep}...`); }); }); });