diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index b2e38c94b9..73b0c45ccb 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -138,7 +138,12 @@ vi.mock('@google/gemini-cli-core', async () => { }; }); -vi.mock('./extension-manager.js'); +vi.mock('./extension-manager.js', () => { + const ExtensionManager = vi.fn(); + ExtensionManager.prototype.loadExtensions = vi.fn(); + ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]); + return { ExtensionManager }; +}); // Global setup to ensure clean environment for all tests in this file const originalArgv = process.argv; @@ -146,6 +151,11 @@ const originalGeminiModel = process.env['GEMINI_MODEL']; beforeEach(() => { delete process.env['GEMINI_MODEL']; + // Restore ExtensionManager mocks by re-assigning them + ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]); + ExtensionManager.prototype.loadExtensions = vi + .fn() + .mockResolvedValue(undefined); }); afterEach(() => { @@ -698,6 +708,12 @@ describe('loadCliConfig', () => { describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { beforeEach(() => { vi.resetAllMocks(); + // Restore ExtensionManager mocks that were reset + ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]); + ExtensionManager.prototype.loadExtensions = vi + .fn() + .mockResolvedValue(undefined); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); // Other common mocks would be reset here. }); @@ -755,6 +771,63 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { 200, // maxDirs ); }); + + it('should pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is true', async () => { + process.argv = ['node', 'script.js']; + const includeDir = path.resolve(path.sep, 'path', 'to', 'include'); + const settings = createTestMergedSettings({ + context: { + includeDirectories: [includeDir], + loadMemoryFromIncludeDirectories: true, + }, + }); + + const argv = await parseArguments(settings); + await loadCliConfig(settings, 'session-id', argv); + + expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( + expect.any(String), + [includeDir], + false, + expect.any(Object), + expect.any(ExtensionManager), + true, + 'tree', + expect.objectContaining({ + respectGitIgnore: true, + respectGeminiIgnore: true, + }), + 200, + ); + }); + + it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => { + process.argv = ['node', 'script.js']; + const settings = createTestMergedSettings({ + context: { + includeDirectories: ['/path/to/include'], + loadMemoryFromIncludeDirectories: false, + }, + }); + + const argv = await parseArguments(settings); + await loadCliConfig(settings, 'session-id', argv); + + expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( + expect.any(String), + [], + false, + expect.any(Object), + expect.any(ExtensionManager), + true, + 'tree', + expect.objectContaining({ + respectGitIgnore: true, + respectGeminiIgnore: true, + }), + 200, + ); + }); }); describe('mergeMcpServers', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 454c0b3a58..92f4d56ea4 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -476,7 +476,9 @@ export async function loadCliConfig( // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const result = await loadServerHierarchicalMemory( cwd, - [], + settings.context?.loadMemoryFromIncludeDirectories || false + ? includeDirectories + : [], debugMode, fileService, extensionManager, diff --git a/packages/cli/src/config/sandboxConfig.test.ts b/packages/cli/src/config/sandboxConfig.test.ts index 1b38909f3b..14080dc30b 100644 --- a/packages/cli/src/config/sandboxConfig.test.ts +++ b/packages/cli/src/config/sandboxConfig.test.ts @@ -25,11 +25,15 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }; }); -vi.mock('command-exists', () => ({ - default: { - sync: vi.fn(), - }, -})); +vi.mock('command-exists', () => { + const sync = vi.fn(); + return { + sync, + default: { + sync, + }, + }; +}); vi.mock('node:os', async (importOriginal) => { const actual = await importOriginal(); @@ -49,6 +53,8 @@ describe('loadSandboxConfig', () => { beforeEach(() => { vi.resetAllMocks(); process.env = { ...originalEnv }; + delete process.env['SANDBOX']; + delete process.env['GEMINI_SANDBOX']; mockedGetPackageJson.mockResolvedValue({ config: { sandboxImageUri: 'default/image' }, }); diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 59f59de4b1..c0fc9d7b7f 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -657,10 +657,48 @@ describe('gemini.tsx main function kitty protocol', () => { refreshAuth: vi.fn(), getRemoteAdminSettings: () => undefined, setTerminalBackground: vi.fn(), + getToolRegistry: () => ({ getAllTools: () => [] }), + getModel: () => 'gemini-pro', + getEmbeddingModel: () => 'embedding-model', + getCoreTools: () => [], + getApprovalMode: () => 'default', + getPreviewFeatures: () => false, + getTargetDir: () => '/', + getUsageStatisticsEnabled: () => false, + getTelemetryEnabled: () => false, + getTelemetryTarget: () => 'none', + getTelemetryOtlpEndpoint: () => '', + getTelemetryOtlpProtocol: () => 'grpc', + getTelemetryLogPromptsEnabled: () => false, + getContinueOnFailedApiCall: () => false, + getShellToolInactivityTimeout: () => 0, + getTruncateToolOutputThreshold: () => 0, + getUseRipgrep: () => false, + getUseWriteTodos: () => false, + getHooks: () => undefined, + getExperiments: () => undefined, + getFileFilteringRespectGitIgnore: () => true, + getOutputFormat: () => 'text', + getFolderTrust: () => false, + getPendingIncludeDirectories: () => [], + getWorkspaceContext: () => ({ getDirectories: () => ['/'] }), + getModelAvailabilityService: () => ({ + reset: vi.fn(), + resetTurn: vi.fn(), + }), + getBaseLlmClient: () => ({}), + getGeminiClient: () => ({}), + getContentGenerator: () => ({}), + isTrustedFolder: () => true, + isYoloModeDisabled: () => true, + isPlanEnabled: () => false, + isEventDrivenSchedulerEnabled: () => false, } as unknown as Config; vi.mocked(loadCliConfig).mockResolvedValue(mockConfig); - vi.mocked(loadSandboxConfig).mockResolvedValue({} as any); // eslint-disable-line @typescript-eslint/no-explicit-any + vi.mocked(loadSandboxConfig).mockResolvedValue({ + command: 'docker', + } as any); // eslint-disable-line @typescript-eslint/no-explicit-any vi.mocked(relaunchOnExitCode).mockImplementation(async (fn) => { await fn(); }); diff --git a/packages/cli/src/ui/commands/directoryCommand.test.tsx b/packages/cli/src/ui/commands/directoryCommand.test.tsx index 45ddd6fbe2..904e8498f3 100644 --- a/packages/cli/src/ui/commands/directoryCommand.test.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.test.tsx @@ -43,6 +43,7 @@ describe('directoryCommand', () => { beforeEach(() => { mockWorkspaceContext = { addDirectory: vi.fn(), + addDirectories: vi.fn().mockReturnValue({ added: [], failed: [] }), getDirectories: vi .fn() .mockReturnValue([ @@ -125,9 +126,15 @@ describe('directoryCommand', () => { it('should call addDirectory and show a success message for a single path', async () => { const newPath = path.normalize('/home/user/new-project'); + vi.mocked(mockWorkspaceContext.addDirectories).mockReturnValue({ + added: [newPath], + failed: [], + }); if (!addCommand?.action) throw new Error('No action'); await addCommand.action(mockContext, newPath); - expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath); + expect(mockWorkspaceContext.addDirectories).toHaveBeenCalledWith([ + newPath, + ]); expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: MessageType.INFO, @@ -139,10 +146,16 @@ describe('directoryCommand', () => { it('should call addDirectory for each path and show a success message for multiple paths', async () => { const newPath1 = path.normalize('/home/user/new-project1'); const newPath2 = path.normalize('/home/user/new-project2'); + vi.mocked(mockWorkspaceContext.addDirectories).mockReturnValue({ + added: [newPath1, newPath2], + failed: [], + }); if (!addCommand?.action) throw new Error('No action'); await addCommand.action(mockContext, `${newPath1},${newPath2}`); - expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath1); - expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath2); + expect(mockWorkspaceContext.addDirectories).toHaveBeenCalledWith([ + newPath1, + newPath2, + ]); expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: MessageType.INFO, @@ -153,10 +166,11 @@ describe('directoryCommand', () => { it('should show an error if addDirectory throws an exception', async () => { const error = new Error('Directory does not exist'); - vi.mocked(mockWorkspaceContext.addDirectory).mockImplementation(() => { - throw error; - }); const newPath = path.normalize('/home/user/invalid-project'); + vi.mocked(mockWorkspaceContext.addDirectories).mockReturnValue({ + added: [], + failed: [{ path: newPath, error }], + }); if (!addCommand?.action) throw new Error('No action'); await addCommand.action(mockContext, newPath); expect(mockContext.ui.addItem).toHaveBeenCalledWith( @@ -171,10 +185,16 @@ describe('directoryCommand', () => { if (!addCommand?.action) throw new Error('No action'); vi.spyOn(trustedFolders, 'isFolderTrustEnabled').mockReturnValue(false); const newPath = path.normalize('/home/user/new-project'); + vi.mocked(mockWorkspaceContext.addDirectories).mockReturnValue({ + added: [newPath], + failed: [], + }); await addCommand.action(mockContext, newPath); - expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath); + expect(mockWorkspaceContext.addDirectories).toHaveBeenCalledWith([ + newPath, + ]); }); it('should show an info message for an already added directory', async () => { @@ -196,13 +216,10 @@ describe('directoryCommand', () => { const validPath = path.normalize('/home/user/valid-project'); const invalidPath = path.normalize('/home/user/invalid-project'); const error = new Error('Directory does not exist'); - vi.mocked(mockWorkspaceContext.addDirectory).mockImplementation( - (p: string) => { - if (p === invalidPath) { - throw error; - } - }, - ); + vi.mocked(mockWorkspaceContext.addDirectories).mockReturnValue({ + added: [validPath], + failed: [{ path: invalidPath, error }], + }); if (!addCommand?.action) throw new Error('No action'); await addCommand.action(mockContext, `${validPath},${invalidPath}`); @@ -290,10 +307,16 @@ describe('directoryCommand', () => { if (!addCommand?.action) throw new Error('No action'); mockIsPathTrusted.mockReturnValue(true); const newPath = path.normalize('/home/user/trusted-project'); + vi.mocked(mockWorkspaceContext.addDirectories).mockReturnValue({ + added: [newPath], + failed: [], + }); await addCommand.action(mockContext, newPath); - expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath); + expect(mockWorkspaceContext.addDirectories).toHaveBeenCalledWith([ + newPath, + ]); }); it('should show an error for an untrusted directory', async () => { @@ -303,7 +326,7 @@ describe('directoryCommand', () => { await addCommand.action(mockContext, newPath); - expect(mockWorkspaceContext.addDirectory).not.toHaveBeenCalled(); + expect(mockWorkspaceContext.addDirectories).not.toHaveBeenCalled(); expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: MessageType.ERROR, diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index c3c56d46f2..9116e216b9 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -17,6 +17,7 @@ import { refreshServerHierarchicalMemory } from '@google/gemini-cli-core'; import { expandHomeDir, getDirectorySuggestions, + batchAddDirectories, } from '../utils/directoryUtils.js'; import type { Config } from '@google/gemini-cli-core'; @@ -193,14 +194,10 @@ export const directoryCommand: SlashCommand = { ); } - for (const pathToAdd of trustedDirs) { - try { - workspaceContext.addDirectory(expandHomeDir(pathToAdd)); - added.push(pathToAdd); - } catch (e) { - const error = e as Error; - errors.push(`Error adding '${pathToAdd}': ${error.message}`); - } + if (trustedDirs.length > 0) { + const result = batchAddDirectories(workspaceContext, trustedDirs); + added.push(...result.added); + errors.push(...result.errors); } if (undefinedTrustDirs.length > 0) { @@ -220,17 +217,9 @@ export const directoryCommand: SlashCommand = { }; } } else { - for (const pathToAdd of pathsToProcess) { - try { - workspaceContext.addDirectory(expandHomeDir(pathToAdd.trim())); - added.push(pathToAdd.trim()); - } catch (e) { - const error = e as Error; - errors.push( - `Error adding '${pathToAdd.trim()}': ${error.message}`, - ); - } - } + const result = batchAddDirectories(workspaceContext, pathsToProcess); + added.push(...result.added); + errors.push(...result.errors); } await finishAddingDirectories(config, addItem, added, errors); diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index a1d45db5cd..ed8ab8307f 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -203,6 +203,15 @@ describe('