mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 02:56:48 +00:00
Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
195 lines
5.8 KiB
TypeScript
195 lines
5.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { format } from 'node:util';
|
|
import { handleList, listCommand } from './list.js';
|
|
import { ExtensionManager } from '../../config/extension-manager.js';
|
|
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
|
|
// Mock dependencies
|
|
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
|
const debugLogger = vi.hoisted(() => ({
|
|
log: vi.fn((message, ...args) => {
|
|
emitConsoleLog('log', format(message, ...args));
|
|
}),
|
|
error: vi.fn((message, ...args) => {
|
|
emitConsoleLog('error', format(message, ...args));
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
|
return {
|
|
...actual,
|
|
coreEvents: {
|
|
emitConsoleLog,
|
|
},
|
|
debugLogger,
|
|
};
|
|
});
|
|
|
|
vi.mock('../../config/extension-manager.js');
|
|
vi.mock('../../config/settings.js');
|
|
vi.mock('../../utils/errors.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(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(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(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(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(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<void>
|
|
)({
|
|
'output-format': 'json',
|
|
});
|
|
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|