/** * @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(); 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), ); }); }); });