mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 08:24:10 -07:00
Add support for sensitive keychain-stored per-extension settings (#11953)
This commit is contained in:
@@ -17,6 +17,7 @@ import { ExtensionStorage } from './storage.js';
|
||||
import prompts from 'prompts';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import * as fs from 'node:fs';
|
||||
import { KeychainTokenStorage } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('prompts');
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
@@ -27,11 +28,59 @@ vi.mock('os', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
|
||||
getSecret: vi.fn(),
|
||||
setSecret: vi.fn(),
|
||||
deleteSecret: vi.fn(),
|
||||
listSecrets: vi.fn(),
|
||||
isAvailable: vi.fn().mockResolvedValue(true),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
interface MockKeychainStorage {
|
||||
getSecret: ReturnType<typeof vi.fn>;
|
||||
setSecret: ReturnType<typeof vi.fn>;
|
||||
deleteSecret: ReturnType<typeof vi.fn>;
|
||||
listSecrets: ReturnType<typeof vi.fn>;
|
||||
isAvailable: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
describe('extensionSettings', () => {
|
||||
let tempHomeDir: string;
|
||||
let extensionDir: string;
|
||||
let mockKeychainStorage: MockKeychainStorage;
|
||||
let keychainData: Record<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
keychainData = {};
|
||||
mockKeychainStorage = {
|
||||
getSecret: vi
|
||||
.fn()
|
||||
.mockImplementation(async (key: string) => keychainData[key] || null),
|
||||
setSecret: vi
|
||||
.fn()
|
||||
.mockImplementation(async (key: string, value: string) => {
|
||||
keychainData[key] = value;
|
||||
}),
|
||||
deleteSecret: vi.fn().mockImplementation(async (key: string) => {
|
||||
delete keychainData[key];
|
||||
}),
|
||||
listSecrets: vi
|
||||
.fn()
|
||||
.mockImplementation(async () => Object.keys(keychainData)),
|
||||
isAvailable: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
(
|
||||
KeychainTokenStorage as unknown as ReturnType<typeof vi.fn>
|
||||
).mockImplementation(() => mockKeychainStorage);
|
||||
|
||||
tempHomeDir = os.tmpdir() + path.sep + `gemini-cli-test-home-${Date.now()}`;
|
||||
extensionDir = path.join(tempHomeDir, '.gemini', 'extensions', 'test-ext');
|
||||
// Spy and mock the method, but also create the directory so we can write to it.
|
||||
@@ -59,7 +108,13 @@ describe('extensionSettings', () => {
|
||||
|
||||
it('should do nothing if settings are undefined', async () => {
|
||||
const config: ExtensionConfig = { name: 'test-ext', version: '1.0.0' };
|
||||
await maybePromptForSettings(config, mockRequestSetting);
|
||||
await maybePromptForSettings(
|
||||
config,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockRequestSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -69,11 +124,17 @@ describe('extensionSettings', () => {
|
||||
version: '1.0.0',
|
||||
settings: [],
|
||||
};
|
||||
await maybePromptForSettings(config, mockRequestSetting);
|
||||
await maybePromptForSettings(
|
||||
config,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockRequestSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call requestSetting for each setting', async () => {
|
||||
it('should prompt for all settings if there is no previous config', async () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
@@ -82,14 +143,25 @@ describe('extensionSettings', () => {
|
||||
{ name: 's2', description: 'd2', envVar: 'VAR2' },
|
||||
],
|
||||
};
|
||||
await maybePromptForSettings(config, mockRequestSetting);
|
||||
await maybePromptForSettings(
|
||||
config,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockRequestSetting).toHaveBeenCalledTimes(2);
|
||||
expect(mockRequestSetting).toHaveBeenCalledWith(config.settings![0]);
|
||||
expect(mockRequestSetting).toHaveBeenCalledWith(config.settings![1]);
|
||||
});
|
||||
|
||||
it('should write the .env file with the correct content', async () => {
|
||||
const config: ExtensionConfig = {
|
||||
it('should only prompt for new settings', async () => {
|
||||
const previousConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
|
||||
};
|
||||
const newConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
@@ -97,35 +169,151 @@ describe('extensionSettings', () => {
|
||||
{ name: 's2', description: 'd2', envVar: 'VAR2' },
|
||||
],
|
||||
};
|
||||
await maybePromptForSettings(config, mockRequestSetting);
|
||||
const previousSettings = { VAR1: 'previous-VAR1' };
|
||||
|
||||
await maybePromptForSettings(
|
||||
newConfig,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
previousConfig,
|
||||
previousSettings,
|
||||
);
|
||||
|
||||
expect(mockRequestSetting).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequestSetting).toHaveBeenCalledWith(newConfig.settings![1]);
|
||||
|
||||
const expectedEnvPath = path.join(extensionDir, '.env');
|
||||
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
|
||||
const expectedContent = 'VAR1=mock-VAR1\nVAR2=mock-VAR2\n';
|
||||
const expectedContent = 'VAR1=previous-VAR1\nVAR2=mock-VAR2\n';
|
||||
expect(actualContent).toBe(expectedContent);
|
||||
});
|
||||
|
||||
it('should remove settings that are no longer in the config', async () => {
|
||||
const previousConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{ name: 's1', description: 'd1', envVar: 'VAR1' },
|
||||
{ name: 's2', description: 'd2', envVar: 'VAR2' },
|
||||
],
|
||||
};
|
||||
const newConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
|
||||
};
|
||||
const previousSettings = {
|
||||
VAR1: 'previous-VAR1',
|
||||
VAR2: 'previous-VAR2',
|
||||
};
|
||||
|
||||
await maybePromptForSettings(
|
||||
newConfig,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
previousConfig,
|
||||
previousSettings,
|
||||
);
|
||||
|
||||
expect(mockRequestSetting).not.toHaveBeenCalled();
|
||||
|
||||
const expectedEnvPath = path.join(extensionDir, '.env');
|
||||
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
|
||||
const expectedContent = 'VAR1=previous-VAR1\n';
|
||||
expect(actualContent).toBe(expectedContent);
|
||||
});
|
||||
|
||||
it('should reprompt if a setting changes sensitivity', async () => {
|
||||
const previousConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{ name: 's1', description: 'd1', envVar: 'VAR1', sensitive: false },
|
||||
],
|
||||
};
|
||||
const newConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{ name: 's1', description: 'd1', envVar: 'VAR1', sensitive: true },
|
||||
],
|
||||
};
|
||||
const previousSettings = { VAR1: 'previous-VAR1' };
|
||||
|
||||
await maybePromptForSettings(
|
||||
newConfig,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
previousConfig,
|
||||
previousSettings,
|
||||
);
|
||||
|
||||
expect(mockRequestSetting).toHaveBeenCalledTimes(1);
|
||||
expect(mockRequestSetting).toHaveBeenCalledWith(newConfig.settings![0]);
|
||||
|
||||
// The value should now be in keychain, not the .env file.
|
||||
const expectedEnvPath = path.join(extensionDir, '.env');
|
||||
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
|
||||
expect(actualContent).toBe('');
|
||||
});
|
||||
|
||||
it('should not prompt if settings are identical', async () => {
|
||||
const previousConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{ name: 's1', description: 'd1', envVar: 'VAR1' },
|
||||
{ name: 's2', description: 'd2', envVar: 'VAR2' },
|
||||
],
|
||||
};
|
||||
const newConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{ name: 's1', description: 'd1', envVar: 'VAR1' },
|
||||
{ name: 's2', description: 'd2', envVar: 'VAR2' },
|
||||
],
|
||||
};
|
||||
const previousSettings = {
|
||||
VAR1: 'previous-VAR1',
|
||||
VAR2: 'previous-VAR2',
|
||||
};
|
||||
|
||||
await maybePromptForSettings(
|
||||
newConfig,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
previousConfig,
|
||||
previousSettings,
|
||||
);
|
||||
|
||||
expect(mockRequestSetting).not.toHaveBeenCalled();
|
||||
const expectedEnvPath = path.join(extensionDir, '.env');
|
||||
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
|
||||
const expectedContent = 'VAR1=previous-VAR1\nVAR2=previous-VAR2\n';
|
||||
expect(actualContent).toBe(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('promptForSetting', () => {
|
||||
// it('should use prompts with type "password" for sensitive settings', async () => {
|
||||
// const setting: ExtensionSetting = {
|
||||
// name: 'API Key',
|
||||
// description: 'Your secret key',
|
||||
// envVar: 'API_KEY',
|
||||
// sensitive: true,
|
||||
// };
|
||||
// vi.mocked(prompts).mockResolvedValue({ value: 'secret-key' });
|
||||
it('should use prompts with type "password" for sensitive settings', async () => {
|
||||
const setting: ExtensionSetting = {
|
||||
name: 'API Key',
|
||||
description: 'Your secret key',
|
||||
envVar: 'API_KEY',
|
||||
sensitive: true,
|
||||
};
|
||||
vi.mocked(prompts).mockResolvedValue({ value: 'secret-key' });
|
||||
|
||||
// const result = await promptForSetting(setting);
|
||||
const result = await promptForSetting(setting);
|
||||
|
||||
// expect(prompts).toHaveBeenCalledWith({
|
||||
// type: 'password',
|
||||
// name: 'value',
|
||||
// message: 'API Key\nYour secret key',
|
||||
// });
|
||||
// expect(result).toBe('secret-key');
|
||||
// });
|
||||
expect(prompts).toHaveBeenCalledWith({
|
||||
type: 'password',
|
||||
name: 'value',
|
||||
message: 'API Key\nYour secret key',
|
||||
});
|
||||
expect(result).toBe('secret-key');
|
||||
});
|
||||
|
||||
it('should use prompts with type "text" for non-sensitive settings', async () => {
|
||||
const setting: ExtensionSetting = {
|
||||
|
||||
@@ -12,57 +12,76 @@ import { ExtensionStorage } from './storage.js';
|
||||
import type { ExtensionConfig } from '../extension.js';
|
||||
|
||||
import prompts from 'prompts';
|
||||
import { KeychainTokenStorage } from '@google/gemini-cli-core';
|
||||
|
||||
export interface ExtensionSetting {
|
||||
name: string;
|
||||
description: string;
|
||||
envVar: string;
|
||||
// NOTE: If no value is set, this setting will be considered NOT sensitive.
|
||||
sensitive?: boolean;
|
||||
}
|
||||
|
||||
export async function maybePromptForSettings(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
previousSettings?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
const { name: extensionName, settings } = extensionConfig;
|
||||
if (
|
||||
(!settings || settings.length === 0) &&
|
||||
(!previousExtensionConfig?.settings ||
|
||||
previousExtensionConfig.settings.length === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const envFilePath = new ExtensionStorage(extensionName).getEnvFilePath();
|
||||
const keychain = new KeychainTokenStorage(extensionId);
|
||||
|
||||
if (!settings || settings.length === 0) {
|
||||
// No settings for this extension. Clear any existing .env file.
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
await fs.writeFile(envFilePath, '');
|
||||
}
|
||||
await clearSettings(envFilePath, keychain);
|
||||
return;
|
||||
}
|
||||
|
||||
let settingsToPrompt = settings;
|
||||
if (previousExtensionConfig) {
|
||||
const oldSettings = new Set(
|
||||
previousExtensionConfig.settings?.map((s) => s.name) || [],
|
||||
);
|
||||
settingsToPrompt = settingsToPrompt.filter((s) => !oldSettings.has(s.name));
|
||||
}
|
||||
const settingsChanges = getSettingsChanges(
|
||||
settings,
|
||||
previousExtensionConfig?.settings ?? [],
|
||||
);
|
||||
|
||||
const allSettings: Record<string, string> = { ...(previousSettings ?? {}) };
|
||||
|
||||
if (settingsToPrompt && settingsToPrompt.length > 0) {
|
||||
for (const setting of settingsToPrompt) {
|
||||
const answer = await requestSetting(setting);
|
||||
allSettings[setting.envVar] = answer;
|
||||
}
|
||||
for (const removedEnvSetting of settingsChanges.removeEnv) {
|
||||
delete allSettings[removedEnvSetting.envVar];
|
||||
}
|
||||
|
||||
const validEnvVars = new Set(settings.map((s) => s.envVar));
|
||||
const finalSettings: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(allSettings)) {
|
||||
if (validEnvVars.has(key)) {
|
||||
finalSettings[key] = value;
|
||||
for (const removedSensitiveSetting of settingsChanges.removeSensitive) {
|
||||
await keychain.deleteSecret(removedSensitiveSetting.envVar);
|
||||
}
|
||||
|
||||
for (const setting of settingsChanges.promptForSensitive.concat(
|
||||
settingsChanges.promptForEnv,
|
||||
)) {
|
||||
const answer = await requestSetting(setting);
|
||||
allSettings[setting.envVar] = answer;
|
||||
}
|
||||
|
||||
const nonSensitiveSettings: Record<string, string> = {};
|
||||
for (const setting of settings) {
|
||||
const value = allSettings[setting.envVar];
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (setting.sensitive) {
|
||||
await keychain.setSecret(setting.envVar, value);
|
||||
} else {
|
||||
nonSensitiveSettings[setting.envVar] = value;
|
||||
}
|
||||
}
|
||||
|
||||
let envContent = '';
|
||||
for (const [key, value] of Object.entries(finalSettings)) {
|
||||
for (const [key, value] of Object.entries(nonSensitiveSettings)) {
|
||||
envContent += `${key}=${value}\n`;
|
||||
}
|
||||
|
||||
@@ -73,17 +92,22 @@ export async function promptForSetting(
|
||||
setting: ExtensionSetting,
|
||||
): Promise<string> {
|
||||
const response = await prompts({
|
||||
// type: setting.sensitive ? 'password' : 'text',
|
||||
type: 'text',
|
||||
type: setting.sensitive ? 'password' : 'text',
|
||||
name: 'value',
|
||||
message: `${setting.name}\n${setting.description}`,
|
||||
});
|
||||
return response.value;
|
||||
}
|
||||
|
||||
export function getEnvContents(
|
||||
extensionStorage: ExtensionStorage,
|
||||
): Record<string, string> {
|
||||
export async function getEnvContents(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
): Promise<Record<string, string>> {
|
||||
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
const extensionStorage = new ExtensionStorage(extensionConfig.name);
|
||||
const keychain = new KeychainTokenStorage(extensionId);
|
||||
let customEnv: Record<string, string> = {};
|
||||
if (fsSync.existsSync(extensionStorage.getEnvFilePath())) {
|
||||
const envFile = fsSync.readFileSync(
|
||||
@@ -92,5 +116,67 @@ export function getEnvContents(
|
||||
);
|
||||
customEnv = dotenv.parse(envFile);
|
||||
}
|
||||
|
||||
if (extensionConfig.settings) {
|
||||
for (const setting of extensionConfig.settings) {
|
||||
if (setting.sensitive) {
|
||||
const secret = await keychain.getSecret(setting.envVar);
|
||||
if (secret) {
|
||||
customEnv[setting.envVar] = secret;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return customEnv;
|
||||
}
|
||||
|
||||
interface settingsChanges {
|
||||
promptForSensitive: ExtensionSetting[];
|
||||
removeSensitive: ExtensionSetting[];
|
||||
promptForEnv: ExtensionSetting[];
|
||||
removeEnv: ExtensionSetting[];
|
||||
}
|
||||
function getSettingsChanges(
|
||||
settings: ExtensionSetting[],
|
||||
oldSettings: ExtensionSetting[],
|
||||
): settingsChanges {
|
||||
const isSameSetting = (a: ExtensionSetting, b: ExtensionSetting) =>
|
||||
a.envVar === b.envVar && (a.sensitive ?? false) === (b.sensitive ?? false);
|
||||
|
||||
const sensitiveOld = oldSettings.filter((s) => s.sensitive ?? false);
|
||||
const sensitiveNew = settings.filter((s) => s.sensitive ?? false);
|
||||
const envOld = oldSettings.filter((s) => !(s.sensitive ?? false));
|
||||
const envNew = settings.filter((s) => !(s.sensitive ?? false));
|
||||
|
||||
return {
|
||||
promptForSensitive: sensitiveNew.filter(
|
||||
(s) => !sensitiveOld.some((old) => isSameSetting(s, old)),
|
||||
),
|
||||
removeSensitive: sensitiveOld.filter(
|
||||
(s) => !sensitiveNew.some((neu) => isSameSetting(s, neu)),
|
||||
),
|
||||
promptForEnv: envNew.filter(
|
||||
(s) => !envOld.some((old) => isSameSetting(s, old)),
|
||||
),
|
||||
removeEnv: envOld.filter(
|
||||
(s) => !envNew.some((neu) => isSameSetting(s, neu)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function clearSettings(
|
||||
envFilePath: string,
|
||||
keychain: KeychainTokenStorage,
|
||||
) {
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
await fs.writeFile(envFilePath, '');
|
||||
}
|
||||
if (!keychain.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
const secrets = await keychain.listSecrets();
|
||||
for (const secret of secrets) {
|
||||
await keychain.deleteSecret(secret);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { checkForAllExtensionUpdates, updateExtension } from './update.js';
|
||||
import { GEMINI_DIR } from '@google/gemini-cli-core';
|
||||
import { GEMINI_DIR, KeychainTokenStorage } from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from '../trustedFolders.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import { createExtension } from '../../test-utils/createExtension.js';
|
||||
@@ -64,9 +64,24 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
logExtensionUninstall: mockLogExtensionUninstall,
|
||||
ExtensionInstallEvent: vi.fn(),
|
||||
ExtensionUninstallEvent: vi.fn(),
|
||||
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
|
||||
getSecret: vi.fn(),
|
||||
setSecret: vi.fn(),
|
||||
deleteSecret: vi.fn(),
|
||||
listSecrets: vi.fn(),
|
||||
isAvailable: vi.fn().mockResolvedValue(true),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
interface MockKeychainStorage {
|
||||
getSecret: ReturnType<typeof vi.fn>;
|
||||
setSecret: ReturnType<typeof vi.fn>;
|
||||
deleteSecret: ReturnType<typeof vi.fn>;
|
||||
listSecrets: ReturnType<typeof vi.fn>;
|
||||
isAvailable: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
describe('update tests', () => {
|
||||
let tempHomeDir: string;
|
||||
let tempWorkspaceDir: string;
|
||||
@@ -76,8 +91,32 @@ describe('update tests', () => {
|
||||
let mockPromptForSettings: MockedFunction<
|
||||
(setting: ExtensionSetting) => Promise<string>
|
||||
>;
|
||||
let mockKeychainStorage: MockKeychainStorage;
|
||||
let keychainData: Record<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
keychainData = {};
|
||||
mockKeychainStorage = {
|
||||
getSecret: vi
|
||||
.fn()
|
||||
.mockImplementation(async (key: string) => keychainData[key] || null),
|
||||
setSecret: vi
|
||||
.fn()
|
||||
.mockImplementation(async (key: string, value: string) => {
|
||||
keychainData[key] = value;
|
||||
}),
|
||||
deleteSecret: vi.fn().mockImplementation(async (key: string) => {
|
||||
delete keychainData[key];
|
||||
}),
|
||||
listSecrets: vi
|
||||
.fn()
|
||||
.mockImplementation(async () => Object.keys(keychainData)),
|
||||
isAvailable: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
(
|
||||
KeychainTokenStorage as unknown as ReturnType<typeof vi.fn>
|
||||
).mockImplementation(() => mockKeychainStorage);
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||
);
|
||||
@@ -110,6 +149,7 @@ describe('update tests', () => {
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('updateExtension', () => {
|
||||
@@ -139,11 +179,10 @@ describe('update tests', () => {
|
||||
);
|
||||
});
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === extensionName)!;
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === extensionName)!;
|
||||
const updateInfo = await updateExtension(
|
||||
extension,
|
||||
extension!,
|
||||
extensionManager,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
() => {},
|
||||
@@ -189,11 +228,10 @@ describe('update tests', () => {
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === extensionName)!;
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === extensionName)!;
|
||||
await updateExtension(
|
||||
extension,
|
||||
extension!,
|
||||
extensionManager,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
dispatch,
|
||||
@@ -231,12 +269,11 @@ describe('update tests', () => {
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === extensionName)!;
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === extensionName)!;
|
||||
await expect(
|
||||
updateExtension(
|
||||
extension,
|
||||
extension!,
|
||||
extensionManager,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
dispatch,
|
||||
@@ -280,7 +317,7 @@ describe('update tests', () => {
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensionManager.loadExtensions(),
|
||||
await extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -312,7 +349,7 @@ describe('update tests', () => {
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensionManager.loadExtensions(),
|
||||
await extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -341,7 +378,7 @@ describe('update tests', () => {
|
||||
});
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensionManager.loadExtensions(),
|
||||
await extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -370,7 +407,7 @@ describe('update tests', () => {
|
||||
});
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensionManager.loadExtensions(),
|
||||
await extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -398,7 +435,7 @@ describe('update tests', () => {
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensionManager.loadExtensions(),
|
||||
await extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function updateExtension(
|
||||
|
||||
const tempDir = await ExtensionStorage.createTmpDir();
|
||||
try {
|
||||
const previousExtensionConfig = await extensionManager.loadExtensionConfig(
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
let updatedExtension: GeminiCLIExtension;
|
||||
|
||||
Reference in New Issue
Block a user