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