diff options
Diffstat (limited to 'packages/cli/src/ui/hooks/useCompletion.integration.test.ts')
| -rw-r--r-- | packages/cli/src/ui/hooks/useCompletion.integration.test.ts | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts new file mode 100644 index 00000000..5235dbd5 --- /dev/null +++ b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts @@ -0,0 +1,228 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import type { Mocked } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useCompletion } from './useCompletion.js'; +import * as fs from 'fs/promises'; +import { FileDiscoveryService } from '@gemini-code/core'; + +// Mock dependencies +vi.mock('fs/promises'); +vi.mock('@gemini-code/core', async () => { + const actual = await vi.importActual('@gemini-code/core'); + return { + ...actual, + FileDiscoveryService: vi.fn(), + isNodeError: vi.fn((error) => error.code === 'ENOENT'), + escapePath: vi.fn((path) => path), + unescapePath: vi.fn((path) => path), + getErrorMessage: vi.fn((error) => error.message), + }; +}); + +describe('useCompletion git-aware filtering integration', () => { + let mockFileDiscoveryService: Mocked<FileDiscoveryService>; + let mockConfig: { + fileFiltering?: { enabled?: boolean; respectGitignore?: boolean }; + }; + const testCwd = '/test/project'; + const slashCommands = [ + { name: 'help', description: 'Show help', action: vi.fn() }, + { name: 'clear', description: 'Clear screen', action: vi.fn() }, + ]; + + beforeEach(() => { + mockFileDiscoveryService = { + initialize: vi.fn(), + shouldIgnoreFile: vi.fn(), + filterFiles: vi.fn(), + getIgnoreInfo: vi.fn(() => ({ gitIgnored: [], customIgnored: [] })), + }; + + mockConfig = { + getFileFilteringRespectGitIgnore: vi.fn(() => true), + getFileFilteringAllowBuildArtifacts: vi.fn(() => false), + getFileService: vi.fn().mockResolvedValue(mockFileDiscoveryService), + }; + + vi.mocked(FileDiscoveryService).mockImplementation( + () => mockFileDiscoveryService, + ); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should filter git-ignored directories from @ completions', async () => { + // Mock fs.readdir to return both regular and git-ignored directories + vi.mocked(fs.readdir).mockResolvedValue([ + { name: 'src', isDirectory: () => true }, + { name: 'node_modules', isDirectory: () => true }, + { name: 'dist', isDirectory: () => true }, + { name: 'README.md', isDirectory: () => false }, + { name: '.env', isDirectory: () => false }, + ] as Array<{ name: string; isDirectory: () => boolean }>); + + // Mock git ignore service to ignore certain files + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string) => + path.includes('node_modules') || + path.includes('dist') || + path.includes('.env'), + ); + + const { result } = renderHook(() => + useCompletion('@', testCwd, true, slashCommands, mockConfig), + ); + + // Wait for async operations to complete + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 150)); // Account for debounce + }); + + expect(result.current.suggestions).toHaveLength(2); + expect(result.current.suggestions).toEqual( + expect.arrayContaining([ + { label: 'src/', value: 'src/' }, + { label: 'README.md', value: 'README.md' }, + ]), + ); + expect(result.current.showSuggestions).toBe(true); + }); + + it('should handle recursive search with git-aware filtering', async () => { + // Mock the recursive file search scenario + vi.mocked(fs.readdir).mockImplementation( + async (dirPath: string | Buffer | URL) => { + if (dirPath === testCwd) { + return [ + { name: 'src', isDirectory: () => true }, + { name: 'node_modules', isDirectory: () => true }, + { name: 'temp', isDirectory: () => true }, + ] as Array<{ name: string; isDirectory: () => boolean }>; + } + if (dirPath.endsWith('/src')) { + return [ + { name: 'index.ts', isDirectory: () => false }, + { name: 'components', isDirectory: () => true }, + ] as Array<{ name: string; isDirectory: () => boolean }>; + } + if (dirPath.endsWith('/temp')) { + return [{ name: 'temp.log', isDirectory: () => false }] as Array<{ + name: string; + isDirectory: () => boolean; + }>; + } + return [] as Array<{ name: string; isDirectory: () => boolean }>; + }, + ); + + // Mock git ignore service + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string) => path.includes('node_modules') || path.includes('temp'), + ); + + const { result } = renderHook(() => + useCompletion('@t', testCwd, true, slashCommands, mockConfig), + ); + + // Wait for async operations to complete + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 150)); + }); + + // Should not include anything from node_modules or dist + const suggestionLabels = result.current.suggestions.map((s) => s.label); + expect(suggestionLabels).not.toContain('temp/'); + expect(suggestionLabels.some((l) => l.includes('node_modules'))).toBe( + false, + ); + }); + + it('should work without config (fallback behavior)', async () => { + vi.mocked(fs.readdir).mockResolvedValue([ + { name: 'src', isDirectory: () => true }, + { name: 'node_modules', isDirectory: () => true }, + { name: 'README.md', isDirectory: () => false }, + ] as Array<{ name: string; isDirectory: () => boolean }>); + + const { result } = renderHook(() => + useCompletion('@', testCwd, true, slashCommands, undefined), + ); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 150)); + }); + + // Without config, should include all files + expect(result.current.suggestions).toHaveLength(3); + expect(result.current.suggestions).toEqual( + expect.arrayContaining([ + { label: 'src/', value: 'src/' }, + { label: 'node_modules/', value: 'node_modules/' }, + { label: 'README.md', value: 'README.md' }, + ]), + ); + }); + + it('should handle git discovery service initialization failure gracefully', async () => { + mockFileDiscoveryService.initialize.mockRejectedValue( + new Error('Git not found'), + ); + + vi.mocked(fs.readdir).mockResolvedValue([ + { name: 'src', isDirectory: () => true }, + { name: 'README.md', isDirectory: () => false }, + ] as Array<{ name: string; isDirectory: () => boolean }>); + + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const { result } = renderHook(() => + useCompletion('@', testCwd, true, slashCommands, mockConfig), + ); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 150)); + }); + + // Since we use centralized service, initialization errors are handled at config level + // This test should verify graceful fallback behavior + expect(result.current.suggestions.length).toBeGreaterThanOrEqual(0); + // Should still show completions even if git discovery fails + expect(result.current.suggestions.length).toBeGreaterThan(0); + + consoleSpy.mockRestore(); + }); + + it('should handle directory-specific completions with git filtering', async () => { + vi.mocked(fs.readdir).mockResolvedValue([ + { name: 'component.tsx', isDirectory: () => false }, + { name: 'temp.log', isDirectory: () => false }, + { name: 'index.ts', isDirectory: () => false }, + ] as Array<{ name: string; isDirectory: () => boolean }>); + + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string) => path.includes('.log'), + ); + + const { result } = renderHook(() => + useCompletion('@src/comp', testCwd, true, slashCommands, mockConfig), + ); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 150)); + }); + + // Should filter out .log files but include matching .tsx files + expect(result.current.suggestions).toEqual([ + { label: 'component.tsx', value: 'component.tsx' }, + ]); + }); +}); |
