@google/gemini-cli 0.12.0-nightly.20251027.cb0947c5 → 0.12.0-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/package.json +2 -2
- package/dist/src/commands/extensions/disable.d.ts +1 -1
- package/dist/src/commands/extensions/disable.js +5 -4
- package/dist/src/commands/extensions/disable.js.map +1 -1
- package/dist/src/commands/extensions/enable.d.ts +1 -1
- package/dist/src/commands/extensions/enable.js +3 -2
- package/dist/src/commands/extensions/enable.js.map +1 -1
- package/dist/src/commands/extensions/install.js +2 -1
- package/dist/src/commands/extensions/install.js.map +1 -1
- package/dist/src/commands/extensions/install.test.js +1 -0
- package/dist/src/commands/extensions/install.test.js.map +1 -1
- package/dist/src/commands/extensions/link.js +2 -1
- package/dist/src/commands/extensions/link.js.map +1 -1
- package/dist/src/commands/extensions/list.js +2 -2
- package/dist/src/commands/extensions/list.js.map +1 -1
- package/dist/src/commands/extensions/uninstall.js +2 -1
- package/dist/src/commands/extensions/uninstall.js.map +1 -1
- package/dist/src/commands/extensions/update.js +2 -2
- package/dist/src/commands/extensions/update.js.map +1 -1
- package/dist/src/commands/mcp/list.js +2 -2
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/config/config.d.ts +5 -3
- package/dist/src/config/config.js +42 -9
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +186 -161
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/extension-manager.d.ts +23 -10
- package/dist/src/config/extension-manager.js +89 -62
- package/dist/src/config/extension-manager.js.map +1 -1
- package/dist/src/config/extension.test.js +158 -74
- package/dist/src/config/extension.test.js.map +1 -1
- package/dist/src/config/extensions/extensionSettings.d.ts +3 -3
- package/dist/src/config/extensions/extensionSettings.js +74 -24
- package/dist/src/config/extensions/extensionSettings.js.map +1 -1
- package/dist/src/config/extensions/extensionSettings.test.js +145 -24
- package/dist/src/config/extensions/extensionSettings.test.js.map +1 -1
- package/dist/src/config/extensions/github.js +3 -3
- package/dist/src/config/extensions/github.js.map +1 -1
- package/dist/src/config/extensions/github.test.js +1 -1
- package/dist/src/config/extensions/github.test.js.map +1 -1
- package/dist/src/config/extensions/update.js +7 -6
- package/dist/src/config/extensions/update.js.map +1 -1
- package/dist/src/config/extensions/update.test.js +54 -31
- package/dist/src/config/extensions/update.test.js.map +1 -1
- package/dist/src/config/keyBindings.js +1 -1
- package/dist/src/config/keyBindings.js.map +1 -1
- package/dist/src/config/policies/read-only.toml +56 -0
- package/dist/src/config/policies/write.toml +63 -0
- package/dist/src/config/policies/yolo.toml +31 -0
- package/dist/src/config/policy-engine.integration.test.js +41 -38
- package/dist/src/config/policy-engine.integration.test.js.map +1 -1
- package/dist/src/config/policy-toml-loader.d.ts +46 -0
- package/dist/src/config/policy-toml-loader.js +314 -0
- package/dist/src/config/policy-toml-loader.js.map +1 -0
- package/dist/src/config/policy-toml-loader.test.d.ts +6 -0
- package/dist/src/config/policy-toml-loader.test.js +626 -0
- package/dist/src/config/policy-toml-loader.test.js.map +1 -0
- package/dist/src/config/policy.d.ts +9 -2
- package/dist/src/config/policy.js +139 -110
- package/dist/src/config/policy.js.map +1 -1
- package/dist/src/config/policy.test.js +780 -82
- package/dist/src/config/policy.test.js.map +1 -1
- package/dist/src/config/settings.test.js +4 -4
- package/dist/src/config/settings.test.js.map +1 -1
- package/dist/src/gemini.js +6 -17
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +1 -0
- package/dist/src/gemini.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/test-utils/render.d.ts +12 -0
- package/dist/src/test-utils/render.js +28 -1
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/test-utils/render.test.d.ts +6 -0
- package/dist/src/test-utils/render.test.js +54 -0
- package/dist/src/test-utils/render.test.js.map +1 -0
- package/dist/src/ui/AppContainer.js +28 -22
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/AppContainer.test.js +8 -0
- package/dist/src/ui/AppContainer.test.js.map +1 -1
- package/dist/src/ui/commands/directoryCommand.js +1 -1
- package/dist/src/ui/commands/directoryCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.js +45 -1
- package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.test.js +64 -1
- package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.js +1 -1
- package/dist/src/ui/commands/memoryCommand.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.test.js +3 -1
- package/dist/src/ui/commands/memoryCommand.test.js.map +1 -1
- package/dist/src/ui/components/ConsoleSummaryDisplay.js +1 -1
- package/dist/src/ui/components/ConsoleSummaryDisplay.js.map +1 -1
- package/dist/src/ui/components/DetailedMessagesDisplay.js +1 -1
- package/dist/src/ui/components/DetailedMessagesDisplay.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js +4 -5
- package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/Footer.js +1 -1
- package/dist/src/ui/components/Footer.js.map +1 -1
- package/dist/src/ui/components/Footer.test.js +24 -0
- package/dist/src/ui/components/Footer.test.js.map +1 -1
- package/dist/src/ui/components/Help.test.js +0 -1
- package/dist/src/ui/components/Help.test.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.test.js +5 -6
- package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +11 -13
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.test.js +12 -14
- package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
- package/dist/src/ui/components/shared/BaseSelectionList.test.js +11 -13
- package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.test.js +2 -2
- package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +6 -5
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/SessionContext.test.js +27 -14
- package/dist/src/ui/contexts/SessionContext.test.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor.js +2 -2
- package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/useAtCompletion.test.js +32 -23
- package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +2 -2
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/useExtensionUpdates.d.ts +1 -2
- package/dist/src/ui/hooks/useExtensionUpdates.js +2 -1
- package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
- package/dist/src/ui/hooks/useExtensionUpdates.test.js +14 -20
- package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
- package/dist/src/ui/hooks/useFlickerDetector.test.js +9 -6
- package/dist/src/ui/hooks/useFlickerDetector.test.js.map +1 -1
- package/dist/src/ui/hooks/useFolderTrust.test.js +45 -23
- package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.js +7 -5
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.test.js +42 -41
- package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
- package/dist/src/ui/hooks/useHistoryManager.test.js +2 -2
- package/dist/src/ui/hooks/useHistoryManager.test.js.map +1 -1
- package/dist/src/ui/hooks/useInputHistory.test.js +2 -2
- package/dist/src/ui/hooks/useInputHistory.test.js.map +1 -1
- package/dist/src/ui/hooks/useInputHistoryStore.test.js +2 -2
- package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -1
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +2 -3
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.test.js +83 -111
- package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -1
- package/dist/src/ui/hooks/useQuotaAndFallback.test.js +2 -2
- package/dist/src/ui/hooks/useQuotaAndFallback.test.js.map +1 -1
- package/dist/src/ui/hooks/useReactToolScheduler.test.js +1 -2
- package/dist/src/ui/hooks/useReactToolScheduler.test.js.map +1 -1
- package/dist/src/ui/hooks/useReverseSearchCompletion.test.js +2 -2
- package/dist/src/ui/hooks/useReverseSearchCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useShellHistory.test.js +40 -17
- package/dist/src/ui/hooks/useShellHistory.test.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.js +54 -49
- package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +48 -42
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/keyMatchers.test.js +3 -3
- package/dist/src/ui/keyMatchers.test.js.map +1 -1
- package/dist/src/zed-integration/zedIntegration.d.ts +2 -2
- package/dist/src/zed-integration/zedIntegration.js +4 -6
- package/dist/src/zed-integration/zedIntegration.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
/** @vitest-environment jsdom */
|
|
7
6
|
import { vi } from 'vitest';
|
|
8
7
|
import * as fs from 'node:fs';
|
|
9
8
|
import * as os from 'node:os';
|
|
10
9
|
import * as path from 'node:path';
|
|
11
|
-
import { ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, } from '@google/gemini-cli-core';
|
|
10
|
+
import { ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, KeychainTokenStorage, } from '@google/gemini-cli-core';
|
|
12
11
|
import { loadSettings, SettingScope } from './settings.js';
|
|
13
12
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
|
14
13
|
import { createExtension } from '../test-utils/createExtension.js';
|
|
@@ -43,11 +42,12 @@ vi.mock('simple-git', () => ({
|
|
|
43
42
|
return mockGit;
|
|
44
43
|
}),
|
|
45
44
|
}));
|
|
45
|
+
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
|
46
46
|
vi.mock('os', async (importOriginal) => {
|
|
47
47
|
const mockedOs = await importOriginal();
|
|
48
48
|
return {
|
|
49
49
|
...mockedOs,
|
|
50
|
-
homedir:
|
|
50
|
+
homedir: mockHomedir,
|
|
51
51
|
};
|
|
52
52
|
});
|
|
53
53
|
vi.mock('./trustedFolders.js', async (importOriginal) => {
|
|
@@ -75,6 +75,13 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
|
75
75
|
ExtensionInstallEvent: vi.fn(),
|
|
76
76
|
ExtensionUninstallEvent: vi.fn(),
|
|
77
77
|
ExtensionDisableEvent: vi.fn(),
|
|
78
|
+
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
|
|
79
|
+
getSecret: vi.fn(),
|
|
80
|
+
setSecret: vi.fn(),
|
|
81
|
+
deleteSecret: vi.fn(),
|
|
82
|
+
listSecrets: vi.fn(),
|
|
83
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
84
|
+
})),
|
|
78
85
|
};
|
|
79
86
|
});
|
|
80
87
|
vi.mock('child_process', async (importOriginal) => {
|
|
@@ -91,7 +98,29 @@ describe('extension tests', () => {
|
|
|
91
98
|
let extensionManager;
|
|
92
99
|
let mockRequestConsent;
|
|
93
100
|
let mockPromptForSettings;
|
|
101
|
+
let mockKeychainStorage;
|
|
102
|
+
let keychainData;
|
|
94
103
|
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
keychainData = {};
|
|
106
|
+
mockKeychainStorage = {
|
|
107
|
+
getSecret: vi
|
|
108
|
+
.fn()
|
|
109
|
+
.mockImplementation(async (key) => keychainData[key] || null),
|
|
110
|
+
setSecret: vi
|
|
111
|
+
.fn()
|
|
112
|
+
.mockImplementation(async (key, value) => {
|
|
113
|
+
keychainData[key] = value;
|
|
114
|
+
}),
|
|
115
|
+
deleteSecret: vi.fn().mockImplementation(async (key) => {
|
|
116
|
+
delete keychainData[key];
|
|
117
|
+
}),
|
|
118
|
+
listSecrets: vi
|
|
119
|
+
.fn()
|
|
120
|
+
.mockImplementation(async () => Object.keys(keychainData)),
|
|
121
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
122
|
+
};
|
|
123
|
+
KeychainTokenStorage.mockImplementation(() => mockKeychainStorage);
|
|
95
124
|
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-home-'));
|
|
96
125
|
tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'gemini-cli-test-workspace-'));
|
|
97
126
|
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
@@ -110,7 +139,7 @@ describe('extension tests', () => {
|
|
|
110
139
|
workspaceDir: tempWorkspaceDir,
|
|
111
140
|
requestConsent: mockRequestConsent,
|
|
112
141
|
requestSetting: mockPromptForSettings,
|
|
113
|
-
|
|
142
|
+
settings: loadSettings(tempWorkspaceDir).merged,
|
|
114
143
|
});
|
|
115
144
|
});
|
|
116
145
|
afterEach(() => {
|
|
@@ -119,7 +148,7 @@ describe('extension tests', () => {
|
|
|
119
148
|
vi.restoreAllMocks();
|
|
120
149
|
});
|
|
121
150
|
describe('loadExtensions', () => {
|
|
122
|
-
it('should include extension path in loaded extension', () => {
|
|
151
|
+
it('should include extension path in loaded extension', async () => {
|
|
123
152
|
const extensionDir = path.join(userExtensionsDir, 'test-extension');
|
|
124
153
|
fs.mkdirSync(extensionDir, { recursive: true });
|
|
125
154
|
createExtension({
|
|
@@ -127,12 +156,12 @@ describe('extension tests', () => {
|
|
|
127
156
|
name: 'test-extension',
|
|
128
157
|
version: '1.0.0',
|
|
129
158
|
});
|
|
130
|
-
const extensions = extensionManager.loadExtensions();
|
|
159
|
+
const extensions = await extensionManager.loadExtensions();
|
|
131
160
|
expect(extensions).toHaveLength(1);
|
|
132
161
|
expect(extensions[0].path).toBe(extensionDir);
|
|
133
162
|
expect(extensions[0].name).toBe('test-extension');
|
|
134
163
|
});
|
|
135
|
-
it('should load context file path when GEMINI.md is present', () => {
|
|
164
|
+
it('should load context file path when GEMINI.md is present', async () => {
|
|
136
165
|
createExtension({
|
|
137
166
|
extensionsDir: userExtensionsDir,
|
|
138
167
|
name: 'ext1',
|
|
@@ -144,7 +173,7 @@ describe('extension tests', () => {
|
|
|
144
173
|
name: 'ext2',
|
|
145
174
|
version: '2.0.0',
|
|
146
175
|
});
|
|
147
|
-
const extensions = extensionManager.loadExtensions();
|
|
176
|
+
const extensions = await extensionManager.loadExtensions();
|
|
148
177
|
expect(extensions).toHaveLength(2);
|
|
149
178
|
const ext1 = extensions.find((e) => e.name === 'ext1');
|
|
150
179
|
const ext2 = extensions.find((e) => e.name === 'ext2');
|
|
@@ -153,7 +182,7 @@ describe('extension tests', () => {
|
|
|
153
182
|
]);
|
|
154
183
|
expect(ext2?.contextFiles).toEqual([]);
|
|
155
184
|
});
|
|
156
|
-
it('should load context file path from the extension config', () => {
|
|
185
|
+
it('should load context file path from the extension config', async () => {
|
|
157
186
|
createExtension({
|
|
158
187
|
extensionsDir: userExtensionsDir,
|
|
159
188
|
name: 'ext1',
|
|
@@ -161,14 +190,14 @@ describe('extension tests', () => {
|
|
|
161
190
|
addContextFile: false,
|
|
162
191
|
contextFileName: 'my-context-file.md',
|
|
163
192
|
});
|
|
164
|
-
const extensions = extensionManager.loadExtensions();
|
|
193
|
+
const extensions = await extensionManager.loadExtensions();
|
|
165
194
|
expect(extensions).toHaveLength(1);
|
|
166
195
|
const ext1 = extensions.find((e) => e.name === 'ext1');
|
|
167
196
|
expect(ext1?.contextFiles).toEqual([
|
|
168
197
|
path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
|
|
169
198
|
]);
|
|
170
199
|
});
|
|
171
|
-
it('should annotate disabled extensions', () => {
|
|
200
|
+
it('should annotate disabled extensions', async () => {
|
|
172
201
|
createExtension({
|
|
173
202
|
extensionsDir: userExtensionsDir,
|
|
174
203
|
name: 'disabled-extension',
|
|
@@ -179,15 +208,16 @@ describe('extension tests', () => {
|
|
|
179
208
|
name: 'enabled-extension',
|
|
180
209
|
version: '2.0.0',
|
|
181
210
|
});
|
|
182
|
-
extensionManager.
|
|
183
|
-
|
|
211
|
+
await extensionManager.loadExtensions();
|
|
212
|
+
await extensionManager.disableExtension('disabled-extension', SettingScope.User);
|
|
213
|
+
const extensions = extensionManager.getExtensions();
|
|
184
214
|
expect(extensions).toHaveLength(2);
|
|
185
215
|
expect(extensions[0].name).toBe('disabled-extension');
|
|
186
216
|
expect(extensions[0].isActive).toBe(false);
|
|
187
217
|
expect(extensions[1].name).toBe('enabled-extension');
|
|
188
218
|
expect(extensions[1].isActive).toBe(true);
|
|
189
219
|
});
|
|
190
|
-
it('should hydrate variables', () => {
|
|
220
|
+
it('should hydrate variables', async () => {
|
|
191
221
|
createExtension({
|
|
192
222
|
extensionsDir: userExtensionsDir,
|
|
193
223
|
name: 'test-extension',
|
|
@@ -200,7 +230,7 @@ describe('extension tests', () => {
|
|
|
200
230
|
},
|
|
201
231
|
},
|
|
202
232
|
});
|
|
203
|
-
const extensions = extensionManager.loadExtensions();
|
|
233
|
+
const extensions = await extensionManager.loadExtensions();
|
|
204
234
|
expect(extensions).toHaveLength(1);
|
|
205
235
|
const expectedCwd = path.join(userExtensionsDir, 'test-extension', 'server');
|
|
206
236
|
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd);
|
|
@@ -213,12 +243,13 @@ describe('extension tests', () => {
|
|
|
213
243
|
contextFileName: 'context.md',
|
|
214
244
|
});
|
|
215
245
|
fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
|
|
216
|
-
|
|
246
|
+
await extensionManager.loadExtensions();
|
|
247
|
+
const extension = await extensionManager.installOrUpdateExtension({
|
|
217
248
|
source: sourceExtDir,
|
|
218
249
|
type: 'link',
|
|
219
250
|
});
|
|
220
|
-
expect(
|
|
221
|
-
const extensions = extensionManager.
|
|
251
|
+
expect(extension.name).toEqual('my-linked-extension');
|
|
252
|
+
const extensions = extensionManager.getExtensions();
|
|
222
253
|
expect(extensions).toHaveLength(1);
|
|
223
254
|
const linkedExt = extensions[0];
|
|
224
255
|
expect(linkedExt.name).toBe('my-linked-extension');
|
|
@@ -244,18 +275,19 @@ describe('extension tests', () => {
|
|
|
244
275
|
},
|
|
245
276
|
},
|
|
246
277
|
});
|
|
278
|
+
await extensionManager.loadExtensions();
|
|
247
279
|
await extensionManager.installOrUpdateExtension({
|
|
248
280
|
source: sourceExtDir,
|
|
249
281
|
type: 'link',
|
|
250
282
|
});
|
|
251
|
-
const extensions = extensionManager.
|
|
283
|
+
const extensions = extensionManager.getExtensions();
|
|
252
284
|
expect(extensions).toHaveLength(1);
|
|
253
285
|
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(path.join(sourceExtDir, 'server'));
|
|
254
286
|
expect(extensions[0].mcpServers?.['test-server'].args).toEqual([
|
|
255
287
|
path.join(sourceExtDir, 'server', 'index.js'),
|
|
256
288
|
]);
|
|
257
289
|
});
|
|
258
|
-
it('should resolve environment variables in extension configuration', () => {
|
|
290
|
+
it('should resolve environment variables in extension configuration', async () => {
|
|
259
291
|
process.env['TEST_API_KEY'] = 'test-api-key-123';
|
|
260
292
|
process.env['TEST_DB_URL'] = 'postgresql://localhost:5432/testdb';
|
|
261
293
|
try {
|
|
@@ -281,7 +313,7 @@ describe('extension tests', () => {
|
|
|
281
313
|
},
|
|
282
314
|
};
|
|
283
315
|
fs.writeFileSync(configPath, JSON.stringify(extensionConfig));
|
|
284
|
-
const extensions = extensionManager.loadExtensions();
|
|
316
|
+
const extensions = await extensionManager.loadExtensions();
|
|
285
317
|
expect(extensions).toHaveLength(1);
|
|
286
318
|
const extension = extensions[0];
|
|
287
319
|
expect(extension.name).toBe('test-extension');
|
|
@@ -298,7 +330,7 @@ describe('extension tests', () => {
|
|
|
298
330
|
delete process.env['TEST_DB_URL'];
|
|
299
331
|
}
|
|
300
332
|
});
|
|
301
|
-
it('should resolve environment variables from an extension .env file', () => {
|
|
333
|
+
it('should resolve environment variables from an extension .env file', async () => {
|
|
302
334
|
const extDir = createExtension({
|
|
303
335
|
extensionsDir: userExtensionsDir,
|
|
304
336
|
name: 'test-extension',
|
|
@@ -313,10 +345,17 @@ describe('extension tests', () => {
|
|
|
313
345
|
},
|
|
314
346
|
},
|
|
315
347
|
},
|
|
348
|
+
settings: [
|
|
349
|
+
{
|
|
350
|
+
name: 'My API Key',
|
|
351
|
+
description: 'API key for testing.',
|
|
352
|
+
envVar: 'MY_API_KEY',
|
|
353
|
+
},
|
|
354
|
+
],
|
|
316
355
|
});
|
|
317
356
|
const envFilePath = path.join(extDir, '.env');
|
|
318
357
|
fs.writeFileSync(envFilePath, 'MY_API_KEY=test-key-from-file\n');
|
|
319
|
-
const extensions = extensionManager.loadExtensions();
|
|
358
|
+
const extensions = await extensionManager.loadExtensions();
|
|
320
359
|
expect(extensions).toHaveLength(1);
|
|
321
360
|
const extension = extensions[0];
|
|
322
361
|
const serverConfig = extension.mcpServers['test-server'];
|
|
@@ -324,7 +363,7 @@ describe('extension tests', () => {
|
|
|
324
363
|
expect(serverConfig.env['API_KEY']).toBe('test-key-from-file');
|
|
325
364
|
expect(serverConfig.env['STATIC_VALUE']).toBe('no-substitution');
|
|
326
365
|
});
|
|
327
|
-
it('should handle missing environment variables gracefully', () => {
|
|
366
|
+
it('should handle missing environment variables gracefully', async () => {
|
|
328
367
|
const userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
329
368
|
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
|
330
369
|
const extDir = path.join(userExtensionsDir, 'test-extension');
|
|
@@ -344,7 +383,7 @@ describe('extension tests', () => {
|
|
|
344
383
|
},
|
|
345
384
|
};
|
|
346
385
|
fs.writeFileSync(path.join(extDir, EXTENSIONS_CONFIG_FILENAME), JSON.stringify(extensionConfig));
|
|
347
|
-
const extensions = extensionManager.loadExtensions();
|
|
386
|
+
const extensions = await extensionManager.loadExtensions();
|
|
348
387
|
expect(extensions).toHaveLength(1);
|
|
349
388
|
const extension = extensions[0];
|
|
350
389
|
const serverConfig = extension.mcpServers['test-server'];
|
|
@@ -352,7 +391,7 @@ describe('extension tests', () => {
|
|
|
352
391
|
expect(serverConfig.env['MISSING_VAR']).toBe('$UNDEFINED_ENV_VAR');
|
|
353
392
|
expect(serverConfig.env['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
|
|
354
393
|
});
|
|
355
|
-
it('should skip extensions with invalid JSON and log a warning', () => {
|
|
394
|
+
it('should skip extensions with invalid JSON and log a warning', async () => {
|
|
356
395
|
const consoleSpy = vi
|
|
357
396
|
.spyOn(console, 'error')
|
|
358
397
|
.mockImplementation(() => { });
|
|
@@ -367,13 +406,13 @@ describe('extension tests', () => {
|
|
|
367
406
|
fs.mkdirSync(badExtDir);
|
|
368
407
|
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
369
408
|
fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
|
|
370
|
-
const extensions = extensionManager.loadExtensions();
|
|
409
|
+
const extensions = await extensionManager.loadExtensions();
|
|
371
410
|
expect(extensions).toHaveLength(1);
|
|
372
411
|
expect(extensions[0].name).toBe('good-ext');
|
|
373
412
|
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
|
|
374
413
|
consoleSpy.mockRestore();
|
|
375
414
|
});
|
|
376
|
-
it('should skip extensions with missing name and log a warning', () => {
|
|
415
|
+
it('should skip extensions with missing name and log a warning', async () => {
|
|
377
416
|
const consoleSpy = vi
|
|
378
417
|
.spyOn(console, 'error')
|
|
379
418
|
.mockImplementation(() => { });
|
|
@@ -388,13 +427,13 @@ describe('extension tests', () => {
|
|
|
388
427
|
fs.mkdirSync(badExtDir);
|
|
389
428
|
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
390
429
|
fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
|
|
391
|
-
const extensions = extensionManager.loadExtensions();
|
|
430
|
+
const extensions = await extensionManager.loadExtensions();
|
|
392
431
|
expect(extensions).toHaveLength(1);
|
|
393
432
|
expect(extensions[0].name).toBe('good-ext');
|
|
394
433
|
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
|
|
395
434
|
consoleSpy.mockRestore();
|
|
396
435
|
});
|
|
397
|
-
it('should filter trust out of mcp servers', () => {
|
|
436
|
+
it('should filter trust out of mcp servers', async () => {
|
|
398
437
|
createExtension({
|
|
399
438
|
extensionsDir: userExtensionsDir,
|
|
400
439
|
name: 'test-extension',
|
|
@@ -407,27 +446,28 @@ describe('extension tests', () => {
|
|
|
407
446
|
},
|
|
408
447
|
},
|
|
409
448
|
});
|
|
410
|
-
const extensions = extensionManager.loadExtensions();
|
|
449
|
+
const extensions = await extensionManager.loadExtensions();
|
|
411
450
|
expect(extensions).toHaveLength(1);
|
|
412
451
|
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
|
413
452
|
});
|
|
414
|
-
it('should throw an error for invalid extension names', () => {
|
|
453
|
+
it('should throw an error for invalid extension names', async () => {
|
|
415
454
|
const consoleSpy = vi
|
|
416
455
|
.spyOn(console, 'error')
|
|
417
456
|
.mockImplementation(() => { });
|
|
418
|
-
|
|
457
|
+
createExtension({
|
|
419
458
|
extensionsDir: userExtensionsDir,
|
|
420
459
|
name: 'bad_name',
|
|
421
460
|
version: '1.0.0',
|
|
422
461
|
});
|
|
423
|
-
const
|
|
424
|
-
|
|
462
|
+
const extensions = await extensionManager.loadExtensions();
|
|
463
|
+
const extension = extensions.find((e) => e.name === 'bad_name');
|
|
464
|
+
expect(extension).toBeUndefined();
|
|
425
465
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid extension name: "bad_name"'));
|
|
426
466
|
consoleSpy.mockRestore();
|
|
427
467
|
});
|
|
428
468
|
describe('id generation', () => {
|
|
429
|
-
it('should generate id from source for non-github git urls', () => {
|
|
430
|
-
|
|
469
|
+
it('should generate id from source for non-github git urls', async () => {
|
|
470
|
+
createExtension({
|
|
431
471
|
extensionsDir: userExtensionsDir,
|
|
432
472
|
name: 'my-ext',
|
|
433
473
|
version: '1.0.0',
|
|
@@ -436,11 +476,12 @@ describe('extension tests', () => {
|
|
|
436
476
|
source: 'http://somehost.com/foo/bar',
|
|
437
477
|
},
|
|
438
478
|
});
|
|
439
|
-
const
|
|
479
|
+
const extensions = await extensionManager.loadExtensions();
|
|
480
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
440
481
|
expect(extension?.id).toBe(hashValue('http://somehost.com/foo/bar'));
|
|
441
482
|
});
|
|
442
|
-
it('should generate id from owner/repo for github http urls', () => {
|
|
443
|
-
|
|
483
|
+
it('should generate id from owner/repo for github http urls', async () => {
|
|
484
|
+
createExtension({
|
|
444
485
|
extensionsDir: userExtensionsDir,
|
|
445
486
|
name: 'my-ext',
|
|
446
487
|
version: '1.0.0',
|
|
@@ -449,11 +490,12 @@ describe('extension tests', () => {
|
|
|
449
490
|
source: 'http://github.com/foo/bar',
|
|
450
491
|
},
|
|
451
492
|
});
|
|
452
|
-
const
|
|
493
|
+
const extensions = await extensionManager.loadExtensions();
|
|
494
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
453
495
|
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
|
454
496
|
});
|
|
455
|
-
it('should generate id from owner/repo for github ssh urls', () => {
|
|
456
|
-
|
|
497
|
+
it('should generate id from owner/repo for github ssh urls', async () => {
|
|
498
|
+
createExtension({
|
|
457
499
|
extensionsDir: userExtensionsDir,
|
|
458
500
|
name: 'my-ext',
|
|
459
501
|
version: '1.0.0',
|
|
@@ -462,11 +504,12 @@ describe('extension tests', () => {
|
|
|
462
504
|
source: 'git@github.com:foo/bar',
|
|
463
505
|
},
|
|
464
506
|
});
|
|
465
|
-
const
|
|
507
|
+
const extensions = await extensionManager.loadExtensions();
|
|
508
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
466
509
|
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
|
467
510
|
});
|
|
468
|
-
it('should generate id from source for github-release extension', () => {
|
|
469
|
-
|
|
511
|
+
it('should generate id from source for github-release extension', async () => {
|
|
512
|
+
createExtension({
|
|
470
513
|
extensionsDir: userExtensionsDir,
|
|
471
514
|
name: 'my-ext',
|
|
472
515
|
version: '1.0.0',
|
|
@@ -475,11 +518,12 @@ describe('extension tests', () => {
|
|
|
475
518
|
source: 'https://github.com/foo/bar',
|
|
476
519
|
},
|
|
477
520
|
});
|
|
478
|
-
const
|
|
521
|
+
const extensions = await extensionManager.loadExtensions();
|
|
522
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
479
523
|
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
|
480
524
|
});
|
|
481
|
-
it('should generate id from the original source for local extension', () => {
|
|
482
|
-
|
|
525
|
+
it('should generate id from the original source for local extension', async () => {
|
|
526
|
+
createExtension({
|
|
483
527
|
extensionsDir: userExtensionsDir,
|
|
484
528
|
name: 'local-ext-name',
|
|
485
529
|
version: '1.0.0',
|
|
@@ -488,7 +532,8 @@ describe('extension tests', () => {
|
|
|
488
532
|
source: '/some/path',
|
|
489
533
|
},
|
|
490
534
|
});
|
|
491
|
-
const
|
|
535
|
+
const extensions = await extensionManager.loadExtensions();
|
|
536
|
+
const extension = extensions.find((e) => e.name === 'local-ext-name');
|
|
492
537
|
expect(extension?.id).toBe(hashValue('/some/path'));
|
|
493
538
|
});
|
|
494
539
|
it('should generate id from the original source for linked extensions', async () => {
|
|
@@ -498,20 +543,24 @@ describe('extension tests', () => {
|
|
|
498
543
|
name: 'link-ext-name',
|
|
499
544
|
version: '1.0.0',
|
|
500
545
|
});
|
|
501
|
-
|
|
546
|
+
await extensionManager.loadExtensions();
|
|
547
|
+
await extensionManager.installOrUpdateExtension({
|
|
502
548
|
type: 'link',
|
|
503
549
|
source: actualExtensionDir,
|
|
504
550
|
});
|
|
505
|
-
const extension = extensionManager
|
|
551
|
+
const extension = extensionManager
|
|
552
|
+
.getExtensions()
|
|
553
|
+
.find((e) => e.name === 'link-ext-name');
|
|
506
554
|
expect(extension?.id).toBe(hashValue(actualExtensionDir));
|
|
507
555
|
});
|
|
508
|
-
it('should generate id from name for extension with no install metadata', () => {
|
|
509
|
-
|
|
556
|
+
it('should generate id from name for extension with no install metadata', async () => {
|
|
557
|
+
createExtension({
|
|
510
558
|
extensionsDir: userExtensionsDir,
|
|
511
559
|
name: 'no-meta-name',
|
|
512
560
|
version: '1.0.0',
|
|
513
561
|
});
|
|
514
|
-
const
|
|
562
|
+
const extensions = await extensionManager.loadExtensions();
|
|
563
|
+
const extension = extensions.find((e) => e.name === 'no-meta-name');
|
|
515
564
|
expect(extension?.id).toBe(hashValue('no-meta-name'));
|
|
516
565
|
});
|
|
517
566
|
});
|
|
@@ -525,6 +574,7 @@ describe('extension tests', () => {
|
|
|
525
574
|
});
|
|
526
575
|
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
|
527
576
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
577
|
+
await extensionManager.loadExtensions();
|
|
528
578
|
await extensionManager.installOrUpdateExtension({
|
|
529
579
|
source: sourceExtDir,
|
|
530
580
|
type: 'local',
|
|
@@ -544,6 +594,7 @@ describe('extension tests', () => {
|
|
|
544
594
|
name: 'my-local-extension',
|
|
545
595
|
version: '1.0.0',
|
|
546
596
|
});
|
|
597
|
+
await extensionManager.loadExtensions();
|
|
547
598
|
await extensionManager.installOrUpdateExtension({
|
|
548
599
|
source: sourceExtDir,
|
|
549
600
|
type: 'local',
|
|
@@ -605,6 +656,7 @@ describe('extension tests', () => {
|
|
|
605
656
|
failureReason: 'no release data',
|
|
606
657
|
type: 'github-release',
|
|
607
658
|
});
|
|
659
|
+
await extensionManager.loadExtensions();
|
|
608
660
|
await extensionManager.installOrUpdateExtension({
|
|
609
661
|
source: gitUrl,
|
|
610
662
|
type: 'git',
|
|
@@ -626,6 +678,7 @@ describe('extension tests', () => {
|
|
|
626
678
|
const targetExtDir = path.join(userExtensionsDir, 'my-linked-extension');
|
|
627
679
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
628
680
|
const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
681
|
+
await extensionManager.loadExtensions();
|
|
629
682
|
await extensionManager.installOrUpdateExtension({
|
|
630
683
|
source: sourceExtDir,
|
|
631
684
|
type: 'link',
|
|
@@ -648,6 +701,7 @@ describe('extension tests', () => {
|
|
|
648
701
|
name: 'my-local-extension',
|
|
649
702
|
version: '1.1.0',
|
|
650
703
|
});
|
|
704
|
+
await extensionManager.loadExtensions();
|
|
651
705
|
if (isUpdate) {
|
|
652
706
|
await extensionManager.installOrUpdateExtension({
|
|
653
707
|
source: sourceExtDir,
|
|
@@ -709,10 +763,13 @@ describe('extension tests', () => {
|
|
|
709
763
|
},
|
|
710
764
|
},
|
|
711
765
|
});
|
|
766
|
+
await extensionManager.loadExtensions();
|
|
712
767
|
await expect(extensionManager.installOrUpdateExtension({
|
|
713
768
|
source: sourceExtDir,
|
|
714
769
|
type: 'local',
|
|
715
|
-
})).resolves.
|
|
770
|
+
})).resolves.toMatchObject({
|
|
771
|
+
name: 'my-local-extension',
|
|
772
|
+
});
|
|
716
773
|
expect(mockRequestConsent).toHaveBeenCalledWith(`Installing extension "my-local-extension".
|
|
717
774
|
${INSTALL_WARNING_MESSAGE}
|
|
718
775
|
This extension will run the following MCP servers:
|
|
@@ -731,10 +788,11 @@ This extension will run the following MCP servers:
|
|
|
731
788
|
},
|
|
732
789
|
},
|
|
733
790
|
});
|
|
791
|
+
await extensionManager.loadExtensions();
|
|
734
792
|
await expect(extensionManager.installOrUpdateExtension({
|
|
735
793
|
source: sourceExtDir,
|
|
736
794
|
type: 'local',
|
|
737
|
-
})).resolves.
|
|
795
|
+
})).resolves.toMatchObject({ name: 'my-local-extension' });
|
|
738
796
|
});
|
|
739
797
|
it('should cancel installation if user declines prompt for local extension with mcp servers', async () => {
|
|
740
798
|
const sourceExtDir = createExtension({
|
|
@@ -749,6 +807,7 @@ This extension will run the following MCP servers:
|
|
|
749
807
|
},
|
|
750
808
|
});
|
|
751
809
|
mockRequestConsent.mockResolvedValue(false);
|
|
810
|
+
await extensionManager.loadExtensions();
|
|
752
811
|
await expect(extensionManager.installOrUpdateExtension({
|
|
753
812
|
source: sourceExtDir,
|
|
754
813
|
type: 'local',
|
|
@@ -762,6 +821,7 @@ This extension will run the following MCP servers:
|
|
|
762
821
|
});
|
|
763
822
|
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
|
764
823
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
824
|
+
await extensionManager.loadExtensions();
|
|
765
825
|
await extensionManager.installOrUpdateExtension({
|
|
766
826
|
source: sourceExtDir,
|
|
767
827
|
type: 'local',
|
|
@@ -789,6 +849,7 @@ This extension will run the following MCP servers:
|
|
|
789
849
|
},
|
|
790
850
|
},
|
|
791
851
|
});
|
|
852
|
+
await extensionManager.loadExtensions();
|
|
792
853
|
// Install it with hard coded consent first.
|
|
793
854
|
await extensionManager.installOrUpdateExtension({
|
|
794
855
|
source: sourceExtDir,
|
|
@@ -798,7 +859,7 @@ This extension will run the following MCP servers:
|
|
|
798
859
|
// Now update it without changing anything.
|
|
799
860
|
await expect(extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
|
|
800
861
|
// Provide its own existing config as the previous config.
|
|
801
|
-
await extensionManager.loadExtensionConfig(sourceExtDir))).resolves.
|
|
862
|
+
await extensionManager.loadExtensionConfig(sourceExtDir))).resolves.toMatchObject({ name: 'my-local-extension' });
|
|
802
863
|
// Still only called once
|
|
803
864
|
expect(mockRequestConsent).toHaveBeenCalledOnce();
|
|
804
865
|
});
|
|
@@ -815,6 +876,7 @@ This extension will run the following MCP servers:
|
|
|
815
876
|
},
|
|
816
877
|
],
|
|
817
878
|
});
|
|
879
|
+
await extensionManager.loadExtensions();
|
|
818
880
|
await extensionManager.installOrUpdateExtension({
|
|
819
881
|
source: sourceExtDir,
|
|
820
882
|
type: 'local',
|
|
@@ -838,8 +900,9 @@ This extension will run the following MCP servers:
|
|
|
838
900
|
workspaceDir: tempWorkspaceDir,
|
|
839
901
|
requestConsent: mockRequestConsent,
|
|
840
902
|
requestSetting: null,
|
|
841
|
-
|
|
903
|
+
settings: loadSettings(tempWorkspaceDir).merged,
|
|
842
904
|
});
|
|
905
|
+
await extensionManager.loadExtensions();
|
|
843
906
|
await extensionManager.installOrUpdateExtension({
|
|
844
907
|
source: sourceExtDir,
|
|
845
908
|
type: 'local',
|
|
@@ -860,6 +923,7 @@ This extension will run the following MCP servers:
|
|
|
860
923
|
],
|
|
861
924
|
});
|
|
862
925
|
mockPromptForSettings.mockResolvedValueOnce('old-api-key');
|
|
926
|
+
await extensionManager.loadExtensions();
|
|
863
927
|
// Install it so it exists in the userExtensionsDir
|
|
864
928
|
await extensionManager.installOrUpdateExtension({
|
|
865
929
|
source: oldSourceExtDir,
|
|
@@ -913,6 +977,7 @@ This extension will run the following MCP servers:
|
|
|
913
977
|
},
|
|
914
978
|
],
|
|
915
979
|
});
|
|
980
|
+
await extensionManager.loadExtensions();
|
|
916
981
|
await extensionManager.installOrUpdateExtension({
|
|
917
982
|
source: oldSourceExtDir,
|
|
918
983
|
type: 'local',
|
|
@@ -977,6 +1042,7 @@ This extension will run the following MCP servers:
|
|
|
977
1042
|
version: '1.0.0',
|
|
978
1043
|
});
|
|
979
1044
|
vi.spyOn(ExtensionStorage, 'createTmpDir').mockResolvedValue(join(tempDir, extensionName));
|
|
1045
|
+
await extensionManager.loadExtensions();
|
|
980
1046
|
await extensionManager.installOrUpdateExtension({
|
|
981
1047
|
source: gitUrl,
|
|
982
1048
|
type: 'github-release',
|
|
@@ -998,6 +1064,7 @@ This extension will run the following MCP servers:
|
|
|
998
1064
|
errorMessage: 'download failed',
|
|
999
1065
|
type: 'github-release',
|
|
1000
1066
|
});
|
|
1067
|
+
await extensionManager.loadExtensions();
|
|
1001
1068
|
await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
|
|
1002
1069
|
// It gets called once to ask for a git clone, and once to consent to
|
|
1003
1070
|
// the actual extension features.
|
|
@@ -1015,6 +1082,7 @@ This extension will run the following MCP servers:
|
|
|
1015
1082
|
type: 'github-release',
|
|
1016
1083
|
});
|
|
1017
1084
|
mockRequestConsent.mockResolvedValue(false);
|
|
1085
|
+
await extensionManager.loadExtensions();
|
|
1018
1086
|
await expect(extensionManager.installOrUpdateExtension({
|
|
1019
1087
|
source: gitUrl,
|
|
1020
1088
|
type: 'github-release',
|
|
@@ -1028,6 +1096,7 @@ This extension will run the following MCP servers:
|
|
|
1028
1096
|
failureReason: 'no release data',
|
|
1029
1097
|
type: 'github-release',
|
|
1030
1098
|
});
|
|
1099
|
+
await extensionManager.loadExtensions();
|
|
1031
1100
|
await extensionManager.installOrUpdateExtension({
|
|
1032
1101
|
source: gitUrl,
|
|
1033
1102
|
type: 'git',
|
|
@@ -1047,6 +1116,7 @@ This extension will run the following MCP servers:
|
|
|
1047
1116
|
errorMessage: 'No release data found',
|
|
1048
1117
|
type: 'github-release',
|
|
1049
1118
|
});
|
|
1119
|
+
await extensionManager.loadExtensions();
|
|
1050
1120
|
await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
|
|
1051
1121
|
expect(mockRequestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
1052
1122
|
expect(mockGit.clone).toHaveBeenCalled();
|
|
@@ -1060,6 +1130,7 @@ This extension will run the following MCP servers:
|
|
|
1060
1130
|
name: 'my-local-extension',
|
|
1061
1131
|
version: '1.0.0',
|
|
1062
1132
|
});
|
|
1133
|
+
await extensionManager.loadExtensions();
|
|
1063
1134
|
await extensionManager.uninstallExtension('my-local-extension', false);
|
|
1064
1135
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
1065
1136
|
});
|
|
@@ -1074,12 +1145,14 @@ This extension will run the following MCP servers:
|
|
|
1074
1145
|
name: 'other-extension',
|
|
1075
1146
|
version: '1.0.0',
|
|
1076
1147
|
});
|
|
1148
|
+
await extensionManager.loadExtensions();
|
|
1077
1149
|
await extensionManager.uninstallExtension('my-local-extension', false);
|
|
1078
1150
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
1079
|
-
expect(extensionManager.
|
|
1151
|
+
expect(extensionManager.getExtensions()).toHaveLength(1);
|
|
1080
1152
|
expect(fs.existsSync(otherExtDir)).toBe(true);
|
|
1081
1153
|
});
|
|
1082
1154
|
it('should throw an error if the extension does not exist', async () => {
|
|
1155
|
+
await extensionManager.loadExtensions();
|
|
1083
1156
|
await expect(extensionManager.uninstallExtension('nonexistent-extension', false)).rejects.toThrow('Extension not found.');
|
|
1084
1157
|
});
|
|
1085
1158
|
describe.each([true, false])('with isUpdate: %s', (isUpdate) => {
|
|
@@ -1093,6 +1166,7 @@ This extension will run the following MCP servers:
|
|
|
1093
1166
|
type: 'local',
|
|
1094
1167
|
},
|
|
1095
1168
|
});
|
|
1169
|
+
await extensionManager.loadExtensions();
|
|
1096
1170
|
await extensionManager.uninstallExtension('my-local-extension', isUpdate);
|
|
1097
1171
|
if (isUpdate) {
|
|
1098
1172
|
expect(mockLogExtensionUninstall).not.toHaveBeenCalled();
|
|
@@ -1111,6 +1185,7 @@ This extension will run the following MCP servers:
|
|
|
1111
1185
|
});
|
|
1112
1186
|
const enablementManager = new ExtensionEnablementManager();
|
|
1113
1187
|
enablementManager.enable('test-extension', true, '/some/scope');
|
|
1188
|
+
await extensionManager.loadExtensions();
|
|
1114
1189
|
await extensionManager.uninstallExtension('test-extension', isUpdate);
|
|
1115
1190
|
const config = enablementManager.readConfig()['test-extension'];
|
|
1116
1191
|
if (isUpdate) {
|
|
@@ -1133,6 +1208,7 @@ This extension will run the following MCP servers:
|
|
|
1133
1208
|
type: 'git',
|
|
1134
1209
|
},
|
|
1135
1210
|
});
|
|
1211
|
+
await extensionManager.loadExtensions();
|
|
1136
1212
|
await extensionManager.uninstallExtension(gitUrl, false);
|
|
1137
1213
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
1138
1214
|
expect(mockLogExtensionUninstall).toHaveBeenCalled();
|
|
@@ -1145,28 +1221,31 @@ This extension will run the following MCP servers:
|
|
|
1145
1221
|
version: '1.0.0',
|
|
1146
1222
|
// No installMetadata provided
|
|
1147
1223
|
});
|
|
1224
|
+
await extensionManager.loadExtensions();
|
|
1148
1225
|
await expect(extensionManager.uninstallExtension('https://github.com/google/no-metadata-extension', false)).rejects.toThrow('Extension not found.');
|
|
1149
1226
|
});
|
|
1150
1227
|
});
|
|
1151
1228
|
describe('disableExtension', () => {
|
|
1152
|
-
it('should disable an extension at the user scope', () => {
|
|
1229
|
+
it('should disable an extension at the user scope', async () => {
|
|
1153
1230
|
createExtension({
|
|
1154
1231
|
extensionsDir: userExtensionsDir,
|
|
1155
1232
|
name: 'my-extension',
|
|
1156
1233
|
version: '1.0.0',
|
|
1157
1234
|
});
|
|
1235
|
+
await extensionManager.loadExtensions();
|
|
1158
1236
|
extensionManager.disableExtension('my-extension', SettingScope.User);
|
|
1159
1237
|
expect(isEnabled({
|
|
1160
1238
|
name: 'my-extension',
|
|
1161
1239
|
enabledForPath: tempWorkspaceDir,
|
|
1162
1240
|
})).toBe(false);
|
|
1163
1241
|
});
|
|
1164
|
-
it('should disable an extension at the workspace scope', () => {
|
|
1242
|
+
it('should disable an extension at the workspace scope', async () => {
|
|
1165
1243
|
createExtension({
|
|
1166
1244
|
extensionsDir: userExtensionsDir,
|
|
1167
1245
|
name: 'my-extension',
|
|
1168
1246
|
version: '1.0.0',
|
|
1169
1247
|
});
|
|
1248
|
+
await extensionManager.loadExtensions();
|
|
1170
1249
|
extensionManager.disableExtension('my-extension', SettingScope.Workspace);
|
|
1171
1250
|
expect(isEnabled({
|
|
1172
1251
|
name: 'my-extension',
|
|
@@ -1177,12 +1256,13 @@ This extension will run the following MCP servers:
|
|
|
1177
1256
|
enabledForPath: tempWorkspaceDir,
|
|
1178
1257
|
})).toBe(false);
|
|
1179
1258
|
});
|
|
1180
|
-
it('should handle disabling the same extension twice', () => {
|
|
1259
|
+
it('should handle disabling the same extension twice', async () => {
|
|
1181
1260
|
createExtension({
|
|
1182
1261
|
extensionsDir: userExtensionsDir,
|
|
1183
1262
|
name: 'my-extension',
|
|
1184
1263
|
version: '1.0.0',
|
|
1185
1264
|
});
|
|
1265
|
+
await extensionManager.loadExtensions();
|
|
1186
1266
|
extensionManager.disableExtension('my-extension', SettingScope.User);
|
|
1187
1267
|
extensionManager.disableExtension('my-extension', SettingScope.User);
|
|
1188
1268
|
expect(isEnabled({
|
|
@@ -1190,10 +1270,10 @@ This extension will run the following MCP servers:
|
|
|
1190
1270
|
enabledForPath: tempWorkspaceDir,
|
|
1191
1271
|
})).toBe(false);
|
|
1192
1272
|
});
|
|
1193
|
-
it('should throw an error if you request system scope', () => {
|
|
1194
|
-
expect(() => extensionManager.disableExtension('my-extension', SettingScope.System)).toThrow('System and SystemDefaults scopes are not supported.');
|
|
1273
|
+
it('should throw an error if you request system scope', async () => {
|
|
1274
|
+
await expect(async () => await extensionManager.disableExtension('my-extension', SettingScope.System)).rejects.toThrow('System and SystemDefaults scopes are not supported.');
|
|
1195
1275
|
});
|
|
1196
|
-
it('should log a disable event', () => {
|
|
1276
|
+
it('should log a disable event', async () => {
|
|
1197
1277
|
createExtension({
|
|
1198
1278
|
extensionsDir: userExtensionsDir,
|
|
1199
1279
|
name: 'ext1',
|
|
@@ -1203,6 +1283,7 @@ This extension will run the following MCP servers:
|
|
|
1203
1283
|
type: 'local',
|
|
1204
1284
|
},
|
|
1205
1285
|
});
|
|
1286
|
+
await extensionManager.loadExtensions();
|
|
1206
1287
|
extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
|
1207
1288
|
expect(mockLogExtensionDisable).toHaveBeenCalled();
|
|
1208
1289
|
expect(ExtensionDisableEvent).toHaveBeenCalledWith(hashValue('ext1'), hashValue(userExtensionsDir), SettingScope.Workspace);
|
|
@@ -1213,38 +1294,40 @@ This extension will run the following MCP servers:
|
|
|
1213
1294
|
vi.restoreAllMocks();
|
|
1214
1295
|
});
|
|
1215
1296
|
const getActiveExtensions = () => {
|
|
1216
|
-
const extensions = extensionManager.
|
|
1297
|
+
const extensions = extensionManager.getExtensions();
|
|
1217
1298
|
return extensions.filter((e) => e.isActive);
|
|
1218
1299
|
};
|
|
1219
|
-
it('should enable an extension at the user scope', () => {
|
|
1300
|
+
it('should enable an extension at the user scope', async () => {
|
|
1220
1301
|
createExtension({
|
|
1221
1302
|
extensionsDir: userExtensionsDir,
|
|
1222
1303
|
name: 'ext1',
|
|
1223
1304
|
version: '1.0.0',
|
|
1224
1305
|
});
|
|
1306
|
+
await extensionManager.loadExtensions();
|
|
1225
1307
|
extensionManager.disableExtension('ext1', SettingScope.User);
|
|
1226
1308
|
let activeExtensions = getActiveExtensions();
|
|
1227
1309
|
expect(activeExtensions).toHaveLength(0);
|
|
1228
|
-
extensionManager.enableExtension('ext1', SettingScope.User);
|
|
1229
|
-
activeExtensions = getActiveExtensions();
|
|
1310
|
+
await extensionManager.enableExtension('ext1', SettingScope.User);
|
|
1311
|
+
activeExtensions = await getActiveExtensions();
|
|
1230
1312
|
expect(activeExtensions).toHaveLength(1);
|
|
1231
1313
|
expect(activeExtensions[0].name).toBe('ext1');
|
|
1232
1314
|
});
|
|
1233
|
-
it('should enable an extension at the workspace scope', () => {
|
|
1315
|
+
it('should enable an extension at the workspace scope', async () => {
|
|
1234
1316
|
createExtension({
|
|
1235
1317
|
extensionsDir: userExtensionsDir,
|
|
1236
1318
|
name: 'ext1',
|
|
1237
1319
|
version: '1.0.0',
|
|
1238
1320
|
});
|
|
1321
|
+
await extensionManager.loadExtensions();
|
|
1239
1322
|
extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
|
1240
1323
|
let activeExtensions = getActiveExtensions();
|
|
1241
1324
|
expect(activeExtensions).toHaveLength(0);
|
|
1242
|
-
extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
|
1243
|
-
activeExtensions = getActiveExtensions();
|
|
1325
|
+
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
|
1326
|
+
activeExtensions = await getActiveExtensions();
|
|
1244
1327
|
expect(activeExtensions).toHaveLength(1);
|
|
1245
1328
|
expect(activeExtensions[0].name).toBe('ext1');
|
|
1246
1329
|
});
|
|
1247
|
-
it('should log an enable event', () => {
|
|
1330
|
+
it('should log an enable event', async () => {
|
|
1248
1331
|
createExtension({
|
|
1249
1332
|
extensionsDir: userExtensionsDir,
|
|
1250
1333
|
name: 'ext1',
|
|
@@ -1254,6 +1337,7 @@ This extension will run the following MCP servers:
|
|
|
1254
1337
|
type: 'local',
|
|
1255
1338
|
},
|
|
1256
1339
|
});
|
|
1340
|
+
await extensionManager.loadExtensions();
|
|
1257
1341
|
extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
|
1258
1342
|
extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
|
1259
1343
|
expect(mockLogExtensionEnable).toHaveBeenCalled();
|