257 lines
8.4 KiB
TypeScript
257 lines
8.4 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2025 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
|
|
import { ideCommand } from './ideCommand.js';
|
||
|
|
import { type CommandContext } from './types.js';
|
||
|
|
import { type Config } from '@google/gemini-cli-core';
|
||
|
|
import * as child_process from 'child_process';
|
||
|
|
import { glob } from 'glob';
|
||
|
|
|
||
|
|
import {
|
||
|
|
getMCPDiscoveryState,
|
||
|
|
getMCPServerStatus,
|
||
|
|
IDE_SERVER_NAME,
|
||
|
|
MCPDiscoveryState,
|
||
|
|
MCPServerStatus,
|
||
|
|
} from '@google/gemini-cli-core';
|
||
|
|
|
||
|
|
vi.mock('child_process');
|
||
|
|
vi.mock('glob');
|
||
|
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||
|
|
const original =
|
||
|
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||
|
|
return {
|
||
|
|
...original,
|
||
|
|
getMCPServerStatus: vi.fn(),
|
||
|
|
getMCPDiscoveryState: vi.fn(),
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ideCommand', () => {
|
||
|
|
let mockContext: CommandContext;
|
||
|
|
let mockConfig: Config;
|
||
|
|
let execSyncSpy: vi.SpyInstance;
|
||
|
|
let globSyncSpy: vi.SpyInstance;
|
||
|
|
let platformSpy: vi.SpyInstance;
|
||
|
|
let getMCPServerStatusSpy: vi.SpyInstance;
|
||
|
|
let getMCPDiscoveryStateSpy: vi.SpyInstance;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
mockContext = {
|
||
|
|
ui: {
|
||
|
|
addItem: vi.fn(),
|
||
|
|
},
|
||
|
|
} as unknown as CommandContext;
|
||
|
|
|
||
|
|
mockConfig = {
|
||
|
|
getIdeMode: vi.fn(),
|
||
|
|
} as unknown as Config;
|
||
|
|
|
||
|
|
execSyncSpy = vi.spyOn(child_process, 'execSync');
|
||
|
|
globSyncSpy = vi.spyOn(glob, 'sync');
|
||
|
|
platformSpy = vi.spyOn(process, 'platform', 'get');
|
||
|
|
getMCPServerStatusSpy = vi.mocked(getMCPServerStatus);
|
||
|
|
getMCPDiscoveryStateSpy = vi.mocked(getMCPDiscoveryState);
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
vi.restoreAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return null if ideMode is not enabled', () => {
|
||
|
|
(mockConfig.getIdeMode as vi.Mock).mockReturnValue(false);
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
expect(command).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return the ide command if ideMode is enabled', () => {
|
||
|
|
(mockConfig.getIdeMode as vi.Mock).mockReturnValue(true);
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
expect(command).not.toBeNull();
|
||
|
|
expect(command?.name).toBe('ide');
|
||
|
|
expect(command?.subCommands).toHaveLength(2);
|
||
|
|
expect(command?.subCommands?.[0].name).toBe('status');
|
||
|
|
expect(command?.subCommands?.[1].name).toBe('install');
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('status subcommand', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
(mockConfig.getIdeMode as vi.Mock).mockReturnValue(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show connected status', () => {
|
||
|
|
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.CONNECTED);
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
const result = command?.subCommands?.[0].action(mockContext, '');
|
||
|
|
expect(getMCPServerStatusSpy).toHaveBeenCalledWith(IDE_SERVER_NAME);
|
||
|
|
expect(result).toEqual({
|
||
|
|
type: 'message',
|
||
|
|
messageType: 'info',
|
||
|
|
content: '🟢 Connected',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show connecting status', () => {
|
||
|
|
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.CONNECTING);
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
const result = command?.subCommands?.[0].action(mockContext, '');
|
||
|
|
expect(result).toEqual({
|
||
|
|
type: 'message',
|
||
|
|
messageType: 'info',
|
||
|
|
content: '🔄 Initializing...',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show discovery in progress status', () => {
|
||
|
|
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.DISCONNECTED);
|
||
|
|
getMCPDiscoveryStateSpy.mockReturnValue(MCPDiscoveryState.IN_PROGRESS);
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
const result = command?.subCommands?.[0].action(mockContext, '');
|
||
|
|
expect(result).toEqual({
|
||
|
|
type: 'message',
|
||
|
|
messageType: 'info',
|
||
|
|
content: '🔄 Initializing...',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show disconnected status', () => {
|
||
|
|
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.DISCONNECTED);
|
||
|
|
getMCPDiscoveryStateSpy.mockReturnValue(MCPDiscoveryState.NOT_FOUND);
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
const result = command?.subCommands?.[0].action(mockContext, '');
|
||
|
|
expect(result).toEqual({
|
||
|
|
type: 'message',
|
||
|
|
messageType: 'error',
|
||
|
|
content: '🔴 Disconnected',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('install subcommand', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
(mockConfig.getIdeMode as vi.Mock).mockReturnValue(true);
|
||
|
|
platformSpy.mockReturnValue('linux');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show an error if VSCode is not installed', async () => {
|
||
|
|
execSyncSpy.mockImplementation(() => {
|
||
|
|
throw new Error('Command not found');
|
||
|
|
});
|
||
|
|
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
await command?.subCommands?.[1].action(mockContext, '');
|
||
|
|
|
||
|
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
type: 'error',
|
||
|
|
text: expect.stringContaining(
|
||
|
|
'VS Code command-line tool "code" not found',
|
||
|
|
),
|
||
|
|
}),
|
||
|
|
expect.any(Number),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show an error if the VSIX file is not found', async () => {
|
||
|
|
execSyncSpy.mockReturnValue(''); // VSCode is installed
|
||
|
|
globSyncSpy.mockReturnValue([]); // No .vsix file found
|
||
|
|
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
await command?.subCommands?.[1].action(mockContext, '');
|
||
|
|
|
||
|
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
type: 'error',
|
||
|
|
text: 'Could not find the required VS Code companion extension. Please file a bug via /bug.',
|
||
|
|
}),
|
||
|
|
expect.any(Number),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should install the extension if found in the bundle directory', async () => {
|
||
|
|
const vsixPath = '/path/to/bundle/gemini.vsix';
|
||
|
|
execSyncSpy.mockReturnValue(''); // VSCode is installed
|
||
|
|
globSyncSpy.mockReturnValue([vsixPath]); // Found .vsix file
|
||
|
|
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
await command?.subCommands?.[1].action(mockContext, '');
|
||
|
|
|
||
|
|
expect(globSyncSpy).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('.vsix'),
|
||
|
|
);
|
||
|
|
expect(execSyncSpy).toHaveBeenCalledWith(
|
||
|
|
`code --install-extension ${vsixPath} --force`,
|
||
|
|
{ stdio: 'pipe' },
|
||
|
|
);
|
||
|
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
type: 'info',
|
||
|
|
text: `Installing VS Code companion extension...`,
|
||
|
|
}),
|
||
|
|
expect.any(Number),
|
||
|
|
);
|
||
|
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
type: 'info',
|
||
|
|
text: 'VS Code companion extension installed successfully. Restart gemini-cli in a fresh terminal window.',
|
||
|
|
}),
|
||
|
|
expect.any(Number),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should install the extension if found in the dev directory', async () => {
|
||
|
|
const vsixPath = '/path/to/dev/gemini.vsix';
|
||
|
|
execSyncSpy.mockReturnValue(''); // VSCode is installed
|
||
|
|
// First glob call for bundle returns nothing, second for dev returns path.
|
||
|
|
globSyncSpy.mockReturnValueOnce([]).mockReturnValueOnce([vsixPath]);
|
||
|
|
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
await command?.subCommands?.[1].action(mockContext, '');
|
||
|
|
|
||
|
|
expect(globSyncSpy).toHaveBeenCalledTimes(2);
|
||
|
|
expect(execSyncSpy).toHaveBeenCalledWith(
|
||
|
|
`code --install-extension ${vsixPath} --force`,
|
||
|
|
{ stdio: 'pipe' },
|
||
|
|
);
|
||
|
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
type: 'info',
|
||
|
|
text: 'VS Code companion extension installed successfully. Restart gemini-cli in a fresh terminal window.',
|
||
|
|
}),
|
||
|
|
expect.any(Number),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should show an error if installation fails', async () => {
|
||
|
|
const vsixPath = '/path/to/bundle/gemini.vsix';
|
||
|
|
const errorMessage = 'Installation failed';
|
||
|
|
execSyncSpy
|
||
|
|
.mockReturnValueOnce('') // VSCode is installed check
|
||
|
|
.mockImplementation(() => {
|
||
|
|
// Installation command
|
||
|
|
const error: Error & { stderr?: Buffer } = new Error(
|
||
|
|
'Command failed',
|
||
|
|
);
|
||
|
|
error.stderr = Buffer.from(errorMessage);
|
||
|
|
throw error;
|
||
|
|
});
|
||
|
|
globSyncSpy.mockReturnValue([vsixPath]);
|
||
|
|
|
||
|
|
const command = ideCommand(mockConfig);
|
||
|
|
await command?.subCommands?.[1].action(mockContext, '');
|
||
|
|
|
||
|
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
type: 'error',
|
||
|
|
text: `Failed to install VS Code companion extension.`,
|
||
|
|
}),
|
||
|
|
expect.any(Number),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|