/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { coreEvents, getErrorMessage } from '@google/gemini-cli-core'; import { handleList, listCommand } from './list.js'; import { ExtensionManager } from '../../config/extension-manager.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; vi.mock('@google/gemini-cli-core', async (importOriginal) => { const { mockCoreDebugLogger } = await import( '../../test-utils/mockDebugLogger.js' ); const actual = await importOriginal(); const mocked = mockCoreDebugLogger(actual, { stripAnsi: false }); return { ...mocked, getErrorMessage: vi.fn() }; }); vi.mock('../../config/extension-manager.js'); vi.mock('../../config/settings.js'); vi.mock('../../config/extensions/consent.js', () => ({ requestConsentNonInteractive: vi.fn(), })); vi.mock('../../config/extensions/extensionSettings.js', () => ({ promptForSetting: vi.fn(), })); vi.mock('../utils.js', () => ({ exitCli: vi.fn(), })); describe('extensions list command', () => { const mockLoadSettings = vi.mocked(loadSettings); const mockGetErrorMessage = vi.mocked(getErrorMessage); const mockExtensionManager = vi.mocked(ExtensionManager); beforeEach(async () => { vi.clearAllMocks(); mockLoadSettings.mockReturnValue({ merged: {}, } as unknown as LoadedSettings); }); afterEach(() => { vi.restoreAllMocks(); }); describe('handleList', () => { it('should log a message if no extensions are installed', async () => { const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); mockExtensionManager.prototype.loadExtensions = vi .fn() .mockResolvedValue([]); await handleList(); expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'No extensions installed.', ); mockCwd.mockRestore(); }); it('should output empty JSON array if no extensions are installed and output-format is json', async () => { const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); mockExtensionManager.prototype.loadExtensions = vi .fn() .mockResolvedValue([]); await handleList({ outputFormat: 'json' }); expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith('log', '[]'); mockCwd.mockRestore(); }); it('should list all installed extensions', async () => { const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); const extensions = [ { name: 'ext1', version: '1.0.0' }, { name: 'ext2', version: '2.0.0' }, ]; mockExtensionManager.prototype.loadExtensions = vi .fn() .mockResolvedValue(extensions); mockExtensionManager.prototype.toOutputString = vi.fn( (ext) => `${ext.name}@${ext.version}`, ); await handleList(); expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'ext1@1.0.0\n\next2@2.0.0', ); mockCwd.mockRestore(); }); it('should list all installed extensions in JSON format', async () => { const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); const extensions = [ { name: 'ext1', version: '1.0.0' }, { name: 'ext2', version: '2.0.0' }, ]; mockExtensionManager.prototype.loadExtensions = vi .fn() .mockResolvedValue(extensions); await handleList({ outputFormat: 'json' }); expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', JSON.stringify(extensions, null, 2), ); mockCwd.mockRestore(); }); it('should log an error message and exit with code 1 when listing fails', async () => { const mockProcessExit = vi .spyOn(process, 'exit') .mockImplementation((() => {}) as ( code?: string | number | null | undefined, ) => never); const error = new Error('List failed'); mockExtensionManager.prototype.loadExtensions = vi .fn() .mockRejectedValue(error); mockGetErrorMessage.mockReturnValue('List failed message'); await handleList(); expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'error', 'List failed message', ); expect(mockProcessExit).toHaveBeenCalledWith(1); mockProcessExit.mockRestore(); }); }); describe('listCommand', () => { const command = listCommand; it('should have correct command and describe', () => { expect(command.command).toBe('list'); expect(command.describe).toBe('Lists installed extensions.'); }); it('builder should have output-format option', () => { const mockYargs = { option: vi.fn().mockReturnThis(), }; ( command.builder as unknown as ( yargs: typeof mockYargs, ) => typeof mockYargs )(mockYargs); expect(mockYargs.option).toHaveBeenCalledWith('output-format', { alias: 'o', type: 'string', describe: 'The format of the CLI output.', choices: ['text', 'json'], default: 'text', }); }); it('handler should call handleList with parsed arguments', async () => { mockExtensionManager.prototype.loadExtensions = vi .fn() .mockResolvedValue([]); await ( command.handler as unknown as (args: { 'output-format': string; }) => Promise )({ 'output-format': 'json', }); expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled(); }); }); });