diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/ui/commands/restoreCommand.test.ts | 209 |
1 files changed, 111 insertions, 98 deletions
diff --git a/packages/cli/src/ui/commands/restoreCommand.test.ts b/packages/cli/src/ui/commands/restoreCommand.test.ts index 53cd7d18..23f71499 100644 --- a/packages/cli/src/ui/commands/restoreCommand.test.ts +++ b/packages/cli/src/ui/commands/restoreCommand.test.ts @@ -4,36 +4,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - vi, - describe, - it, - expect, - beforeEach, - afterEach, - Mocked, - Mock, -} from 'vitest'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; import { restoreCommand } from './restoreCommand.js'; import { type CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { Config, GitService } from '@google/gemini-cli-core'; -vi.mock('fs/promises', () => ({ - readdir: vi.fn(), - readFile: vi.fn(), - mkdir: vi.fn(), -})); - describe('restoreCommand', () => { let mockContext: CommandContext; let mockConfig: Config; let mockGitService: GitService; - const mockFsPromises = fs as Mocked<typeof fs>; let mockSetHistory: ReturnType<typeof vi.fn>; + let testRootDir: string; + let geminiTempDir: string; + let checkpointsDir: string; + + beforeEach(async () => { + testRootDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'restore-command-test-'), + ); + geminiTempDir = path.join(testRootDir, '.gemini'); + checkpointsDir = path.join(geminiTempDir, 'checkpoints'); + // The command itself creates this, but for tests it's easier to have it ready. + // Some tests might remove it to test error paths. + await fs.mkdir(checkpointsDir, { recursive: true }); - beforeEach(() => { mockSetHistory = vi.fn().mockResolvedValue(undefined); mockGitService = { restoreProjectFromSnapshot: vi.fn().mockResolvedValue(undefined), @@ -41,7 +39,7 @@ describe('restoreCommand', () => { mockConfig = { getCheckpointingEnabled: vi.fn().mockReturnValue(true), - getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini'), + getProjectTempDir: vi.fn().mockReturnValue(geminiTempDir), getGeminiClient: vi.fn().mockReturnValue({ setHistory: mockSetHistory, }), @@ -55,31 +53,35 @@ describe('restoreCommand', () => { }); }); - afterEach(() => { + afterEach(async () => { vi.restoreAllMocks(); + await fs.rm(testRootDir, { recursive: true, force: true }); }); it('should return null if checkpointing is not enabled', () => { - (mockConfig.getCheckpointingEnabled as Mock).mockReturnValue(false); - const command = restoreCommand(mockConfig); - expect(command).toBeNull(); + vi.mocked(mockConfig.getCheckpointingEnabled).mockReturnValue(false); + + expect(restoreCommand(mockConfig)).toBeNull(); }); it('should return the command if checkpointing is enabled', () => { - const command = restoreCommand(mockConfig); - expect(command).not.toBeNull(); - expect(command?.name).toBe('restore'); - expect(command?.description).toBeDefined(); - expect(command?.action).toBeDefined(); - expect(command?.completion).toBeDefined(); + expect(restoreCommand(mockConfig)).toEqual( + expect.objectContaining({ + name: 'restore', + description: expect.any(String), + action: expect.any(Function), + completion: expect.any(Function), + }), + ); }); describe('action', () => { it('should return an error if temp dir is not found', async () => { - (mockConfig.getProjectTempDir as Mock).mockReturnValue(undefined); - const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, ''); - expect(result).toEqual({ + vi.mocked(mockConfig.getProjectTempDir).mockReturnValue(''); + + expect( + await restoreCommand(mockConfig)?.action?.(mockContext, ''), + ).toEqual({ type: 'message', messageType: 'error', content: 'Could not determine the .gemini directory path.', @@ -87,31 +89,25 @@ describe('restoreCommand', () => { }); it('should inform when no checkpoints are found if no args are passed', async () => { - mockFsPromises.readdir.mockResolvedValue([]); + // Remove the directory to ensure the command creates it. + await fs.rm(checkpointsDir, { recursive: true, force: true }); const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, ''); - expect(result).toEqual({ + + expect(await command?.action?.(mockContext, '')).toEqual({ type: 'message', messageType: 'info', content: 'No restorable tool calls found.', }); - expect(mockFsPromises.mkdir).toHaveBeenCalledWith( - '/tmp/gemini/checkpoints', - { - recursive: true, - }, - ); + // Verify the directory was created by the command. + await expect(fs.stat(checkpointsDir)).resolves.toBeDefined(); }); it('should list available checkpoints if no args are passed', async () => { - mockFsPromises.readdir.mockResolvedValue([ - 'test1.json', - 'test2.json', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ] as any); + await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}'); + await fs.writeFile(path.join(checkpointsDir, 'test2.json'), '{}'); const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, ''); - expect(result).toEqual({ + + expect(await command?.action?.(mockContext, '')).toEqual({ type: 'message', messageType: 'info', content: 'Available tool calls to restore:\n\ntest1\ntest2', @@ -119,11 +115,10 @@ describe('restoreCommand', () => { }); it('should return an error if the specified file is not found', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockFsPromises.readdir.mockResolvedValue(['test1.json'] as any); + await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}'); const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, 'test2'); - expect(result).toEqual({ + + expect(await command?.action?.(mockContext, 'test2')).toEqual({ type: 'message', messageType: 'error', content: 'File not found: test2.json', @@ -131,16 +126,21 @@ describe('restoreCommand', () => { }); it('should handle file read errors gracefully', async () => { - const readError = new Error('Read failed'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockFsPromises.readdir.mockResolvedValue(['test1.json'] as any); - mockFsPromises.readFile.mockRejectedValue(readError); + const checkpointName = 'test1'; + const checkpointPath = path.join( + checkpointsDir, + `${checkpointName}.json`, + ); + // Create a directory instead of a file to cause a read error. + await fs.mkdir(checkpointPath); const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, 'test1'); - expect(result).toEqual({ + + expect(await command?.action?.(mockContext, checkpointName)).toEqual({ type: 'message', messageType: 'error', - content: `Could not read restorable tool calls. This is the error: ${readError}`, + content: expect.stringContaining( + 'Could not read restorable tool calls.', + ), }); }); @@ -151,20 +151,21 @@ describe('restoreCommand', () => { commitHash: 'abcdef123', toolCall: { name: 'run_shell_command', args: 'ls' }, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockFsPromises.readdir.mockResolvedValue(['my-checkpoint.json'] as any); - mockFsPromises.readFile.mockResolvedValue(JSON.stringify(toolCallData)); - + await fs.writeFile( + path.join(checkpointsDir, 'my-checkpoint.json'), + JSON.stringify(toolCallData), + ); const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, 'my-checkpoint'); - // Check history restoration + expect(await command?.action?.(mockContext, 'my-checkpoint')).toEqual({ + type: 'tool', + toolName: 'run_shell_command', + toolArgs: 'ls', + }); expect(mockContext.ui.loadHistory).toHaveBeenCalledWith( toolCallData.history, ); expect(mockSetHistory).toHaveBeenCalledWith(toolCallData.clientHistory); - - // Check git restoration expect(mockGitService.restoreProjectFromSnapshot).toHaveBeenCalledWith( toolCallData.commitHash, ); @@ -175,63 +176,75 @@ describe('restoreCommand', () => { }, expect.any(Number), ); - - // Check returned action - expect(result).toEqual({ - type: 'tool', - toolName: 'run_shell_command', - toolArgs: 'ls', - }); }); it('should restore even if only toolCall is present', async () => { const toolCallData = { toolCall: { name: 'run_shell_command', args: 'ls' }, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockFsPromises.readdir.mockResolvedValue(['my-checkpoint.json'] as any); - mockFsPromises.readFile.mockResolvedValue(JSON.stringify(toolCallData)); + await fs.writeFile( + path.join(checkpointsDir, 'my-checkpoint.json'), + JSON.stringify(toolCallData), + ); const command = restoreCommand(mockConfig); - const result = await command?.action?.(mockContext, 'my-checkpoint'); - expect(mockContext.ui.loadHistory).not.toHaveBeenCalled(); - expect(mockSetHistory).not.toHaveBeenCalled(); - expect(mockGitService.restoreProjectFromSnapshot).not.toHaveBeenCalled(); - - expect(result).toEqual({ + expect(await command?.action?.(mockContext, 'my-checkpoint')).toEqual({ type: 'tool', toolName: 'run_shell_command', toolArgs: 'ls', }); + + expect(mockContext.ui.loadHistory).not.toHaveBeenCalled(); + expect(mockSetHistory).not.toHaveBeenCalled(); + expect(mockGitService.restoreProjectFromSnapshot).not.toHaveBeenCalled(); + }); + }); + + it('should return an error for a checkpoint file missing the toolCall property', async () => { + const checkpointName = 'missing-toolcall'; + await fs.writeFile( + path.join(checkpointsDir, `${checkpointName}.json`), + JSON.stringify({ history: [] }), // An object that is valid JSON but missing the 'toolCall' property + ); + const command = restoreCommand(mockConfig); + + expect(await command?.action?.(mockContext, checkpointName)).toEqual({ + type: 'message', + messageType: 'error', + // A more specific error message would be ideal, but for now, we can assert the current behavior. + content: expect.stringContaining('Could not read restorable tool calls.'), }); }); describe('completion', () => { it('should return an empty array if temp dir is not found', async () => { - (mockConfig.getProjectTempDir as Mock).mockReturnValue(undefined); + vi.mocked(mockConfig.getProjectTempDir).mockReturnValue(''); const command = restoreCommand(mockConfig); - const result = await command?.completion?.(mockContext, ''); - expect(result).toEqual([]); + + expect(await command?.completion?.(mockContext, '')).toEqual([]); }); it('should return an empty array on readdir error', async () => { - mockFsPromises.readdir.mockRejectedValue(new Error('ENOENT')); + await fs.rm(checkpointsDir, { recursive: true, force: true }); const command = restoreCommand(mockConfig); - const result = await command?.completion?.(mockContext, ''); - expect(result).toEqual([]); + + expect(await command?.completion?.(mockContext, '')).toEqual([]); }); it('should return a list of checkpoint names', async () => { - mockFsPromises.readdir.mockResolvedValue([ - 'test1.json', - 'test2.json', - 'not-a-checkpoint.txt', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ] as any); + await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}'); + await fs.writeFile(path.join(checkpointsDir, 'test2.json'), '{}'); + await fs.writeFile( + path.join(checkpointsDir, 'not-a-checkpoint.txt'), + '{}', + ); const command = restoreCommand(mockConfig); - const result = await command?.completion?.(mockContext, ''); - expect(result).toEqual(['test1', 'test2']); + + expect(await command?.completion?.(mockContext, '')).toEqual([ + 'test1', + 'test2', + ]); }); }); }); |
