Add extension settings to be requested on install (#9802)

This commit is contained in:
christine betts
2025-10-23 11:47:08 -04:00
committed by GitHub
parent bde5d61812
commit 750c0e366f
13 changed files with 641 additions and 20 deletions
+211
View File
@@ -35,6 +35,7 @@ import { isWorkspaceTrusted } from './trustedFolders.js';
import { createExtension } from '../test-utils/createExtension.js';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import { join } from 'node:path';
import type { ExtensionSetting } from './extensions/extensionSettings.js';
const mockGit = {
clone: vi.fn(),
@@ -340,6 +341,36 @@ describe('extension tests', () => {
}
});
it('should resolve environment variables from an extension .env file', () => {
const extDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'test-extension',
version: '1.0.0',
mcpServers: {
'test-server': {
command: 'node',
args: ['server.js'],
env: {
API_KEY: '$MY_API_KEY',
STATIC_VALUE: 'no-substitution',
},
},
},
});
const envFilePath = path.join(extDir, '.env');
fs.writeFileSync(envFilePath, 'MY_API_KEY=test-key-from-file\n');
const extensions = loadExtensions(new ExtensionEnablementManager());
expect(extensions).toHaveLength(1);
const extension = extensions[0];
const serverConfig = extension.mcpServers!['test-server'];
expect(serverConfig.env).toBeDefined();
expect(serverConfig.env!['API_KEY']).toBe('test-key-from-file');
expect(serverConfig.env!['STATIC_VALUE']).toBe('no-substitution');
});
it('should handle missing environment variables gracefully', () => {
const userExtensionsDir = path.join(
tempHomeDir,
@@ -1033,6 +1064,186 @@ This extension will run the following MCP servers:
expect(mockRequestConsent).not.toHaveBeenCalled();
});
it('should prompt for settings if promptForSettings', async () => {
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
settings: [
{
name: 'API Key',
description: 'Your API key for the service.',
envVar: 'MY_API_KEY',
},
],
});
const promptForSettingsMock = vi.fn(
async (_: ExtensionSetting): Promise<string> => Promise.resolve(''),
);
await installOrUpdateExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
process.cwd(),
undefined,
promptForSettingsMock,
);
expect(promptForSettingsMock).toHaveBeenCalled();
});
it('should not prompt for settings if promptForSettings is false', async () => {
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
settings: [
{
name: 'API Key',
description: 'Your API key for the service.',
envVar: 'MY_API_KEY',
},
],
});
await installOrUpdateExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
);
});
it('should only prompt for new settings on update, and preserve old settings', async () => {
// 1. Create and install the "old" version of the extension.
const oldSourceExtDir = createExtension({
extensionsDir: tempHomeDir, // Create it in a temp location first
name: 'my-local-extension',
version: '1.0.0',
settings: [
{
name: 'API Key',
description: 'Your API key for the service.',
envVar: 'MY_API_KEY',
},
],
});
// Install it so it exists in the userExtensionsDir
await installOrUpdateExtension(
{ source: oldSourceExtDir, type: 'local' },
async (_) => true,
process.cwd(),
undefined,
async () => 'old-api-key',
);
const envPath = new ExtensionStorage(
'my-local-extension',
).getEnvFilePath();
expect(fs.existsSync(envPath)).toBe(true);
let envContent = fs.readFileSync(envPath, 'utf-8');
expect(envContent).toContain('MY_API_KEY=old-api-key');
// 2. Create the "new" version of the extension in a new source directory.
const newSourceExtDir = createExtension({
extensionsDir: path.join(tempHomeDir, 'new-source'), // Another temp location
name: 'my-local-extension', // Same name
version: '1.1.0', // New version
settings: [
{
name: 'API Key',
description: 'Your API key for the service.',
envVar: 'MY_API_KEY',
},
{
name: 'New Setting',
description: 'A new setting.',
envVar: 'NEW_SETTING',
},
],
});
const previousExtensionConfig = loadExtensionConfig({
extensionDir: path.join(userExtensionsDir, 'my-local-extension'),
workspaceDir: process.cwd(),
extensionEnablementManager: new ExtensionEnablementManager(),
});
const promptForSettingsMock = vi.fn(
async (_: ExtensionSetting): Promise<string> => 'new-setting-value',
);
// 3. Call installOrUpdateExtension to perform the update.
await installOrUpdateExtension(
{ source: newSourceExtDir, type: 'local' },
async (_) => true,
process.cwd(),
previousExtensionConfig,
promptForSettingsMock,
);
expect(promptForSettingsMock).toHaveBeenCalledTimes(1);
expect(promptForSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({ name: 'New Setting' }),
);
expect(fs.existsSync(envPath)).toBe(true);
envContent = fs.readFileSync(envPath, 'utf-8');
expect(envContent).toContain('MY_API_KEY=old-api-key');
expect(envContent).toContain('NEW_SETTING=new-setting-value');
});
it('should fail auto-update if settings have changed', async () => {
// 1. Install initial version with autoUpdate: true
const oldSourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-auto-update-ext',
version: '1.0.0',
settings: [
{
name: 'OLD_SETTING',
envVar: 'OLD_SETTING',
description: 'An old setting',
},
],
});
await installOrUpdateExtension(
{ source: oldSourceExtDir, type: 'local', autoUpdate: true },
async () => true,
);
// 2. Create new version with different settings
const newSourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-auto-update-ext',
version: '1.1.0',
settings: [
{
name: 'NEW_SETTING',
envVar: 'NEW_SETTING',
description: 'A new setting',
},
],
});
const previousExtensionConfig = loadExtensionConfig({
extensionDir: path.join(userExtensionsDir, 'my-auto-update-ext'),
workspaceDir: process.cwd(),
extensionEnablementManager: new ExtensionEnablementManager(),
});
// 3. Attempt to update and assert it fails
await expect(
installOrUpdateExtension(
{ source: newSourceExtDir, type: 'local', autoUpdate: true },
async () => true,
process.cwd(),
previousExtensionConfig,
),
).rejects.toThrow(
'Extension "my-auto-update-ext" has settings changes and cannot be auto-updated. Please update manually.',
);
});
it('should throw an error for invalid extension names', async () => {
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,