Add /ide status & /ide install commands to manage IDE integration (#4265)

This commit is contained in:
Shreya Keshive
2025-07-16 18:36:14 -04:00
committed by GitHub
parent 69a8ae6a89
commit ab9eb9377f
7 changed files with 514 additions and 28 deletions

View File

@@ -0,0 +1,256 @@
/**
* @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),
);
});
});
});