summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/useCompletion.integration.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/hooks/useCompletion.integration.test.ts')
-rw-r--r--packages/cli/src/ui/hooks/useCompletion.integration.test.ts228
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' },
+ ]);
+ });
+});