@google/gemini-cli 0.12.0-nightly.20251023.c4c0c0d1 → 0.12.0-preview.0

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.
Files changed (248) hide show
  1. package/README.md +7 -5
  2. package/dist/package.json +3 -3
  3. package/dist/src/commands/extensions/disable.d.ts +1 -1
  4. package/dist/src/commands/extensions/disable.js +5 -4
  5. package/dist/src/commands/extensions/disable.js.map +1 -1
  6. package/dist/src/commands/extensions/enable.d.ts +1 -1
  7. package/dist/src/commands/extensions/enable.js +3 -2
  8. package/dist/src/commands/extensions/enable.js.map +1 -1
  9. package/dist/src/commands/extensions/install.js +2 -1
  10. package/dist/src/commands/extensions/install.js.map +1 -1
  11. package/dist/src/commands/extensions/install.test.js +1 -0
  12. package/dist/src/commands/extensions/install.test.js.map +1 -1
  13. package/dist/src/commands/extensions/link.js +2 -1
  14. package/dist/src/commands/extensions/link.js.map +1 -1
  15. package/dist/src/commands/extensions/list.js +2 -2
  16. package/dist/src/commands/extensions/list.js.map +1 -1
  17. package/dist/src/commands/extensions/uninstall.js +2 -1
  18. package/dist/src/commands/extensions/uninstall.js.map +1 -1
  19. package/dist/src/commands/extensions/update.js +2 -2
  20. package/dist/src/commands/extensions/update.js.map +1 -1
  21. package/dist/src/commands/mcp/list.js +2 -2
  22. package/dist/src/commands/mcp/list.js.map +1 -1
  23. package/dist/src/config/config.d.ts +6 -3
  24. package/dist/src/config/config.js +56 -11
  25. package/dist/src/config/config.js.map +1 -1
  26. package/dist/src/config/config.test.js +208 -175
  27. package/dist/src/config/config.test.js.map +1 -1
  28. package/dist/src/config/extension-manager.d.ts +23 -10
  29. package/dist/src/config/extension-manager.js +90 -64
  30. package/dist/src/config/extension-manager.js.map +1 -1
  31. package/dist/src/config/extension.test.js +183 -76
  32. package/dist/src/config/extension.test.js.map +1 -1
  33. package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
  34. package/dist/src/config/extensions/extensionEnablement.js +3 -2
  35. package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
  36. package/dist/src/config/extensions/extensionEnablement.test.js +10 -10
  37. package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
  38. package/dist/src/config/extensions/extensionSettings.d.ts +3 -3
  39. package/dist/src/config/extensions/extensionSettings.js +74 -24
  40. package/dist/src/config/extensions/extensionSettings.js.map +1 -1
  41. package/dist/src/config/extensions/extensionSettings.test.js +145 -24
  42. package/dist/src/config/extensions/extensionSettings.test.js.map +1 -1
  43. package/dist/src/config/extensions/github.js +3 -3
  44. package/dist/src/config/extensions/github.js.map +1 -1
  45. package/dist/src/config/extensions/github.test.js +1 -1
  46. package/dist/src/config/extensions/github.test.js.map +1 -1
  47. package/dist/src/config/extensions/github_fetch.d.ts +1 -1
  48. package/dist/src/config/extensions/github_fetch.js +13 -1
  49. package/dist/src/config/extensions/github_fetch.js.map +1 -1
  50. package/dist/src/config/extensions/github_fetch.test.d.ts +6 -0
  51. package/dist/src/config/extensions/github_fetch.test.js +169 -0
  52. package/dist/src/config/extensions/github_fetch.test.js.map +1 -0
  53. package/dist/src/config/extensions/update.js +7 -6
  54. package/dist/src/config/extensions/update.js.map +1 -1
  55. package/dist/src/config/extensions/update.test.js +54 -30
  56. package/dist/src/config/extensions/update.test.js.map +1 -1
  57. package/dist/src/config/keyBindings.js +1 -1
  58. package/dist/src/config/keyBindings.js.map +1 -1
  59. package/dist/src/config/policies/read-only.toml +56 -0
  60. package/dist/src/config/policies/write.toml +63 -0
  61. package/dist/src/config/policies/yolo.toml +31 -0
  62. package/dist/src/config/policy-engine.integration.test.js +41 -38
  63. package/dist/src/config/policy-engine.integration.test.js.map +1 -1
  64. package/dist/src/config/policy-toml-loader.d.ts +46 -0
  65. package/dist/src/config/policy-toml-loader.js +314 -0
  66. package/dist/src/config/policy-toml-loader.js.map +1 -0
  67. package/dist/src/config/policy-toml-loader.test.d.ts +6 -0
  68. package/dist/src/config/policy-toml-loader.test.js +626 -0
  69. package/dist/src/config/policy-toml-loader.test.js.map +1 -0
  70. package/dist/src/config/policy.d.ts +9 -2
  71. package/dist/src/config/policy.js +139 -110
  72. package/dist/src/config/policy.js.map +1 -1
  73. package/dist/src/config/policy.test.js +780 -82
  74. package/dist/src/config/policy.test.js.map +1 -1
  75. package/dist/src/config/settings.test.js +6 -6
  76. package/dist/src/config/settings.test.js.map +1 -1
  77. package/dist/src/core/initializer.js +2 -1
  78. package/dist/src/core/initializer.js.map +1 -1
  79. package/dist/src/gemini.js +6 -17
  80. package/dist/src/gemini.js.map +1 -1
  81. package/dist/src/gemini.test.js +27 -2
  82. package/dist/src/gemini.test.js.map +1 -1
  83. package/dist/src/generated/git-commit.d.ts +2 -2
  84. package/dist/src/generated/git-commit.js +2 -2
  85. package/dist/src/generated/git-commit.js.map +1 -1
  86. package/dist/src/nonInteractiveCli.js +16 -4
  87. package/dist/src/nonInteractiveCli.js.map +1 -1
  88. package/dist/src/nonInteractiveCli.test.js +67 -12
  89. package/dist/src/nonInteractiveCli.test.js.map +1 -1
  90. package/dist/src/test-utils/render.d.ts +12 -0
  91. package/dist/src/test-utils/render.js +28 -1
  92. package/dist/src/test-utils/render.js.map +1 -1
  93. package/dist/src/test-utils/render.test.d.ts +6 -0
  94. package/dist/src/test-utils/render.test.js +54 -0
  95. package/dist/src/test-utils/render.test.js.map +1 -0
  96. package/dist/src/ui/AppContainer.js +29 -23
  97. package/dist/src/ui/AppContainer.js.map +1 -1
  98. package/dist/src/ui/AppContainer.test.js +8 -0
  99. package/dist/src/ui/AppContainer.test.js.map +1 -1
  100. package/dist/src/ui/commands/directoryCommand.js +1 -1
  101. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  102. package/dist/src/ui/commands/extensionsCommand.js +45 -1
  103. package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
  104. package/dist/src/ui/commands/extensionsCommand.test.js +64 -1
  105. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
  106. package/dist/src/ui/commands/memoryCommand.js +1 -1
  107. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  108. package/dist/src/ui/commands/memoryCommand.test.js +3 -1
  109. package/dist/src/ui/commands/memoryCommand.test.js.map +1 -1
  110. package/dist/src/ui/components/ConsoleSummaryDisplay.js +1 -1
  111. package/dist/src/ui/components/ConsoleSummaryDisplay.js.map +1 -1
  112. package/dist/src/ui/components/DetailedMessagesDisplay.js +1 -1
  113. package/dist/src/ui/components/DetailedMessagesDisplay.js.map +1 -1
  114. package/dist/src/ui/components/FolderTrustDialog.test.js +4 -4
  115. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  116. package/dist/src/ui/components/Footer.js +1 -1
  117. package/dist/src/ui/components/Footer.js.map +1 -1
  118. package/dist/src/ui/components/Footer.test.js +24 -0
  119. package/dist/src/ui/components/Footer.test.js.map +1 -1
  120. package/dist/src/ui/components/Help.test.js +0 -1
  121. package/dist/src/ui/components/Help.test.js.map +1 -1
  122. package/dist/src/ui/components/InputPrompt.test.js +442 -342
  123. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  124. package/dist/src/ui/components/ModelDialog.test.js +5 -5
  125. package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
  126. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +11 -12
  127. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
  128. package/dist/src/ui/components/SettingsDialog.test.js +13 -14
  129. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  130. package/dist/src/ui/components/ThemeDialog.test.js +1 -2
  131. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  132. package/dist/src/ui/components/shared/BaseSelectionList.test.js +11 -12
  133. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
  134. package/dist/src/ui/components/shared/text-buffer.test.js +2 -1
  135. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
  136. package/dist/src/ui/components/views/ExtensionsList.d.ts +1 -1
  137. package/dist/src/ui/components/views/ExtensionsList.js +4 -1
  138. package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
  139. package/dist/src/ui/contexts/KeypressContext.d.ts +3 -2
  140. package/dist/src/ui/contexts/KeypressContext.js +114 -64
  141. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  142. package/dist/src/ui/contexts/KeypressContext.test.js +166 -482
  143. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  144. package/dist/src/ui/contexts/SessionContext.test.js +27 -13
  145. package/dist/src/ui/contexts/SessionContext.test.js.map +1 -1
  146. package/dist/src/ui/hooks/atCommandProcessor.js +2 -2
  147. package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
  148. package/dist/src/ui/hooks/shellCommandProcessor.test.js +18 -2
  149. package/dist/src/ui/hooks/shellCommandProcessor.test.js.map +1 -1
  150. package/dist/src/ui/hooks/slashCommandProcessor.test.js +74 -80
  151. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  152. package/dist/src/ui/hooks/useAtCompletion.test.js +32 -23
  153. package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -1
  154. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +2 -1
  155. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
  156. package/dist/src/ui/hooks/useCommandCompletion.test.js +79 -78
  157. package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -1
  158. package/dist/src/ui/hooks/useConsoleMessages.test.js +26 -9
  159. package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -1
  160. package/dist/src/ui/hooks/useEditorSettings.test.js +40 -34
  161. package/dist/src/ui/hooks/useEditorSettings.test.js.map +1 -1
  162. package/dist/src/ui/hooks/useExtensionUpdates.d.ts +1 -2
  163. package/dist/src/ui/hooks/useExtensionUpdates.js +4 -2
  164. package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
  165. package/dist/src/ui/hooks/useExtensionUpdates.test.js +37 -26
  166. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  167. package/dist/src/ui/hooks/useFlickerDetector.test.js +9 -5
  168. package/dist/src/ui/hooks/useFlickerDetector.test.js.map +1 -1
  169. package/dist/src/ui/hooks/useFocus.test.js +25 -9
  170. package/dist/src/ui/hooks/useFocus.test.js.map +1 -1
  171. package/dist/src/ui/hooks/useFolderTrust.test.js +45 -22
  172. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
  173. package/dist/src/ui/hooks/useGeminiStream.js +56 -19
  174. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  175. package/dist/src/ui/hooks/useGeminiStream.test.js +155 -74
  176. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
  177. package/dist/src/ui/hooks/useGitBranchName.test.js +29 -16
  178. package/dist/src/ui/hooks/useGitBranchName.test.js.map +1 -1
  179. package/dist/src/ui/hooks/useHistoryManager.test.js +2 -1
  180. package/dist/src/ui/hooks/useHistoryManager.test.js.map +1 -1
  181. package/dist/src/ui/hooks/useIdeTrustListener.test.js +24 -7
  182. package/dist/src/ui/hooks/useIdeTrustListener.test.js.map +1 -1
  183. package/dist/src/ui/hooks/useInputHistory.test.js +2 -1
  184. package/dist/src/ui/hooks/useInputHistory.test.js.map +1 -1
  185. package/dist/src/ui/hooks/useInputHistoryStore.test.js +2 -1
  186. package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -1
  187. package/dist/src/ui/hooks/useKeypress.test.js +94 -113
  188. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
  189. package/dist/src/ui/hooks/useLoadingIndicator.test.js +24 -6
  190. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  191. package/dist/src/ui/hooks/useMemoryMonitor.test.js +10 -5
  192. package/dist/src/ui/hooks/useMemoryMonitor.test.js.map +1 -1
  193. package/dist/src/ui/hooks/useMessageQueue.test.js +61 -45
  194. package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
  195. package/dist/src/ui/hooks/useModelCommand.test.js +18 -11
  196. package/dist/src/ui/hooks/useModelCommand.test.js.map +1 -1
  197. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +2 -2
  198. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
  199. package/dist/src/ui/hooks/usePhraseCycler.js +1 -1
  200. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  201. package/dist/src/ui/hooks/usePhraseCycler.test.js +83 -110
  202. package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -1
  203. package/dist/src/ui/hooks/usePrivacySettings.test.js +26 -10
  204. package/dist/src/ui/hooks/usePrivacySettings.test.js.map +1 -1
  205. package/dist/src/ui/hooks/useQuotaAndFallback.js +13 -14
  206. package/dist/src/ui/hooks/useQuotaAndFallback.js.map +1 -1
  207. package/dist/src/ui/hooks/useQuotaAndFallback.test.js +33 -40
  208. package/dist/src/ui/hooks/useQuotaAndFallback.test.js.map +1 -1
  209. package/dist/src/ui/hooks/useReactToolScheduler.d.ts +8 -1
  210. package/dist/src/ui/hooks/useReactToolScheduler.js +37 -26
  211. package/dist/src/ui/hooks/useReactToolScheduler.js.map +1 -1
  212. package/dist/src/ui/hooks/useReactToolScheduler.test.js +1 -1
  213. package/dist/src/ui/hooks/useReactToolScheduler.test.js.map +1 -1
  214. package/dist/src/ui/hooks/useReverseSearchCompletion.test.js +2 -2
  215. package/dist/src/ui/hooks/useReverseSearchCompletion.test.js.map +1 -1
  216. package/dist/src/ui/hooks/useSelectionList.test.js +193 -132
  217. package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
  218. package/dist/src/ui/hooks/useShellHistory.test.js +40 -16
  219. package/dist/src/ui/hooks/useShellHistory.test.js.map +1 -1
  220. package/dist/src/ui/hooks/useSlashCompletion.test.js +54 -49
  221. package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
  222. package/dist/src/ui/hooks/useTimer.test.js +43 -14
  223. package/dist/src/ui/hooks/useTimer.test.js.map +1 -1
  224. package/dist/src/ui/hooks/useToolScheduler.test.js +163 -74
  225. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  226. package/dist/src/ui/hooks/vim.test.js +251 -356
  227. package/dist/src/ui/hooks/vim.test.js.map +1 -1
  228. package/dist/src/ui/keyMatchers.test.js +3 -3
  229. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  230. package/dist/src/ui/utils/textOutput.d.ts +25 -0
  231. package/dist/src/ui/utils/textOutput.js +49 -0
  232. package/dist/src/ui/utils/textOutput.js.map +1 -0
  233. package/dist/src/ui/utils/textOutput.test.d.ts +6 -0
  234. package/dist/src/ui/utils/textOutput.test.js +79 -0
  235. package/dist/src/ui/utils/textOutput.test.js.map +1 -0
  236. package/dist/src/ui/utils/updateCheck.d.ts +7 -1
  237. package/dist/src/ui/utils/updateCheck.js +27 -26
  238. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  239. package/dist/src/ui/utils/updateCheck.test.js +19 -49
  240. package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
  241. package/dist/src/utils/handleAutoUpdate.js +9 -3
  242. package/dist/src/utils/handleAutoUpdate.js.map +1 -1
  243. package/dist/src/zed-integration/zedIntegration.d.ts +2 -2
  244. package/dist/src/zed-integration/zedIntegration.js +9 -16
  245. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  246. package/dist/tsconfig.tsbuildinfo +1 -1
  247. package/package.json +4 -4
  248. package/dist/google-gemini-cli-0.12.0-nightly.20251022.0542de95.tgz +0 -0
@@ -7,7 +7,7 @@ import { vi } from 'vitest';
7
7
  import * as fs from 'node:fs';
8
8
  import * as os from 'node:os';
9
9
  import * as path from 'node:path';
10
- import { ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, } from '@google/gemini-cli-core';
10
+ import { ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, KeychainTokenStorage, } from '@google/gemini-cli-core';
11
11
  import { loadSettings, SettingScope } from './settings.js';
12
12
  import { isWorkspaceTrusted } from './trustedFolders.js';
13
13
  import { createExtension } from '../test-utils/createExtension.js';
@@ -42,11 +42,12 @@ vi.mock('simple-git', () => ({
42
42
  return mockGit;
43
43
  }),
44
44
  }));
45
+ const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
45
46
  vi.mock('os', async (importOriginal) => {
46
47
  const mockedOs = await importOriginal();
47
48
  return {
48
49
  ...mockedOs,
49
- homedir: vi.fn(),
50
+ homedir: mockHomedir,
50
51
  };
51
52
  });
52
53
  vi.mock('./trustedFolders.js', async (importOriginal) => {
@@ -74,6 +75,13 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
74
75
  ExtensionInstallEvent: vi.fn(),
75
76
  ExtensionUninstallEvent: vi.fn(),
76
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
+ })),
77
85
  };
78
86
  });
79
87
  vi.mock('child_process', async (importOriginal) => {
@@ -90,7 +98,29 @@ describe('extension tests', () => {
90
98
  let extensionManager;
91
99
  let mockRequestConsent;
92
100
  let mockPromptForSettings;
101
+ let mockKeychainStorage;
102
+ let keychainData;
93
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);
94
124
  tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-home-'));
95
125
  tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'gemini-cli-test-workspace-'));
96
126
  userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
@@ -109,7 +139,7 @@ describe('extension tests', () => {
109
139
  workspaceDir: tempWorkspaceDir,
110
140
  requestConsent: mockRequestConsent,
111
141
  requestSetting: mockPromptForSettings,
112
- loadedSettings: loadSettings(tempWorkspaceDir),
142
+ settings: loadSettings(tempWorkspaceDir).merged,
113
143
  });
114
144
  });
115
145
  afterEach(() => {
@@ -118,7 +148,7 @@ describe('extension tests', () => {
118
148
  vi.restoreAllMocks();
119
149
  });
120
150
  describe('loadExtensions', () => {
121
- it('should include extension path in loaded extension', () => {
151
+ it('should include extension path in loaded extension', async () => {
122
152
  const extensionDir = path.join(userExtensionsDir, 'test-extension');
123
153
  fs.mkdirSync(extensionDir, { recursive: true });
124
154
  createExtension({
@@ -126,12 +156,12 @@ describe('extension tests', () => {
126
156
  name: 'test-extension',
127
157
  version: '1.0.0',
128
158
  });
129
- const extensions = extensionManager.loadExtensions();
159
+ const extensions = await extensionManager.loadExtensions();
130
160
  expect(extensions).toHaveLength(1);
131
161
  expect(extensions[0].path).toBe(extensionDir);
132
162
  expect(extensions[0].name).toBe('test-extension');
133
163
  });
134
- it('should load context file path when GEMINI.md is present', () => {
164
+ it('should load context file path when GEMINI.md is present', async () => {
135
165
  createExtension({
136
166
  extensionsDir: userExtensionsDir,
137
167
  name: 'ext1',
@@ -143,7 +173,7 @@ describe('extension tests', () => {
143
173
  name: 'ext2',
144
174
  version: '2.0.0',
145
175
  });
146
- const extensions = extensionManager.loadExtensions();
176
+ const extensions = await extensionManager.loadExtensions();
147
177
  expect(extensions).toHaveLength(2);
148
178
  const ext1 = extensions.find((e) => e.name === 'ext1');
149
179
  const ext2 = extensions.find((e) => e.name === 'ext2');
@@ -152,7 +182,7 @@ describe('extension tests', () => {
152
182
  ]);
153
183
  expect(ext2?.contextFiles).toEqual([]);
154
184
  });
155
- it('should load context file path from the extension config', () => {
185
+ it('should load context file path from the extension config', async () => {
156
186
  createExtension({
157
187
  extensionsDir: userExtensionsDir,
158
188
  name: 'ext1',
@@ -160,14 +190,14 @@ describe('extension tests', () => {
160
190
  addContextFile: false,
161
191
  contextFileName: 'my-context-file.md',
162
192
  });
163
- const extensions = extensionManager.loadExtensions();
193
+ const extensions = await extensionManager.loadExtensions();
164
194
  expect(extensions).toHaveLength(1);
165
195
  const ext1 = extensions.find((e) => e.name === 'ext1');
166
196
  expect(ext1?.contextFiles).toEqual([
167
197
  path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
168
198
  ]);
169
199
  });
170
- it('should annotate disabled extensions', () => {
200
+ it('should annotate disabled extensions', async () => {
171
201
  createExtension({
172
202
  extensionsDir: userExtensionsDir,
173
203
  name: 'disabled-extension',
@@ -178,15 +208,16 @@ describe('extension tests', () => {
178
208
  name: 'enabled-extension',
179
209
  version: '2.0.0',
180
210
  });
181
- extensionManager.disableExtension('disabled-extension', SettingScope.User);
182
- const extensions = extensionManager.loadExtensions();
211
+ await extensionManager.loadExtensions();
212
+ await extensionManager.disableExtension('disabled-extension', SettingScope.User);
213
+ const extensions = extensionManager.getExtensions();
183
214
  expect(extensions).toHaveLength(2);
184
215
  expect(extensions[0].name).toBe('disabled-extension');
185
216
  expect(extensions[0].isActive).toBe(false);
186
217
  expect(extensions[1].name).toBe('enabled-extension');
187
218
  expect(extensions[1].isActive).toBe(true);
188
219
  });
189
- it('should hydrate variables', () => {
220
+ it('should hydrate variables', async () => {
190
221
  createExtension({
191
222
  extensionsDir: userExtensionsDir,
192
223
  name: 'test-extension',
@@ -199,7 +230,7 @@ describe('extension tests', () => {
199
230
  },
200
231
  },
201
232
  });
202
- const extensions = extensionManager.loadExtensions();
233
+ const extensions = await extensionManager.loadExtensions();
203
234
  expect(extensions).toHaveLength(1);
204
235
  const expectedCwd = path.join(userExtensionsDir, 'test-extension', 'server');
205
236
  expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd);
@@ -212,12 +243,13 @@ describe('extension tests', () => {
212
243
  contextFileName: 'context.md',
213
244
  });
214
245
  fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
215
- const extensionName = await extensionManager.installOrUpdateExtension({
246
+ await extensionManager.loadExtensions();
247
+ const extension = await extensionManager.installOrUpdateExtension({
216
248
  source: sourceExtDir,
217
249
  type: 'link',
218
250
  });
219
- expect(extensionName).toEqual('my-linked-extension');
220
- const extensions = extensionManager.loadExtensions();
251
+ expect(extension.name).toEqual('my-linked-extension');
252
+ const extensions = extensionManager.getExtensions();
221
253
  expect(extensions).toHaveLength(1);
222
254
  const linkedExt = extensions[0];
223
255
  expect(linkedExt.name).toBe('my-linked-extension');
@@ -230,7 +262,32 @@ describe('extension tests', () => {
230
262
  path.join(sourceExtDir, 'context.md'),
231
263
  ]);
232
264
  });
233
- it('should resolve environment variables in extension configuration', () => {
265
+ it('should hydrate ${extensionPath} correctly for linked extensions', async () => {
266
+ const sourceExtDir = createExtension({
267
+ extensionsDir: tempWorkspaceDir,
268
+ name: 'my-linked-extension-with-path',
269
+ version: '1.0.0',
270
+ mcpServers: {
271
+ 'test-server': {
272
+ command: 'node',
273
+ args: ['${extensionPath}${/}server${/}index.js'],
274
+ cwd: '${extensionPath}${/}server',
275
+ },
276
+ },
277
+ });
278
+ await extensionManager.loadExtensions();
279
+ await extensionManager.installOrUpdateExtension({
280
+ source: sourceExtDir,
281
+ type: 'link',
282
+ });
283
+ const extensions = extensionManager.getExtensions();
284
+ expect(extensions).toHaveLength(1);
285
+ expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(path.join(sourceExtDir, 'server'));
286
+ expect(extensions[0].mcpServers?.['test-server'].args).toEqual([
287
+ path.join(sourceExtDir, 'server', 'index.js'),
288
+ ]);
289
+ });
290
+ it('should resolve environment variables in extension configuration', async () => {
234
291
  process.env['TEST_API_KEY'] = 'test-api-key-123';
235
292
  process.env['TEST_DB_URL'] = 'postgresql://localhost:5432/testdb';
236
293
  try {
@@ -256,7 +313,7 @@ describe('extension tests', () => {
256
313
  },
257
314
  };
258
315
  fs.writeFileSync(configPath, JSON.stringify(extensionConfig));
259
- const extensions = extensionManager.loadExtensions();
316
+ const extensions = await extensionManager.loadExtensions();
260
317
  expect(extensions).toHaveLength(1);
261
318
  const extension = extensions[0];
262
319
  expect(extension.name).toBe('test-extension');
@@ -273,7 +330,7 @@ describe('extension tests', () => {
273
330
  delete process.env['TEST_DB_URL'];
274
331
  }
275
332
  });
276
- it('should resolve environment variables from an extension .env file', () => {
333
+ it('should resolve environment variables from an extension .env file', async () => {
277
334
  const extDir = createExtension({
278
335
  extensionsDir: userExtensionsDir,
279
336
  name: 'test-extension',
@@ -288,10 +345,17 @@ describe('extension tests', () => {
288
345
  },
289
346
  },
290
347
  },
348
+ settings: [
349
+ {
350
+ name: 'My API Key',
351
+ description: 'API key for testing.',
352
+ envVar: 'MY_API_KEY',
353
+ },
354
+ ],
291
355
  });
292
356
  const envFilePath = path.join(extDir, '.env');
293
357
  fs.writeFileSync(envFilePath, 'MY_API_KEY=test-key-from-file\n');
294
- const extensions = extensionManager.loadExtensions();
358
+ const extensions = await extensionManager.loadExtensions();
295
359
  expect(extensions).toHaveLength(1);
296
360
  const extension = extensions[0];
297
361
  const serverConfig = extension.mcpServers['test-server'];
@@ -299,7 +363,7 @@ describe('extension tests', () => {
299
363
  expect(serverConfig.env['API_KEY']).toBe('test-key-from-file');
300
364
  expect(serverConfig.env['STATIC_VALUE']).toBe('no-substitution');
301
365
  });
302
- it('should handle missing environment variables gracefully', () => {
366
+ it('should handle missing environment variables gracefully', async () => {
303
367
  const userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
304
368
  fs.mkdirSync(userExtensionsDir, { recursive: true });
305
369
  const extDir = path.join(userExtensionsDir, 'test-extension');
@@ -319,7 +383,7 @@ describe('extension tests', () => {
319
383
  },
320
384
  };
321
385
  fs.writeFileSync(path.join(extDir, EXTENSIONS_CONFIG_FILENAME), JSON.stringify(extensionConfig));
322
- const extensions = extensionManager.loadExtensions();
386
+ const extensions = await extensionManager.loadExtensions();
323
387
  expect(extensions).toHaveLength(1);
324
388
  const extension = extensions[0];
325
389
  const serverConfig = extension.mcpServers['test-server'];
@@ -327,7 +391,7 @@ describe('extension tests', () => {
327
391
  expect(serverConfig.env['MISSING_VAR']).toBe('$UNDEFINED_ENV_VAR');
328
392
  expect(serverConfig.env['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
329
393
  });
330
- it('should skip extensions with invalid JSON and log a warning', () => {
394
+ it('should skip extensions with invalid JSON and log a warning', async () => {
331
395
  const consoleSpy = vi
332
396
  .spyOn(console, 'error')
333
397
  .mockImplementation(() => { });
@@ -342,14 +406,13 @@ describe('extension tests', () => {
342
406
  fs.mkdirSync(badExtDir);
343
407
  const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
344
408
  fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
345
- const extensions = extensionManager.loadExtensions();
409
+ const extensions = await extensionManager.loadExtensions();
346
410
  expect(extensions).toHaveLength(1);
347
411
  expect(extensions[0].name).toBe('good-ext');
348
- expect(consoleSpy).toHaveBeenCalledOnce();
349
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
412
+ expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
350
413
  consoleSpy.mockRestore();
351
414
  });
352
- it('should skip extensions with missing name and log a warning', () => {
415
+ it('should skip extensions with missing name and log a warning', async () => {
353
416
  const consoleSpy = vi
354
417
  .spyOn(console, 'error')
355
418
  .mockImplementation(() => { });
@@ -364,14 +427,13 @@ describe('extension tests', () => {
364
427
  fs.mkdirSync(badExtDir);
365
428
  const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
366
429
  fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
367
- const extensions = extensionManager.loadExtensions();
430
+ const extensions = await extensionManager.loadExtensions();
368
431
  expect(extensions).toHaveLength(1);
369
432
  expect(extensions[0].name).toBe('good-ext');
370
- expect(consoleSpy).toHaveBeenCalledOnce();
371
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
433
+ expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
372
434
  consoleSpy.mockRestore();
373
435
  });
374
- it('should filter trust out of mcp servers', () => {
436
+ it('should filter trust out of mcp servers', async () => {
375
437
  createExtension({
376
438
  extensionsDir: userExtensionsDir,
377
439
  name: 'test-extension',
@@ -384,27 +446,28 @@ describe('extension tests', () => {
384
446
  },
385
447
  },
386
448
  });
387
- const extensions = extensionManager.loadExtensions();
449
+ const extensions = await extensionManager.loadExtensions();
388
450
  expect(extensions).toHaveLength(1);
389
451
  expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
390
452
  });
391
- it('should throw an error for invalid extension names', () => {
453
+ it('should throw an error for invalid extension names', async () => {
392
454
  const consoleSpy = vi
393
455
  .spyOn(console, 'error')
394
456
  .mockImplementation(() => { });
395
- const badExtDir = createExtension({
457
+ createExtension({
396
458
  extensionsDir: userExtensionsDir,
397
459
  name: 'bad_name',
398
460
  version: '1.0.0',
399
461
  });
400
- const extension = extensionManager.loadExtension(badExtDir);
401
- expect(extension).toBeNull();
462
+ const extensions = await extensionManager.loadExtensions();
463
+ const extension = extensions.find((e) => e.name === 'bad_name');
464
+ expect(extension).toBeUndefined();
402
465
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid extension name: "bad_name"'));
403
466
  consoleSpy.mockRestore();
404
467
  });
405
468
  describe('id generation', () => {
406
- it('should generate id from source for non-github git urls', () => {
407
- const extensionDir = createExtension({
469
+ it('should generate id from source for non-github git urls', async () => {
470
+ createExtension({
408
471
  extensionsDir: userExtensionsDir,
409
472
  name: 'my-ext',
410
473
  version: '1.0.0',
@@ -413,11 +476,12 @@ describe('extension tests', () => {
413
476
  source: 'http://somehost.com/foo/bar',
414
477
  },
415
478
  });
416
- const extension = extensionManager.loadExtension(extensionDir);
479
+ const extensions = await extensionManager.loadExtensions();
480
+ const extension = extensions.find((e) => e.name === 'my-ext');
417
481
  expect(extension?.id).toBe(hashValue('http://somehost.com/foo/bar'));
418
482
  });
419
- it('should generate id from owner/repo for github http urls', () => {
420
- const extensionDir = createExtension({
483
+ it('should generate id from owner/repo for github http urls', async () => {
484
+ createExtension({
421
485
  extensionsDir: userExtensionsDir,
422
486
  name: 'my-ext',
423
487
  version: '1.0.0',
@@ -426,11 +490,12 @@ describe('extension tests', () => {
426
490
  source: 'http://github.com/foo/bar',
427
491
  },
428
492
  });
429
- const extension = extensionManager.loadExtension(extensionDir);
493
+ const extensions = await extensionManager.loadExtensions();
494
+ const extension = extensions.find((e) => e.name === 'my-ext');
430
495
  expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
431
496
  });
432
- it('should generate id from owner/repo for github ssh urls', () => {
433
- const extensionDir = createExtension({
497
+ it('should generate id from owner/repo for github ssh urls', async () => {
498
+ createExtension({
434
499
  extensionsDir: userExtensionsDir,
435
500
  name: 'my-ext',
436
501
  version: '1.0.0',
@@ -439,11 +504,12 @@ describe('extension tests', () => {
439
504
  source: 'git@github.com:foo/bar',
440
505
  },
441
506
  });
442
- const extension = extensionManager.loadExtension(extensionDir);
507
+ const extensions = await extensionManager.loadExtensions();
508
+ const extension = extensions.find((e) => e.name === 'my-ext');
443
509
  expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
444
510
  });
445
- it('should generate id from source for github-release extension', () => {
446
- const extensionDir = createExtension({
511
+ it('should generate id from source for github-release extension', async () => {
512
+ createExtension({
447
513
  extensionsDir: userExtensionsDir,
448
514
  name: 'my-ext',
449
515
  version: '1.0.0',
@@ -452,11 +518,12 @@ describe('extension tests', () => {
452
518
  source: 'https://github.com/foo/bar',
453
519
  },
454
520
  });
455
- const extension = extensionManager.loadExtension(extensionDir);
521
+ const extensions = await extensionManager.loadExtensions();
522
+ const extension = extensions.find((e) => e.name === 'my-ext');
456
523
  expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
457
524
  });
458
- it('should generate id from the original source for local extension', () => {
459
- const extensionDir = createExtension({
525
+ it('should generate id from the original source for local extension', async () => {
526
+ createExtension({
460
527
  extensionsDir: userExtensionsDir,
461
528
  name: 'local-ext-name',
462
529
  version: '1.0.0',
@@ -465,7 +532,8 @@ describe('extension tests', () => {
465
532
  source: '/some/path',
466
533
  },
467
534
  });
468
- const extension = extensionManager.loadExtension(extensionDir);
535
+ const extensions = await extensionManager.loadExtensions();
536
+ const extension = extensions.find((e) => e.name === 'local-ext-name');
469
537
  expect(extension?.id).toBe(hashValue('/some/path'));
470
538
  });
471
539
  it('should generate id from the original source for linked extensions', async () => {
@@ -475,20 +543,24 @@ describe('extension tests', () => {
475
543
  name: 'link-ext-name',
476
544
  version: '1.0.0',
477
545
  });
478
- const extensionName = await extensionManager.installOrUpdateExtension({
546
+ await extensionManager.loadExtensions();
547
+ await extensionManager.installOrUpdateExtension({
479
548
  type: 'link',
480
549
  source: actualExtensionDir,
481
550
  });
482
- const extension = extensionManager.loadExtension(new ExtensionStorage(extensionName).getExtensionDir());
551
+ const extension = extensionManager
552
+ .getExtensions()
553
+ .find((e) => e.name === 'link-ext-name');
483
554
  expect(extension?.id).toBe(hashValue(actualExtensionDir));
484
555
  });
485
- it('should generate id from name for extension with no install metadata', () => {
486
- const extensionDir = createExtension({
556
+ it('should generate id from name for extension with no install metadata', async () => {
557
+ createExtension({
487
558
  extensionsDir: userExtensionsDir,
488
559
  name: 'no-meta-name',
489
560
  version: '1.0.0',
490
561
  });
491
- const extension = extensionManager.loadExtension(extensionDir);
562
+ const extensions = await extensionManager.loadExtensions();
563
+ const extension = extensions.find((e) => e.name === 'no-meta-name');
492
564
  expect(extension?.id).toBe(hashValue('no-meta-name'));
493
565
  });
494
566
  });
@@ -502,6 +574,7 @@ describe('extension tests', () => {
502
574
  });
503
575
  const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
504
576
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
577
+ await extensionManager.loadExtensions();
505
578
  await extensionManager.installOrUpdateExtension({
506
579
  source: sourceExtDir,
507
580
  type: 'local',
@@ -521,6 +594,7 @@ describe('extension tests', () => {
521
594
  name: 'my-local-extension',
522
595
  version: '1.0.0',
523
596
  });
597
+ await extensionManager.loadExtensions();
524
598
  await extensionManager.installOrUpdateExtension({
525
599
  source: sourceExtDir,
526
600
  type: 'local',
@@ -582,6 +656,7 @@ describe('extension tests', () => {
582
656
  failureReason: 'no release data',
583
657
  type: 'github-release',
584
658
  });
659
+ await extensionManager.loadExtensions();
585
660
  await extensionManager.installOrUpdateExtension({
586
661
  source: gitUrl,
587
662
  type: 'git',
@@ -603,6 +678,7 @@ describe('extension tests', () => {
603
678
  const targetExtDir = path.join(userExtensionsDir, 'my-linked-extension');
604
679
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
605
680
  const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
681
+ await extensionManager.loadExtensions();
606
682
  await extensionManager.installOrUpdateExtension({
607
683
  source: sourceExtDir,
608
684
  type: 'link',
@@ -625,6 +701,7 @@ describe('extension tests', () => {
625
701
  name: 'my-local-extension',
626
702
  version: '1.1.0',
627
703
  });
704
+ await extensionManager.loadExtensions();
628
705
  if (isUpdate) {
629
706
  await extensionManager.installOrUpdateExtension({
630
707
  source: sourceExtDir,
@@ -686,10 +763,13 @@ describe('extension tests', () => {
686
763
  },
687
764
  },
688
765
  });
766
+ await extensionManager.loadExtensions();
689
767
  await expect(extensionManager.installOrUpdateExtension({
690
768
  source: sourceExtDir,
691
769
  type: 'local',
692
- })).resolves.toBe('my-local-extension');
770
+ })).resolves.toMatchObject({
771
+ name: 'my-local-extension',
772
+ });
693
773
  expect(mockRequestConsent).toHaveBeenCalledWith(`Installing extension "my-local-extension".
694
774
  ${INSTALL_WARNING_MESSAGE}
695
775
  This extension will run the following MCP servers:
@@ -708,10 +788,11 @@ This extension will run the following MCP servers:
708
788
  },
709
789
  },
710
790
  });
791
+ await extensionManager.loadExtensions();
711
792
  await expect(extensionManager.installOrUpdateExtension({
712
793
  source: sourceExtDir,
713
794
  type: 'local',
714
- })).resolves.toBe('my-local-extension');
795
+ })).resolves.toMatchObject({ name: 'my-local-extension' });
715
796
  });
716
797
  it('should cancel installation if user declines prompt for local extension with mcp servers', async () => {
717
798
  const sourceExtDir = createExtension({
@@ -726,6 +807,7 @@ This extension will run the following MCP servers:
726
807
  },
727
808
  });
728
809
  mockRequestConsent.mockResolvedValue(false);
810
+ await extensionManager.loadExtensions();
729
811
  await expect(extensionManager.installOrUpdateExtension({
730
812
  source: sourceExtDir,
731
813
  type: 'local',
@@ -739,6 +821,7 @@ This extension will run the following MCP servers:
739
821
  });
740
822
  const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
741
823
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
824
+ await extensionManager.loadExtensions();
742
825
  await extensionManager.installOrUpdateExtension({
743
826
  source: sourceExtDir,
744
827
  type: 'local',
@@ -766,6 +849,7 @@ This extension will run the following MCP servers:
766
849
  },
767
850
  },
768
851
  });
852
+ await extensionManager.loadExtensions();
769
853
  // Install it with hard coded consent first.
770
854
  await extensionManager.installOrUpdateExtension({
771
855
  source: sourceExtDir,
@@ -775,7 +859,7 @@ This extension will run the following MCP servers:
775
859
  // Now update it without changing anything.
776
860
  await expect(extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
777
861
  // Provide its own existing config as the previous config.
778
- await extensionManager.loadExtensionConfig(sourceExtDir))).resolves.toBe('my-local-extension');
862
+ await extensionManager.loadExtensionConfig(sourceExtDir))).resolves.toMatchObject({ name: 'my-local-extension' });
779
863
  // Still only called once
780
864
  expect(mockRequestConsent).toHaveBeenCalledOnce();
781
865
  });
@@ -792,6 +876,7 @@ This extension will run the following MCP servers:
792
876
  },
793
877
  ],
794
878
  });
879
+ await extensionManager.loadExtensions();
795
880
  await extensionManager.installOrUpdateExtension({
796
881
  source: sourceExtDir,
797
882
  type: 'local',
@@ -815,8 +900,9 @@ This extension will run the following MCP servers:
815
900
  workspaceDir: tempWorkspaceDir,
816
901
  requestConsent: mockRequestConsent,
817
902
  requestSetting: null,
818
- loadedSettings: loadSettings(tempWorkspaceDir),
903
+ settings: loadSettings(tempWorkspaceDir).merged,
819
904
  });
905
+ await extensionManager.loadExtensions();
820
906
  await extensionManager.installOrUpdateExtension({
821
907
  source: sourceExtDir,
822
908
  type: 'local',
@@ -837,6 +923,7 @@ This extension will run the following MCP servers:
837
923
  ],
838
924
  });
839
925
  mockPromptForSettings.mockResolvedValueOnce('old-api-key');
926
+ await extensionManager.loadExtensions();
840
927
  // Install it so it exists in the userExtensionsDir
841
928
  await extensionManager.installOrUpdateExtension({
842
929
  source: oldSourceExtDir,
@@ -890,6 +977,7 @@ This extension will run the following MCP servers:
890
977
  },
891
978
  ],
892
979
  });
980
+ await extensionManager.loadExtensions();
893
981
  await extensionManager.installOrUpdateExtension({
894
982
  source: oldSourceExtDir,
895
983
  type: 'local',
@@ -954,6 +1042,7 @@ This extension will run the following MCP servers:
954
1042
  version: '1.0.0',
955
1043
  });
956
1044
  vi.spyOn(ExtensionStorage, 'createTmpDir').mockResolvedValue(join(tempDir, extensionName));
1045
+ await extensionManager.loadExtensions();
957
1046
  await extensionManager.installOrUpdateExtension({
958
1047
  source: gitUrl,
959
1048
  type: 'github-release',
@@ -975,6 +1064,7 @@ This extension will run the following MCP servers:
975
1064
  errorMessage: 'download failed',
976
1065
  type: 'github-release',
977
1066
  });
1067
+ await extensionManager.loadExtensions();
978
1068
  await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
979
1069
  // It gets called once to ask for a git clone, and once to consent to
980
1070
  // the actual extension features.
@@ -992,6 +1082,7 @@ This extension will run the following MCP servers:
992
1082
  type: 'github-release',
993
1083
  });
994
1084
  mockRequestConsent.mockResolvedValue(false);
1085
+ await extensionManager.loadExtensions();
995
1086
  await expect(extensionManager.installOrUpdateExtension({
996
1087
  source: gitUrl,
997
1088
  type: 'github-release',
@@ -1005,6 +1096,7 @@ This extension will run the following MCP servers:
1005
1096
  failureReason: 'no release data',
1006
1097
  type: 'github-release',
1007
1098
  });
1099
+ await extensionManager.loadExtensions();
1008
1100
  await extensionManager.installOrUpdateExtension({
1009
1101
  source: gitUrl,
1010
1102
  type: 'git',
@@ -1024,6 +1116,7 @@ This extension will run the following MCP servers:
1024
1116
  errorMessage: 'No release data found',
1025
1117
  type: 'github-release',
1026
1118
  });
1119
+ await extensionManager.loadExtensions();
1027
1120
  await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
1028
1121
  expect(mockRequestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
1029
1122
  expect(mockGit.clone).toHaveBeenCalled();
@@ -1037,6 +1130,7 @@ This extension will run the following MCP servers:
1037
1130
  name: 'my-local-extension',
1038
1131
  version: '1.0.0',
1039
1132
  });
1133
+ await extensionManager.loadExtensions();
1040
1134
  await extensionManager.uninstallExtension('my-local-extension', false);
1041
1135
  expect(fs.existsSync(sourceExtDir)).toBe(false);
1042
1136
  });
@@ -1051,12 +1145,14 @@ This extension will run the following MCP servers:
1051
1145
  name: 'other-extension',
1052
1146
  version: '1.0.0',
1053
1147
  });
1148
+ await extensionManager.loadExtensions();
1054
1149
  await extensionManager.uninstallExtension('my-local-extension', false);
1055
1150
  expect(fs.existsSync(sourceExtDir)).toBe(false);
1056
- expect(extensionManager.loadExtensions()).toHaveLength(1);
1151
+ expect(extensionManager.getExtensions()).toHaveLength(1);
1057
1152
  expect(fs.existsSync(otherExtDir)).toBe(true);
1058
1153
  });
1059
1154
  it('should throw an error if the extension does not exist', async () => {
1155
+ await extensionManager.loadExtensions();
1060
1156
  await expect(extensionManager.uninstallExtension('nonexistent-extension', false)).rejects.toThrow('Extension not found.');
1061
1157
  });
1062
1158
  describe.each([true, false])('with isUpdate: %s', (isUpdate) => {
@@ -1070,6 +1166,7 @@ This extension will run the following MCP servers:
1070
1166
  type: 'local',
1071
1167
  },
1072
1168
  });
1169
+ await extensionManager.loadExtensions();
1073
1170
  await extensionManager.uninstallExtension('my-local-extension', isUpdate);
1074
1171
  if (isUpdate) {
1075
1172
  expect(mockLogExtensionUninstall).not.toHaveBeenCalled();
@@ -1088,6 +1185,7 @@ This extension will run the following MCP servers:
1088
1185
  });
1089
1186
  const enablementManager = new ExtensionEnablementManager();
1090
1187
  enablementManager.enable('test-extension', true, '/some/scope');
1188
+ await extensionManager.loadExtensions();
1091
1189
  await extensionManager.uninstallExtension('test-extension', isUpdate);
1092
1190
  const config = enablementManager.readConfig()['test-extension'];
1093
1191
  if (isUpdate) {
@@ -1110,6 +1208,7 @@ This extension will run the following MCP servers:
1110
1208
  type: 'git',
1111
1209
  },
1112
1210
  });
1211
+ await extensionManager.loadExtensions();
1113
1212
  await extensionManager.uninstallExtension(gitUrl, false);
1114
1213
  expect(fs.existsSync(sourceExtDir)).toBe(false);
1115
1214
  expect(mockLogExtensionUninstall).toHaveBeenCalled();
@@ -1122,28 +1221,31 @@ This extension will run the following MCP servers:
1122
1221
  version: '1.0.0',
1123
1222
  // No installMetadata provided
1124
1223
  });
1224
+ await extensionManager.loadExtensions();
1125
1225
  await expect(extensionManager.uninstallExtension('https://github.com/google/no-metadata-extension', false)).rejects.toThrow('Extension not found.');
1126
1226
  });
1127
1227
  });
1128
1228
  describe('disableExtension', () => {
1129
- it('should disable an extension at the user scope', () => {
1229
+ it('should disable an extension at the user scope', async () => {
1130
1230
  createExtension({
1131
1231
  extensionsDir: userExtensionsDir,
1132
1232
  name: 'my-extension',
1133
1233
  version: '1.0.0',
1134
1234
  });
1235
+ await extensionManager.loadExtensions();
1135
1236
  extensionManager.disableExtension('my-extension', SettingScope.User);
1136
1237
  expect(isEnabled({
1137
1238
  name: 'my-extension',
1138
1239
  enabledForPath: tempWorkspaceDir,
1139
1240
  })).toBe(false);
1140
1241
  });
1141
- it('should disable an extension at the workspace scope', () => {
1242
+ it('should disable an extension at the workspace scope', async () => {
1142
1243
  createExtension({
1143
1244
  extensionsDir: userExtensionsDir,
1144
1245
  name: 'my-extension',
1145
1246
  version: '1.0.0',
1146
1247
  });
1248
+ await extensionManager.loadExtensions();
1147
1249
  extensionManager.disableExtension('my-extension', SettingScope.Workspace);
1148
1250
  expect(isEnabled({
1149
1251
  name: 'my-extension',
@@ -1154,12 +1256,13 @@ This extension will run the following MCP servers:
1154
1256
  enabledForPath: tempWorkspaceDir,
1155
1257
  })).toBe(false);
1156
1258
  });
1157
- it('should handle disabling the same extension twice', () => {
1259
+ it('should handle disabling the same extension twice', async () => {
1158
1260
  createExtension({
1159
1261
  extensionsDir: userExtensionsDir,
1160
1262
  name: 'my-extension',
1161
1263
  version: '1.0.0',
1162
1264
  });
1265
+ await extensionManager.loadExtensions();
1163
1266
  extensionManager.disableExtension('my-extension', SettingScope.User);
1164
1267
  extensionManager.disableExtension('my-extension', SettingScope.User);
1165
1268
  expect(isEnabled({
@@ -1167,10 +1270,10 @@ This extension will run the following MCP servers:
1167
1270
  enabledForPath: tempWorkspaceDir,
1168
1271
  })).toBe(false);
1169
1272
  });
1170
- it('should throw an error if you request system scope', () => {
1171
- 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.');
1172
1275
  });
1173
- it('should log a disable event', () => {
1276
+ it('should log a disable event', async () => {
1174
1277
  createExtension({
1175
1278
  extensionsDir: userExtensionsDir,
1176
1279
  name: 'ext1',
@@ -1180,6 +1283,7 @@ This extension will run the following MCP servers:
1180
1283
  type: 'local',
1181
1284
  },
1182
1285
  });
1286
+ await extensionManager.loadExtensions();
1183
1287
  extensionManager.disableExtension('ext1', SettingScope.Workspace);
1184
1288
  expect(mockLogExtensionDisable).toHaveBeenCalled();
1185
1289
  expect(ExtensionDisableEvent).toHaveBeenCalledWith(hashValue('ext1'), hashValue(userExtensionsDir), SettingScope.Workspace);
@@ -1190,38 +1294,40 @@ This extension will run the following MCP servers:
1190
1294
  vi.restoreAllMocks();
1191
1295
  });
1192
1296
  const getActiveExtensions = () => {
1193
- const extensions = extensionManager.loadExtensions();
1297
+ const extensions = extensionManager.getExtensions();
1194
1298
  return extensions.filter((e) => e.isActive);
1195
1299
  };
1196
- it('should enable an extension at the user scope', () => {
1300
+ it('should enable an extension at the user scope', async () => {
1197
1301
  createExtension({
1198
1302
  extensionsDir: userExtensionsDir,
1199
1303
  name: 'ext1',
1200
1304
  version: '1.0.0',
1201
1305
  });
1306
+ await extensionManager.loadExtensions();
1202
1307
  extensionManager.disableExtension('ext1', SettingScope.User);
1203
1308
  let activeExtensions = getActiveExtensions();
1204
1309
  expect(activeExtensions).toHaveLength(0);
1205
- extensionManager.enableExtension('ext1', SettingScope.User);
1206
- activeExtensions = getActiveExtensions();
1310
+ await extensionManager.enableExtension('ext1', SettingScope.User);
1311
+ activeExtensions = await getActiveExtensions();
1207
1312
  expect(activeExtensions).toHaveLength(1);
1208
1313
  expect(activeExtensions[0].name).toBe('ext1');
1209
1314
  });
1210
- it('should enable an extension at the workspace scope', () => {
1315
+ it('should enable an extension at the workspace scope', async () => {
1211
1316
  createExtension({
1212
1317
  extensionsDir: userExtensionsDir,
1213
1318
  name: 'ext1',
1214
1319
  version: '1.0.0',
1215
1320
  });
1321
+ await extensionManager.loadExtensions();
1216
1322
  extensionManager.disableExtension('ext1', SettingScope.Workspace);
1217
1323
  let activeExtensions = getActiveExtensions();
1218
1324
  expect(activeExtensions).toHaveLength(0);
1219
- extensionManager.enableExtension('ext1', SettingScope.Workspace);
1220
- activeExtensions = getActiveExtensions();
1325
+ await extensionManager.enableExtension('ext1', SettingScope.Workspace);
1326
+ activeExtensions = await getActiveExtensions();
1221
1327
  expect(activeExtensions).toHaveLength(1);
1222
1328
  expect(activeExtensions[0].name).toBe('ext1');
1223
1329
  });
1224
- it('should log an enable event', () => {
1330
+ it('should log an enable event', async () => {
1225
1331
  createExtension({
1226
1332
  extensionsDir: userExtensionsDir,
1227
1333
  name: 'ext1',
@@ -1231,6 +1337,7 @@ This extension will run the following MCP servers:
1231
1337
  type: 'local',
1232
1338
  },
1233
1339
  });
1340
+ await extensionManager.loadExtensions();
1234
1341
  extensionManager.disableExtension('ext1', SettingScope.Workspace);
1235
1342
  extensionManager.enableExtension('ext1', SettingScope.Workspace);
1236
1343
  expect(mockLogExtensionEnable).toHaveBeenCalled();