mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Make restoreCommand test windows compatible. (#4873)
This commit is contained in:
committed by
GitHub
parent
820105e982
commit
91f016d44a
@@ -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',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user