mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 22:55:13 +00:00
300 lines
8.7 KiB
TypeScript
300 lines
8.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import {
|
|
describe,
|
|
it,
|
|
expect,
|
|
vi,
|
|
beforeEach,
|
|
afterEach,
|
|
type Mock,
|
|
} from 'vitest';
|
|
import { configureCommand } from './configure.js';
|
|
import yargs from 'yargs';
|
|
import { debugLogger } from '@google/gemini-cli-core';
|
|
import {
|
|
updateSetting,
|
|
promptForSetting,
|
|
getScopedEnvContents,
|
|
type ExtensionSetting,
|
|
} from '../../config/extensions/extensionSettings.js';
|
|
import prompts from 'prompts';
|
|
import * as fs from 'node:fs';
|
|
|
|
const {
|
|
mockExtensionManager,
|
|
mockGetExtensionAndManager,
|
|
mockGetExtensionManager,
|
|
mockLoadSettings,
|
|
} = vi.hoisted(() => {
|
|
const extensionManager = {
|
|
loadExtensionConfig: vi.fn(),
|
|
getExtensions: vi.fn(),
|
|
loadExtensions: vi.fn(),
|
|
getSettings: vi.fn(),
|
|
};
|
|
return {
|
|
mockExtensionManager: extensionManager,
|
|
mockGetExtensionAndManager: vi.fn(),
|
|
mockGetExtensionManager: vi.fn(),
|
|
mockLoadSettings: vi.fn().mockReturnValue({ merged: {} }),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../config/extension-manager.js', () => ({
|
|
ExtensionManager: vi.fn().mockImplementation(() => mockExtensionManager),
|
|
}));
|
|
|
|
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
|
updateSetting: vi.fn(),
|
|
promptForSetting: vi.fn(),
|
|
getScopedEnvContents: vi.fn(),
|
|
ExtensionSettingScope: {
|
|
USER: 'user',
|
|
WORKSPACE: 'workspace',
|
|
},
|
|
}));
|
|
|
|
vi.mock('../utils.js', () => ({
|
|
exitCli: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('./utils.js', () => ({
|
|
getExtensionAndManager: mockGetExtensionAndManager,
|
|
getExtensionManager: mockGetExtensionManager,
|
|
}));
|
|
|
|
vi.mock('prompts');
|
|
|
|
vi.mock('../../config/extensions/consent.js', () => ({
|
|
requestConsentNonInteractive: vi.fn(),
|
|
}));
|
|
|
|
import { ExtensionManager } from '../../config/extension-manager.js';
|
|
|
|
vi.mock('../../config/settings.js', () => ({
|
|
loadSettings: mockLoadSettings,
|
|
}));
|
|
|
|
describe('extensions configure command', () => {
|
|
let tempWorkspaceDir: string;
|
|
|
|
beforeEach(() => {
|
|
vi.spyOn(debugLogger, 'log');
|
|
vi.spyOn(debugLogger, 'error');
|
|
vi.clearAllMocks();
|
|
|
|
tempWorkspaceDir = fs.mkdtempSync('gemini-cli-test-workspace');
|
|
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
|
// Default behaviors
|
|
mockLoadSettings.mockReturnValue({ merged: {} });
|
|
mockGetExtensionAndManager.mockResolvedValue({
|
|
extension: null,
|
|
extensionManager: null,
|
|
});
|
|
mockGetExtensionManager.mockResolvedValue(mockExtensionManager);
|
|
(ExtensionManager as unknown as Mock).mockImplementation(
|
|
() => mockExtensionManager,
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const runCommand = async (command: string) => {
|
|
const parser = yargs().command(configureCommand).help(false).version(false);
|
|
await parser.parse(command);
|
|
};
|
|
|
|
const setupExtension = (
|
|
name: string,
|
|
settings: Array<Partial<ExtensionSetting>> = [],
|
|
id = 'test-id',
|
|
path = '/test/path',
|
|
) => {
|
|
const extension = { name, path, id };
|
|
mockGetExtensionAndManager.mockImplementation(async (n) => {
|
|
if (n === name)
|
|
return { extension, extensionManager: mockExtensionManager };
|
|
return { extension: null, extensionManager: null };
|
|
});
|
|
|
|
mockExtensionManager.getExtensions.mockReturnValue([extension]);
|
|
mockExtensionManager.loadExtensionConfig.mockResolvedValue({
|
|
name,
|
|
settings,
|
|
});
|
|
return extension;
|
|
};
|
|
|
|
describe('Specific setting configuration', () => {
|
|
it('should configure a specific setting', async () => {
|
|
setupExtension('test-ext', [
|
|
{ name: 'Test Setting', envVar: 'TEST_VAR' },
|
|
]);
|
|
(updateSetting as Mock).mockResolvedValue(undefined);
|
|
|
|
await runCommand('config test-ext TEST_VAR');
|
|
|
|
expect(updateSetting).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: 'test-ext' }),
|
|
'test-id',
|
|
'TEST_VAR',
|
|
promptForSetting,
|
|
'user',
|
|
tempWorkspaceDir,
|
|
);
|
|
});
|
|
|
|
it('should handle missing extension', async () => {
|
|
mockGetExtensionAndManager.mockResolvedValue({
|
|
extension: null,
|
|
extensionManager: null,
|
|
});
|
|
|
|
await runCommand('config missing-ext TEST_VAR');
|
|
|
|
expect(updateSetting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject invalid extension names', async () => {
|
|
await runCommand('config ../invalid TEST_VAR');
|
|
expect(debugLogger.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('Invalid extension name'),
|
|
);
|
|
|
|
await runCommand('config ext/with/slash TEST_VAR');
|
|
expect(debugLogger.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('Invalid extension name'),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Extension configuration (all settings)', () => {
|
|
it('should configure all settings for an extension', async () => {
|
|
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
|
setupExtension('test-ext', settings);
|
|
(getScopedEnvContents as Mock).mockResolvedValue({});
|
|
(updateSetting as Mock).mockResolvedValue(undefined);
|
|
|
|
await runCommand('config test-ext');
|
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
|
'Configuring settings for "test-ext"...',
|
|
);
|
|
expect(updateSetting).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: 'test-ext' }),
|
|
'test-id',
|
|
'VAR_1',
|
|
promptForSetting,
|
|
'user',
|
|
tempWorkspaceDir,
|
|
);
|
|
});
|
|
|
|
it('should verify overwrite if setting is already set', async () => {
|
|
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
|
setupExtension('test-ext', settings);
|
|
(getScopedEnvContents as Mock).mockImplementation(
|
|
async (_config, _id, scope) => {
|
|
if (scope === 'user') return { VAR_1: 'existing' };
|
|
return {};
|
|
},
|
|
);
|
|
(prompts as unknown as Mock).mockResolvedValue({ overwrite: true });
|
|
(updateSetting as Mock).mockResolvedValue(undefined);
|
|
|
|
await runCommand('config test-ext');
|
|
|
|
expect(prompts).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: 'confirm',
|
|
message: expect.stringContaining('is already set. Overwrite?'),
|
|
}),
|
|
);
|
|
expect(updateSetting).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should note if setting is configured in workspace', async () => {
|
|
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
|
setupExtension('test-ext', settings);
|
|
(getScopedEnvContents as Mock).mockImplementation(
|
|
async (_config, _id, scope) => {
|
|
if (scope === 'workspace') return { VAR_1: 'workspace_value' };
|
|
return {};
|
|
},
|
|
);
|
|
(updateSetting as Mock).mockResolvedValue(undefined);
|
|
|
|
await runCommand('config test-ext');
|
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('is already configured in the workspace scope'),
|
|
);
|
|
});
|
|
|
|
it('should skip update if user denies overwrite', async () => {
|
|
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
|
setupExtension('test-ext', settings);
|
|
(getScopedEnvContents as Mock).mockResolvedValue({ VAR_1: 'existing' });
|
|
(prompts as unknown as Mock).mockResolvedValue({ overwrite: false });
|
|
|
|
await runCommand('config test-ext');
|
|
|
|
expect(prompts).toHaveBeenCalled();
|
|
expect(updateSetting).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Configure all extensions', () => {
|
|
it('should configure settings for all installed extensions', async () => {
|
|
const ext1 = {
|
|
name: 'ext1',
|
|
path: '/p1',
|
|
id: 'id1',
|
|
settings: [{ envVar: 'V1' }],
|
|
};
|
|
const ext2 = {
|
|
name: 'ext2',
|
|
path: '/p2',
|
|
id: 'id2',
|
|
settings: [{ envVar: 'V2' }],
|
|
};
|
|
mockExtensionManager.getExtensions.mockReturnValue([ext1, ext2]);
|
|
|
|
mockExtensionManager.loadExtensionConfig.mockImplementation(
|
|
async (path) => {
|
|
if (path === '/p1')
|
|
return { name: 'ext1', settings: [{ name: 'S1', envVar: 'V1' }] };
|
|
if (path === '/p2')
|
|
return { name: 'ext2', settings: [{ name: 'S2', envVar: 'V2' }] };
|
|
return null;
|
|
},
|
|
);
|
|
|
|
(getScopedEnvContents as Mock).mockResolvedValue({});
|
|
(updateSetting as Mock).mockResolvedValue(undefined);
|
|
|
|
await runCommand('config');
|
|
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('Configuring settings for "ext1"'),
|
|
);
|
|
expect(debugLogger.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('Configuring settings for "ext2"'),
|
|
);
|
|
expect(updateSetting).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should log if no extensions installed', async () => {
|
|
mockExtensionManager.getExtensions.mockReturnValue([]);
|
|
await runCommand('config');
|
|
expect(debugLogger.log).toHaveBeenCalledWith('No extensions installed.');
|
|
});
|
|
});
|
|
});
|