Make restoreCommand test windows compatible. (#4873)

This commit is contained in:
Tommaso Sciortino
2025-07-25 12:26:09 -07:00
committed by GitHub
parent 820105e982
commit 91f016d44a

View File

@@ -4,36 +4,34 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
vi,
describe,
it,
expect,
beforeEach,
afterEach,
Mocked,
Mock,
} from 'vitest';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { restoreCommand } from './restoreCommand.js'; import { restoreCommand } from './restoreCommand.js';
import { type CommandContext } from './types.js'; import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { Config, GitService } from '@google/gemini-cli-core'; import { Config, GitService } from '@google/gemini-cli-core';
vi.mock('fs/promises', () => ({
readdir: vi.fn(),
readFile: vi.fn(),
mkdir: vi.fn(),
}));
describe('restoreCommand', () => { describe('restoreCommand', () => {
let mockContext: CommandContext; let mockContext: CommandContext;
let mockConfig: Config; let mockConfig: Config;
let mockGitService: GitService; let mockGitService: GitService;
const mockFsPromises = fs as Mocked<typeof fs>;
let mockSetHistory: ReturnType<typeof vi.fn>; 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); mockSetHistory = vi.fn().mockResolvedValue(undefined);
mockGitService = { mockGitService = {
restoreProjectFromSnapshot: vi.fn().mockResolvedValue(undefined), restoreProjectFromSnapshot: vi.fn().mockResolvedValue(undefined),
@@ -41,7 +39,7 @@ describe('restoreCommand', () => {
mockConfig = { mockConfig = {
getCheckpointingEnabled: vi.fn().mockReturnValue(true), getCheckpointingEnabled: vi.fn().mockReturnValue(true),
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini'), getProjectTempDir: vi.fn().mockReturnValue(geminiTempDir),
getGeminiClient: vi.fn().mockReturnValue({ getGeminiClient: vi.fn().mockReturnValue({
setHistory: mockSetHistory, setHistory: mockSetHistory,
}), }),
@@ -55,31 +53,35 @@ describe('restoreCommand', () => {
}); });
}); });
afterEach(() => { afterEach(async () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
await fs.rm(testRootDir, { recursive: true, force: true });
}); });
it('should return null if checkpointing is not enabled', () => { it('should return null if checkpointing is not enabled', () => {
(mockConfig.getCheckpointingEnabled as Mock).mockReturnValue(false); vi.mocked(mockConfig.getCheckpointingEnabled).mockReturnValue(false);
const command = restoreCommand(mockConfig);
expect(command).toBeNull(); expect(restoreCommand(mockConfig)).toBeNull();
}); });
it('should return the command if checkpointing is enabled', () => { it('should return the command if checkpointing is enabled', () => {
const command = restoreCommand(mockConfig); expect(restoreCommand(mockConfig)).toEqual(
expect(command).not.toBeNull(); expect.objectContaining({
expect(command?.name).toBe('restore'); name: 'restore',
expect(command?.description).toBeDefined(); description: expect.any(String),
expect(command?.action).toBeDefined(); action: expect.any(Function),
expect(command?.completion).toBeDefined(); completion: expect.any(Function),
}),
);
}); });
describe('action', () => { describe('action', () => {
it('should return an error if temp dir is not found', async () => { it('should return an error 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?.action?.(mockContext, ''); expect(
expect(result).toEqual({ await restoreCommand(mockConfig)?.action?.(mockContext, ''),
).toEqual({
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Could not determine the .gemini directory path.', 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 () => { 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 command = restoreCommand(mockConfig);
const result = await command?.action?.(mockContext, '');
expect(result).toEqual({ expect(await command?.action?.(mockContext, '')).toEqual({
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'No restorable tool calls found.', content: 'No restorable tool calls found.',
}); });
expect(mockFsPromises.mkdir).toHaveBeenCalledWith( // Verify the directory was created by the command.
'/tmp/gemini/checkpoints', await expect(fs.stat(checkpointsDir)).resolves.toBeDefined();
{
recursive: true,
},
);
}); });
it('should list available checkpoints if no args are passed', async () => { it('should list available checkpoints if no args are passed', async () => {
mockFsPromises.readdir.mockResolvedValue([ await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}');
'test1.json', await fs.writeFile(path.join(checkpointsDir, 'test2.json'), '{}');
'test2.json',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);
const command = restoreCommand(mockConfig); const command = restoreCommand(mockConfig);
const result = await command?.action?.(mockContext, '');
expect(result).toEqual({ expect(await command?.action?.(mockContext, '')).toEqual({
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'Available tool calls to restore:\n\ntest1\ntest2', 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 () => { it('should return an error if the specified file is not found', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}');
mockFsPromises.readdir.mockResolvedValue(['test1.json'] as any);
const command = restoreCommand(mockConfig); const command = restoreCommand(mockConfig);
const result = await command?.action?.(mockContext, 'test2');
expect(result).toEqual({ expect(await command?.action?.(mockContext, 'test2')).toEqual({
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'File not found: test2.json', content: 'File not found: test2.json',
@@ -131,16 +126,21 @@ describe('restoreCommand', () => {
}); });
it('should handle file read errors gracefully', async () => { it('should handle file read errors gracefully', async () => {
const readError = new Error('Read failed'); const checkpointName = 'test1';
// eslint-disable-next-line @typescript-eslint/no-explicit-any const checkpointPath = path.join(
mockFsPromises.readdir.mockResolvedValue(['test1.json'] as any); checkpointsDir,
mockFsPromises.readFile.mockRejectedValue(readError); `${checkpointName}.json`,
);
// Create a directory instead of a file to cause a read error.
await fs.mkdir(checkpointPath);
const command = restoreCommand(mockConfig); const command = restoreCommand(mockConfig);
const result = await command?.action?.(mockContext, 'test1');
expect(result).toEqual({ expect(await command?.action?.(mockContext, checkpointName)).toEqual({
type: 'message', type: 'message',
messageType: 'error', 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', commitHash: 'abcdef123',
toolCall: { name: 'run_shell_command', args: 'ls' }, toolCall: { name: 'run_shell_command', args: 'ls' },
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any await fs.writeFile(
mockFsPromises.readdir.mockResolvedValue(['my-checkpoint.json'] as any); path.join(checkpointsDir, 'my-checkpoint.json'),
mockFsPromises.readFile.mockResolvedValue(JSON.stringify(toolCallData)); JSON.stringify(toolCallData),
);
const command = restoreCommand(mockConfig); 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( expect(mockContext.ui.loadHistory).toHaveBeenCalledWith(
toolCallData.history, toolCallData.history,
); );
expect(mockSetHistory).toHaveBeenCalledWith(toolCallData.clientHistory); expect(mockSetHistory).toHaveBeenCalledWith(toolCallData.clientHistory);
// Check git restoration
expect(mockGitService.restoreProjectFromSnapshot).toHaveBeenCalledWith( expect(mockGitService.restoreProjectFromSnapshot).toHaveBeenCalledWith(
toolCallData.commitHash, toolCallData.commitHash,
); );
@@ -175,63 +176,75 @@ describe('restoreCommand', () => {
}, },
expect.any(Number), 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 () => { it('should restore even if only toolCall is present', async () => {
const toolCallData = { const toolCallData = {
toolCall: { name: 'run_shell_command', args: 'ls' }, toolCall: { name: 'run_shell_command', args: 'ls' },
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any await fs.writeFile(
mockFsPromises.readdir.mockResolvedValue(['my-checkpoint.json'] as any); path.join(checkpointsDir, 'my-checkpoint.json'),
mockFsPromises.readFile.mockResolvedValue(JSON.stringify(toolCallData)); JSON.stringify(toolCallData),
);
const command = restoreCommand(mockConfig); const command = restoreCommand(mockConfig);
const result = await command?.action?.(mockContext, 'my-checkpoint');
expect(mockContext.ui.loadHistory).not.toHaveBeenCalled(); expect(await command?.action?.(mockContext, 'my-checkpoint')).toEqual({
expect(mockSetHistory).not.toHaveBeenCalled();
expect(mockGitService.restoreProjectFromSnapshot).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'tool', type: 'tool',
toolName: 'run_shell_command', toolName: 'run_shell_command',
toolArgs: 'ls', 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', () => { describe('completion', () => {
it('should return an empty array if temp dir is not found', async () => { 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 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 () => { 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 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 () => { it('should return a list of checkpoint names', async () => {
mockFsPromises.readdir.mockResolvedValue([ await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}');
'test1.json', await fs.writeFile(path.join(checkpointsDir, 'test2.json'), '{}');
'test2.json', await fs.writeFile(
'not-a-checkpoint.txt', path.join(checkpointsDir, 'not-a-checkpoint.txt'),
// eslint-disable-next-line @typescript-eslint/no-explicit-any '{}',
] as any); );
const command = restoreCommand(mockConfig); const command = restoreCommand(mockConfig);
const result = await command?.completion?.(mockContext, '');
expect(result).toEqual(['test1', 'test2']); expect(await command?.completion?.(mockContext, '')).toEqual([
'test1',
'test2',
]);
}); });
}); });
}); });