diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index c82e9e6992..6fefd84919 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -575,7 +575,10 @@ export class Task { EDIT_TOOL_NAMES.has(request.name), ); - if (restorableToolCalls.length > 0) { + if ( + restorableToolCalls.length > 0 && + this.config.getCheckpointingEnabled() + ) { const gitService = await this.config.getGitService(); if (gitService) { const { checkpointsToWrite, toolCallToCheckpointMap, errors } = diff --git a/packages/a2a-server/src/config/config.test.ts b/packages/a2a-server/src/config/config.test.ts new file mode 100644 index 0000000000..6ca5b049e5 --- /dev/null +++ b/packages/a2a-server/src/config/config.test.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { loadConfig } from './config.js'; +import type { ExtensionLoader } from '@google/gemini-cli-core'; +import type { Settings } from './settings.js'; + +const { + mockLoadServerHierarchicalMemory, + mockConfigConstructor, + mockVerifyGitAvailability, +} = vi.hoisted(() => ({ + mockLoadServerHierarchicalMemory: vi.fn().mockResolvedValue({ + memoryContent: '', + fileCount: 0, + filePaths: [], + }), + mockConfigConstructor: vi.fn(), + mockVerifyGitAvailability: vi.fn(), +})); + +vi.mock('@google/gemini-cli-core', async () => ({ + Config: class MockConfig { + constructor(params: unknown) { + mockConfigConstructor(params); + } + initialize = vi.fn(); + refreshAuth = vi.fn(); + }, + loadServerHierarchicalMemory: mockLoadServerHierarchicalMemory, + startupProfiler: { + flush: vi.fn(), + }, + FileDiscoveryService: vi.fn(), + ApprovalMode: { DEFAULT: 'default', YOLO: 'yolo' }, + AuthType: { + LOGIN_WITH_GOOGLE: 'login_with_google', + USE_GEMINI: 'use_gemini', + }, + GEMINI_DIR: '.gemini', + DEFAULT_GEMINI_EMBEDDING_MODEL: 'models/embedding-001', + DEFAULT_GEMINI_MODEL: 'models/gemini-1.5-flash', + PREVIEW_GEMINI_MODEL: 'models/gemini-1.5-pro-latest', + homedir: () => '/tmp', + GitService: { + verifyGitAvailability: mockVerifyGitAvailability, + }, +})); + +describe('loadConfig', () => { + const mockSettings = { + checkpointing: { enabled: true }, + }; + const mockExtensionLoader = { + start: vi.fn(), + getExtensions: vi.fn().mockReturnValue([]), + } as unknown as ExtensionLoader; + + beforeEach(() => { + vi.resetAllMocks(); + process.env['GEMINI_API_KEY'] = 'test-key'; + // Reset the mock return value just in case + mockLoadServerHierarchicalMemory.mockResolvedValue({ + memoryContent: '', + fileCount: 0, + filePaths: [], + }); + }); + + afterEach(() => { + delete process.env['GEMINI_API_KEY']; + delete process.env['CHECKPOINTING']; + }); + + it('should disable checkpointing if git is not installed', async () => { + mockVerifyGitAvailability.mockResolvedValue(false); + + await loadConfig( + mockSettings as unknown as Settings, + mockExtensionLoader, + 'test-task', + ); + + expect(mockConfigConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + checkpointing: false, + }), + ); + }); + + it('should enable checkpointing if git is installed', async () => { + mockVerifyGitAvailability.mockResolvedValue(true); + + await loadConfig( + mockSettings as unknown as Settings, + mockExtensionLoader, + 'test-task', + ); + + expect(mockConfigConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + checkpointing: true, + }), + ); + }); +}); diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index 13d0d56995..b9e895dde0 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -23,6 +23,7 @@ import { startupProfiler, PREVIEW_GEMINI_MODEL, homedir, + GitService, } from '@google/gemini-cli-core'; import { logger } from '../utils/logger.js'; @@ -41,6 +42,19 @@ export async function loadConfig( settings.folderTrust === true || process.env['GEMINI_FOLDER_TRUST'] === 'true'; + let checkpointing = process.env['CHECKPOINTING'] + ? process.env['CHECKPOINTING'] === 'true' + : settings.checkpointing?.enabled; + + if (checkpointing) { + if (!(await GitService.verifyGitAvailability())) { + logger.warn( + '[Config] Checkpointing is enabled but git is not installed. Disabling checkpointing.', + ); + checkpointing = false; + } + } + const configParams: ConfigParameters = { sessionId: taskId, model: settings.general?.previewFeatures @@ -79,9 +93,7 @@ export async function loadConfig( folderTrust, trustedFolder: true, extensionLoader, - checkpointing: process.env['CHECKPOINTING'] - ? process.env['CHECKPOINTING'] === 'true' - : settings.checkpointing?.enabled, + checkpointing, previewFeatures: settings.general?.previewFeatures, interactive: true, enableInteractiveShell: true, diff --git a/packages/core/src/services/gitService.test.ts b/packages/core/src/services/gitService.test.ts index b3e3975265..2990b5553d 100644 --- a/packages/core/src/services/gitService.test.ts +++ b/packages/core/src/services/gitService.test.ts @@ -147,15 +147,13 @@ describe('GitService', () => { describe('verifyGitAvailability', () => { it('should resolve true if git --version command succeeds', async () => { - const service = new GitService(projectRoot, storage); - await expect(service.verifyGitAvailability()).resolves.toBe(true); + await expect(GitService.verifyGitAvailability()).resolves.toBe(true); expect(spawnAsync).toHaveBeenCalledWith('git', ['--version']); }); it('should resolve false if git --version command fails', async () => { (spawnAsync as Mock).mockRejectedValue(new Error('git not found')); - const service = new GitService(projectRoot, storage); - await expect(service.verifyGitAvailability()).resolves.toBe(false); + await expect(GitService.verifyGitAvailability()).resolves.toBe(false); }); }); diff --git a/packages/core/src/services/gitService.ts b/packages/core/src/services/gitService.ts index c0c36b2b88..a5b36969c3 100644 --- a/packages/core/src/services/gitService.ts +++ b/packages/core/src/services/gitService.ts @@ -27,7 +27,7 @@ export class GitService { } async initialize(): Promise { - const gitAvailable = await this.verifyGitAvailability(); + const gitAvailable = await GitService.verifyGitAvailability(); if (!gitAvailable) { throw new Error( 'Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.', @@ -42,7 +42,7 @@ export class GitService { } } - async verifyGitAvailability(): Promise { + static async verifyGitAvailability(): Promise { try { await spawnAsync('git', ['--version']); return true;