diff --git a/packages/core/src/services/fileDiscoveryService.test.ts b/packages/core/src/services/fileDiscoveryService.test.ts index d7530cd6..f8a03f62 100644 --- a/packages/core/src/services/fileDiscoveryService.test.ts +++ b/packages/core/src/services/fileDiscoveryService.test.ts @@ -4,80 +4,86 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import type { Mocked } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; import { FileDiscoveryService } from './fileDiscoveryService.js'; -import { GitIgnoreParser } from '../utils/gitIgnoreParser.js'; -import * as gitUtils from '../utils/gitUtils.js'; - -// Mock the GitIgnoreParser -vi.mock('../utils/gitIgnoreParser.js'); - -// Mock gitUtils module -vi.mock('../utils/gitUtils.js'); describe('FileDiscoveryService', () => { - let service: FileDiscoveryService; - let mockGitIgnoreParser: Mocked; - const mockProjectRoot = '/test/project'; + let testRootDir: string; + let projectRoot: string; - beforeEach(() => { - mockGitIgnoreParser = { - initialize: vi.fn(), - isIgnored: vi.fn(), - loadPatterns: vi.fn(), - loadGitRepoPatterns: vi.fn(), - } as unknown as Mocked; + async function createTestFile(filePath: string, content = '') { + const fullPath = path.join(projectRoot, filePath); + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content); + return fullPath; + } - vi.mocked(GitIgnoreParser).mockImplementation(() => mockGitIgnoreParser); - vi.mocked(gitUtils.isGitRepository).mockReturnValue(true); - vi.mocked(gitUtils.findGitRoot).mockReturnValue('/test/project'); - vi.clearAllMocks(); + beforeEach(async () => { + testRootDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'file-discovery-test-'), + ); + projectRoot = path.join(testRootDir, 'project'); + await fs.mkdir(projectRoot, { recursive: true }); }); - afterEach(() => { - vi.restoreAllMocks(); + afterEach(async () => { + await fs.rm(testRootDir, { recursive: true, force: true }); }); describe('initialization', () => { - it('should initialize git ignore parser by default', () => { - service = new FileDiscoveryService(mockProjectRoot); - expect(GitIgnoreParser).toHaveBeenCalledWith(mockProjectRoot); - expect(GitIgnoreParser).toHaveBeenCalledTimes(2); - expect(mockGitIgnoreParser.loadGitRepoPatterns).toHaveBeenCalled(); - expect(mockGitIgnoreParser.loadPatterns).toHaveBeenCalled(); + it('should initialize git ignore parser by default in a git repo', async () => { + await fs.mkdir(path.join(projectRoot, '.git')); + await createTestFile('.gitignore', 'node_modules/'); + + const service = new FileDiscoveryService(projectRoot); + // Let's check the effect of the parser instead of mocking it. + expect(service.shouldGitIgnoreFile('node_modules/foo.js')).toBe(true); + expect(service.shouldGitIgnoreFile('src/foo.js')).toBe(false); }); - it('should not initialize git ignore parser when not a git repo', () => { - vi.mocked(gitUtils.isGitRepository).mockReturnValue(false); - service = new FileDiscoveryService(mockProjectRoot); + it('should not load git repo patterns when not in a git repo', async () => { + // No .git directory + await createTestFile('.gitignore', 'node_modules/'); + const service = new FileDiscoveryService(projectRoot); - expect(GitIgnoreParser).toHaveBeenCalledOnce(); - expect(mockGitIgnoreParser.loadGitRepoPatterns).not.toHaveBeenCalled(); + // .gitignore is not loaded in non-git repos + expect(service.shouldGitIgnoreFile('node_modules/foo.js')).toBe(false); + }); + + it('should load .geminiignore patterns even when not in a git repo', async () => { + await createTestFile('.geminiignore', 'secrets.txt'); + const service = new FileDiscoveryService(projectRoot); + + expect(service.shouldGeminiIgnoreFile('secrets.txt')).toBe(true); + expect(service.shouldGeminiIgnoreFile('src/index.js')).toBe(false); }); }); describe('filterFiles', () => { - beforeEach(() => { - mockGitIgnoreParser.isIgnored.mockImplementation( - (path: string) => - path.includes('node_modules') || path.includes('.git'), - ); - service = new FileDiscoveryService(mockProjectRoot); + beforeEach(async () => { + await fs.mkdir(path.join(projectRoot, '.git')); + await createTestFile('.gitignore', 'node_modules/\n.git/\ndist'); + await createTestFile('.geminiignore', 'logs/'); }); - it('should filter out git-ignored files by default', () => { + it('should filter out git-ignored and gemini-ignored files by default', () => { const files = [ 'src/index.ts', 'node_modules/package/index.js', 'README.md', '.git/config', 'dist/bundle.js', - ]; + 'logs/latest.log', + ].map((f) => path.join(projectRoot, f)); - const filtered = service.filterFiles(files); + const service = new FileDiscoveryService(projectRoot); - expect(filtered).toEqual(['src/index.ts', 'README.md', 'dist/bundle.js']); + expect(service.filterFiles(files)).toEqual( + ['src/index.ts', 'README.md'].map((f) => path.join(projectRoot, f)), + ); }); it('should not filter files when respectGitIgnore is false', () => { @@ -85,48 +91,121 @@ describe('FileDiscoveryService', () => { 'src/index.ts', 'node_modules/package/index.js', '.git/config', - ]; + 'logs/latest.log', + ].map((f) => path.join(projectRoot, f)); - const filtered = service.filterFiles(files, { respectGitIgnore: false }); + const service = new FileDiscoveryService(projectRoot); - expect(filtered).toEqual(files); + const filtered = service.filterFiles(files, { + respectGitIgnore: false, + respectGeminiIgnore: true, // still respect this one + }); + + expect(filtered).toEqual( + ['src/index.ts', 'node_modules/package/index.js', '.git/config'].map( + (f) => path.join(projectRoot, f), + ), + ); + }); + + it('should not filter files when respectGeminiIgnore is false', () => { + const files = [ + 'src/index.ts', + 'node_modules/package/index.js', + 'logs/latest.log', + ].map((f) => path.join(projectRoot, f)); + + const service = new FileDiscoveryService(projectRoot); + + const filtered = service.filterFiles(files, { + respectGitIgnore: true, + respectGeminiIgnore: false, + }); + + expect(filtered).toEqual( + ['src/index.ts', 'logs/latest.log'].map((f) => + path.join(projectRoot, f), + ), + ); }); it('should handle empty file list', () => { - const filtered = service.filterFiles([]); - expect(filtered).toEqual([]); + const service = new FileDiscoveryService(projectRoot); + + expect(service.filterFiles([])).toEqual([]); }); }); - describe('shouldGitIgnoreFile', () => { - beforeEach(() => { - mockGitIgnoreParser.isIgnored.mockImplementation((path: string) => - path.includes('node_modules'), - ); - service = new FileDiscoveryService(mockProjectRoot); + describe('shouldGitIgnoreFile & shouldGeminiIgnoreFile', () => { + beforeEach(async () => { + await fs.mkdir(path.join(projectRoot, '.git')); + await createTestFile('.gitignore', 'node_modules/'); + await createTestFile('.geminiignore', '*.log'); }); it('should return true for git-ignored files', () => { - expect(service.shouldGitIgnoreFile('node_modules/package/index.js')).toBe( - true, - ); + const service = new FileDiscoveryService(projectRoot); + + expect( + service.shouldGitIgnoreFile( + path.join(projectRoot, 'node_modules/package/index.js'), + ), + ).toBe(true); }); - it('should return false for non-ignored files', () => { - expect(service.shouldGitIgnoreFile('src/index.ts')).toBe(false); + it('should return false for non-git-ignored files', () => { + const service = new FileDiscoveryService(projectRoot); + + expect( + service.shouldGitIgnoreFile(path.join(projectRoot, 'src/index.ts')), + ).toBe(false); + }); + + it('should return true for gemini-ignored files', () => { + const service = new FileDiscoveryService(projectRoot); + + expect( + service.shouldGeminiIgnoreFile(path.join(projectRoot, 'debug.log')), + ).toBe(true); + }); + + it('should return false for non-gemini-ignored files', () => { + const service = new FileDiscoveryService(projectRoot); + + expect( + service.shouldGeminiIgnoreFile(path.join(projectRoot, 'src/index.ts')), + ).toBe(false); }); }); describe('edge cases', () => { - it('should handle relative project root paths', () => { - const relativeService = new FileDiscoveryService('./relative/path'); - expect(relativeService).toBeInstanceOf(FileDiscoveryService); + it('should handle relative project root paths', async () => { + await fs.mkdir(path.join(projectRoot, '.git')); + await createTestFile('.gitignore', 'ignored.txt'); + const service = new FileDiscoveryService( + path.relative(process.cwd(), projectRoot), + ); + + expect( + service.shouldGitIgnoreFile(path.join(projectRoot, 'ignored.txt')), + ).toBe(true); + expect( + service.shouldGitIgnoreFile(path.join(projectRoot, 'not-ignored.txt')), + ).toBe(false); }); - it('should handle filterFiles with undefined options', () => { - const files = ['src/index.ts']; - const filtered = service.filterFiles(files, undefined); - expect(filtered).toEqual(files); + it('should handle filterFiles with undefined options', async () => { + await fs.mkdir(path.join(projectRoot, '.git')); + await createTestFile('.gitignore', 'ignored.txt'); + const service = new FileDiscoveryService(projectRoot); + + const files = ['src/index.ts', 'ignored.txt'].map((f) => + path.join(projectRoot, f), + ); + + expect(service.filterFiles(files, undefined)).toEqual([ + path.join(projectRoot, 'src/index.ts'), + ]); }); }); });