@google/gemini-cli 0.10.0-preview.1 → 0.11.0-nightly.20251020.a96f0659
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 +9 -0
- package/dist/google-gemini-cli-0.11.0-nightly.20251015.203bad7c.tgz +0 -0
- package/dist/package.json +2 -2
- package/dist/src/commands/extensions/examples/mcp-server/example.d.ts +6 -0
- package/dist/src/commands/extensions/examples/mcp-server/example.js +46 -0
- package/dist/src/commands/extensions/examples/mcp-server/example.js.map +1 -0
- package/dist/src/commands/extensions/install.d.ts +1 -0
- package/dist/src/commands/extensions/install.js +15 -2
- package/dist/src/commands/extensions/install.js.map +1 -1
- package/dist/src/commands/extensions/list.js +3 -2
- package/dist/src/commands/extensions/list.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/add.test.d.ts +6 -0
- package/dist/src/commands/mcp/add.test.js +234 -0
- package/dist/src/commands/mcp/add.test.js.map +1 -0
- package/dist/src/commands/mcp/list.js +5 -3
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/commands/mcp/list.test.d.ts +6 -0
- package/dist/src/commands/mcp/list.test.js +117 -0
- package/dist/src/commands/mcp/list.test.js.map +1 -0
- package/dist/src/commands/mcp/remove.test.d.ts +6 -0
- package/dist/src/commands/mcp/remove.test.js +175 -0
- package/dist/src/commands/mcp/remove.test.js.map +1 -0
- package/dist/src/commands/mcp.test.d.ts +6 -0
- package/dist/src/commands/mcp.test.js +62 -0
- package/dist/src/commands/mcp.test.js.map +1 -0
- package/dist/src/config/auth.js +3 -1
- package/dist/src/config/auth.js.map +1 -1
- package/dist/src/config/auth.test.js +3 -1
- package/dist/src/config/auth.test.js.map +1 -1
- package/dist/src/config/config.d.ts +0 -11
- package/dist/src/config/config.integration.test.d.ts +6 -0
- package/dist/src/config/config.integration.test.js +351 -0
- package/dist/src/config/config.integration.test.js.map +1 -0
- package/dist/src/config/config.js +19 -74
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.d.ts +6 -0
- package/dist/src/config/config.test.js +2001 -0
- package/dist/src/config/config.test.js.map +1 -0
- package/dist/src/config/extension.d.ts +1 -4
- package/dist/src/config/extension.js +64 -69
- package/dist/src/config/extension.js.map +1 -1
- package/dist/src/config/extension.test.d.ts +6 -0
- package/dist/src/config/extension.test.js +1176 -0
- package/dist/src/config/extension.test.js.map +1 -0
- package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
- package/dist/src/config/extensions/extensionEnablement.js +4 -3
- package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
- package/dist/src/config/extensions/extensionEnablement.test.js +21 -18
- package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
- package/dist/src/config/extensions/github.d.ts +15 -7
- package/dist/src/config/extensions/github.js +95 -21
- package/dist/src/config/extensions/github.js.map +1 -1
- package/dist/src/config/extensions/github.test.js +14 -12
- package/dist/src/config/extensions/github.test.js.map +1 -1
- package/dist/src/config/extensions/update.test.js +9 -9
- package/dist/src/config/extensions/update.test.js.map +1 -1
- package/dist/src/config/keyBindings.d.ts +2 -1
- package/dist/src/config/keyBindings.js +4 -2
- package/dist/src/config/keyBindings.js.map +1 -1
- package/dist/src/config/policy.js +13 -14
- package/dist/src/config/policy.js.map +1 -1
- package/dist/src/config/policy.test.js +3 -3
- package/dist/src/config/policy.test.js.map +1 -1
- package/dist/src/config/sandboxConfig.d.ts +0 -1
- package/dist/src/config/sandboxConfig.js +1 -3
- package/dist/src/config/sandboxConfig.js.map +1 -1
- package/dist/src/config/settings.test.d.ts +6 -0
- package/dist/src/config/settings.test.js +1937 -0
- package/dist/src/config/settings.test.js.map +1 -0
- package/dist/src/gemini.js +12 -10
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +34 -11
- 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/nonInteractiveCli.js +91 -3
- package/dist/src/nonInteractiveCli.js.map +1 -1
- package/dist/src/nonInteractiveCli.test.d.ts +6 -0
- package/dist/src/nonInteractiveCli.test.js +711 -0
- package/dist/src/nonInteractiveCli.test.js.map +1 -0
- package/dist/src/services/FileCommandLoader.test.d.ts +6 -0
- package/dist/src/services/FileCommandLoader.test.js +971 -0
- package/dist/src/services/FileCommandLoader.test.js.map +1 -0
- package/dist/src/services/prompt-processors/argumentProcessor.test.d.ts +6 -0
- package/dist/src/services/prompt-processors/argumentProcessor.test.js +40 -0
- package/dist/src/services/prompt-processors/argumentProcessor.test.js.map +1 -0
- package/dist/src/services/prompt-processors/shellProcessor.test.d.ts +6 -0
- package/dist/src/services/prompt-processors/shellProcessor.test.js +482 -0
- package/dist/src/services/prompt-processors/shellProcessor.test.js.map +1 -0
- package/dist/src/test-utils/render.d.ts +2 -1
- package/dist/src/test-utils/render.js +5 -2
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/ui/App.test.d.ts +6 -0
- package/dist/src/ui/App.test.js +110 -0
- package/dist/src/ui/App.test.js.map +1 -0
- package/dist/src/ui/AppContainer.js +39 -26
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/AppContainer.test.js +35 -9
- package/dist/src/ui/AppContainer.test.js.map +1 -1
- package/dist/src/ui/auth/AuthDialog.d.ts +1 -1
- package/dist/src/ui/auth/AuthDialog.js +3 -1
- package/dist/src/ui/auth/AuthDialog.js.map +1 -1
- package/dist/src/ui/auth/useAuth.d.ts +1 -1
- package/dist/src/ui/auth/useAuth.js +3 -1
- package/dist/src/ui/auth/useAuth.js.map +1 -1
- package/dist/src/ui/commands/aboutCommand.js +1 -1
- package/dist/src/ui/commands/aboutCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/aboutCommand.test.js +130 -0
- package/dist/src/ui/commands/aboutCommand.test.js.map +1 -0
- package/dist/src/ui/commands/authCommand.js +1 -1
- package/dist/src/ui/commands/authCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/authCommand.test.js +30 -0
- package/dist/src/ui/commands/authCommand.test.js.map +1 -0
- package/dist/src/ui/commands/bugCommand.js +1 -1
- package/dist/src/ui/commands/bugCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/bugCommand.test.js +105 -0
- package/dist/src/ui/commands/bugCommand.test.js.map +1 -0
- package/dist/src/ui/commands/chatCommand.js +1 -1
- package/dist/src/ui/commands/chatCommand.js.map +1 -1
- package/dist/src/ui/commands/chatCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/chatCommand.test.js +555 -0
- package/dist/src/ui/commands/chatCommand.test.js.map +1 -0
- package/dist/src/ui/commands/clearCommand.js +1 -1
- package/dist/src/ui/commands/clearCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/clearCommand.test.js +76 -0
- package/dist/src/ui/commands/clearCommand.test.js.map +1 -0
- package/dist/src/ui/commands/compressCommand.js +1 -1
- package/dist/src/ui/commands/compressCommand.js.map +1 -1
- package/dist/src/ui/commands/compressCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/compressCommand.test.js +98 -0
- package/dist/src/ui/commands/compressCommand.test.js.map +1 -0
- package/dist/src/ui/commands/copyCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/copyCommand.test.js +242 -0
- package/dist/src/ui/commands/copyCommand.test.js.map +1 -0
- package/dist/src/ui/commands/corgiCommand.js +1 -1
- package/dist/src/ui/commands/corgiCommand.js.map +1 -1
- package/dist/src/ui/commands/corgiCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/corgiCommand.test.js +28 -0
- package/dist/src/ui/commands/corgiCommand.test.js.map +1 -0
- package/dist/src/ui/commands/directoryCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/directoryCommand.test.js +145 -0
- package/dist/src/ui/commands/directoryCommand.test.js.map +1 -0
- package/dist/src/ui/commands/docsCommand.js +1 -1
- package/dist/src/ui/commands/docsCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/docsCommand.test.js +72 -0
- package/dist/src/ui/commands/docsCommand.test.js.map +1 -0
- package/dist/src/ui/commands/editorCommand.js +1 -1
- package/dist/src/ui/commands/editorCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/editorCommand.test.js +27 -0
- package/dist/src/ui/commands/editorCommand.test.js.map +1 -0
- package/dist/src/ui/commands/extensionsCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/extensionsCommand.test.js +241 -0
- package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -0
- package/dist/src/ui/commands/helpCommand.js +1 -1
- package/dist/src/ui/commands/helpCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/helpCommand.test.js +42 -0
- package/dist/src/ui/commands/helpCommand.test.js.map +1 -0
- package/dist/src/ui/commands/ideCommand.js +6 -6
- package/dist/src/ui/commands/ideCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/ideCommand.test.js +203 -0
- package/dist/src/ui/commands/ideCommand.test.js.map +1 -0
- package/dist/src/ui/commands/initCommand.js +1 -1
- package/dist/src/ui/commands/initCommand.js.map +1 -1
- package/dist/src/ui/commands/initCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/initCommand.test.js +80 -0
- package/dist/src/ui/commands/initCommand.test.js.map +1 -0
- package/dist/src/ui/commands/mcpCommand.js +98 -88
- package/dist/src/ui/commands/mcpCommand.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/mcpCommand.test.js +148 -0
- package/dist/src/ui/commands/mcpCommand.test.js.map +1 -0
- package/dist/src/ui/commands/memoryCommand.js +5 -5
- package/dist/src/ui/commands/memoryCommand.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/memoryCommand.test.js +266 -0
- package/dist/src/ui/commands/memoryCommand.test.js.map +1 -0
- package/dist/src/ui/commands/privacyCommand.js +1 -1
- package/dist/src/ui/commands/privacyCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/privacyCommand.test.js +32 -0
- package/dist/src/ui/commands/privacyCommand.test.js.map +1 -0
- package/dist/src/ui/commands/quitCommand.js +1 -1
- package/dist/src/ui/commands/quitCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/quitCommand.test.js +50 -0
- package/dist/src/ui/commands/quitCommand.test.js.map +1 -0
- package/dist/src/ui/commands/restoreCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/restoreCommand.test.js +190 -0
- package/dist/src/ui/commands/restoreCommand.test.js.map +1 -0
- package/dist/src/ui/commands/settingsCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/settingsCommand.test.js +30 -0
- package/dist/src/ui/commands/settingsCommand.test.js.map +1 -0
- package/dist/src/ui/commands/setupGithubCommand.test.js +1 -2
- package/dist/src/ui/commands/setupGithubCommand.test.js.map +1 -1
- package/dist/src/ui/commands/statsCommand.js +3 -3
- package/dist/src/ui/commands/statsCommand.js.map +1 -1
- package/dist/src/ui/commands/statsCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/statsCommand.test.js +53 -0
- package/dist/src/ui/commands/statsCommand.test.js.map +1 -0
- package/dist/src/ui/commands/terminalSetupCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/terminalSetupCommand.test.js +66 -0
- package/dist/src/ui/commands/terminalSetupCommand.test.js.map +1 -0
- package/dist/src/ui/commands/themeCommand.js +1 -1
- package/dist/src/ui/commands/themeCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/themeCommand.test.js +32 -0
- package/dist/src/ui/commands/themeCommand.test.js.map +1 -0
- package/dist/src/ui/commands/toolsCommand.js +1 -1
- package/dist/src/ui/commands/toolsCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/toolsCommand.test.js +100 -0
- package/dist/src/ui/commands/toolsCommand.test.js.map +1 -0
- package/dist/src/ui/commands/vimCommand.js +1 -1
- package/dist/src/ui/components/Composer.js +5 -3
- package/dist/src/ui/components/Composer.js.map +1 -1
- package/dist/src/ui/components/Composer.test.js +16 -1
- package/dist/src/ui/components/Composer.test.js.map +1 -1
- package/dist/src/ui/components/ContextSummaryDisplay.d.ts +0 -1
- package/dist/src/ui/components/ContextSummaryDisplay.js +2 -12
- package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
- package/dist/src/ui/components/ContextSummaryDisplay.test.d.ts +6 -0
- package/dist/src/ui/components/ContextSummaryDisplay.test.js +66 -0
- package/dist/src/ui/components/ContextSummaryDisplay.test.js.map +1 -0
- package/dist/src/ui/components/DialogManager.js +1 -5
- package/dist/src/ui/components/DialogManager.js.map +1 -1
- package/dist/src/ui/components/EditorSettingsDialog.js +1 -1
- package/dist/src/ui/components/EditorSettingsDialog.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js +7 -3
- 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.d.ts +6 -0
- package/dist/src/ui/components/Footer.test.js +231 -0
- package/dist/src/ui/components/Footer.test.js.map +1 -0
- package/dist/src/ui/components/InputPrompt.d.ts +4 -0
- package/dist/src/ui/components/InputPrompt.js +53 -4
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.test.d.ts +6 -0
- package/dist/src/ui/components/InputPrompt.test.js +1716 -0
- package/dist/src/ui/components/InputPrompt.test.js.map +1 -0
- package/dist/src/ui/components/ModelStatsDisplay.test.d.ts +6 -0
- package/dist/src/ui/components/ModelStatsDisplay.test.js +285 -0
- package/dist/src/ui/components/ModelStatsDisplay.test.js.map +1 -0
- package/dist/src/ui/components/PermissionsModifyTrustDialog.js +22 -18
- package/dist/src/ui/components/PermissionsModifyTrustDialog.js.map +1 -1
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +10 -2
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/QueuedMessageDisplay.js +3 -3
- package/dist/src/ui/components/QueuedMessageDisplay.js.map +1 -1
- package/dist/src/ui/components/QueuedMessageDisplay.test.js +4 -0
- package/dist/src/ui/components/QueuedMessageDisplay.test.js.map +1 -1
- package/dist/src/ui/components/RawMarkdownIndicator.d.ts +7 -0
- package/dist/src/ui/components/RawMarkdownIndicator.js +8 -0
- package/dist/src/ui/components/RawMarkdownIndicator.js.map +1 -0
- package/dist/src/ui/components/SessionSummaryDisplay.test.d.ts +6 -0
- package/dist/src/ui/components/SessionSummaryDisplay.test.js +74 -0
- package/dist/src/ui/components/SessionSummaryDisplay.test.js.map +1 -0
- package/dist/src/ui/components/SettingsDialog.js +8 -8
- package/dist/src/ui/components/SettingsDialog.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.test.js +188 -56
- package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
- package/dist/src/ui/components/StatsDisplay.test.d.ts +6 -0
- package/dist/src/ui/components/StatsDisplay.test.js +351 -0
- package/dist/src/ui/components/StatsDisplay.test.js.map +1 -0
- package/dist/src/ui/components/ThemeDialog.d.ts +4 -2
- package/dist/src/ui/components/ThemeDialog.js +3 -3
- package/dist/src/ui/components/ThemeDialog.js.map +1 -1
- package/dist/src/ui/components/ThemeDialog.test.js +13 -0
- package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
- package/dist/src/ui/components/ToolStatsDisplay.test.d.ts +6 -0
- package/dist/src/ui/components/ToolStatsDisplay.test.js +227 -0
- package/dist/src/ui/components/ToolStatsDisplay.test.js.map +1 -0
- package/dist/src/ui/components/messages/GeminiMessage.js +3 -1
- package/dist/src/ui/components/messages/GeminiMessage.js.map +1 -1
- package/dist/src/ui/components/messages/GeminiMessage.test.d.ts +6 -0
- package/dist/src/ui/components/messages/GeminiMessage.test.js +35 -0
- package/dist/src/ui/components/messages/GeminiMessage.test.js.map +1 -0
- package/dist/src/ui/components/messages/GeminiMessageContent.js +3 -1
- package/dist/src/ui/components/messages/GeminiMessageContent.js.map +1 -1
- package/dist/src/ui/components/messages/Todo.d.ts +7 -0
- package/dist/src/ui/components/messages/Todo.js +59 -0
- package/dist/src/ui/components/messages/Todo.js.map +1 -0
- package/dist/src/ui/components/messages/Todo.test.d.ts +6 -0
- package/dist/src/ui/components/messages/Todo.test.js +113 -0
- package/dist/src/ui/components/messages/Todo.test.js.map +1 -0
- package/dist/src/ui/components/messages/ToolGroupMessage.js +1 -1
- package/dist/src/ui/components/messages/ToolGroupMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolMessage.js +8 -3
- package/dist/src/ui/components/messages/ToolMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolMessage.test.js +2 -2
- package/dist/src/ui/components/messages/ToolMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.d.ts +6 -0
- package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js +30 -0
- package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js.map +1 -0
- package/dist/src/ui/components/messages/UserShellMessage.js +1 -1
- package/dist/src/ui/components/messages/UserShellMessage.js.map +1 -1
- package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -1
- package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.test.d.ts +6 -0
- package/dist/src/ui/components/shared/text-buffer.test.js +1554 -0
- package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -0
- package/dist/src/ui/components/shared/vim-buffer-actions.test.d.ts +6 -0
- package/dist/src/ui/components/shared/vim-buffer-actions.test.js +951 -0
- package/dist/src/ui/components/shared/vim-buffer-actions.test.js.map +1 -0
- package/dist/src/ui/components/views/McpStatus.d.ts +0 -1
- package/dist/src/ui/components/views/McpStatus.js +2 -2
- package/dist/src/ui/components/views/McpStatus.js.map +1 -1
- package/dist/src/ui/components/views/McpStatus.test.js +0 -5
- package/dist/src/ui/components/views/McpStatus.test.js.map +1 -1
- package/dist/src/ui/components/views/ToolsList.test.js +4 -4
- package/dist/src/ui/components/views/ToolsList.test.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.d.ts +1 -0
- package/dist/src/ui/contexts/KeypressContext.js +176 -50
- package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +413 -14
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/SessionContext.test.d.ts +6 -0
- package/dist/src/ui/contexts/SessionContext.test.js +177 -0
- package/dist/src/ui/contexts/SessionContext.test.js.map +1 -0
- package/dist/src/ui/contexts/UIActionsContext.d.ts +5 -4
- package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
- package/dist/src/ui/contexts/UIStateContext.d.ts +3 -3
- package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.test.d.ts +6 -0
- package/dist/src/ui/hooks/slashCommandProcessor.test.js +779 -0
- package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -0
- package/dist/src/ui/hooks/useAtCompletion.js +2 -2
- package/dist/src/ui/hooks/useAtCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useAtCompletion.test.d.ts +6 -0
- package/dist/src/ui/hooks/useAtCompletion.test.js +385 -0
- package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -0
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +0 -1
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.d.ts +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.js +5 -3
- package/dist/src/ui/hooks/useCommandCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.test.d.ts +6 -0
- package/dist/src/ui/hooks/useCommandCompletion.test.js +375 -0
- package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -0
- package/dist/src/ui/hooks/useConsoleMessages.test.d.ts +6 -0
- package/dist/src/ui/hooks/useConsoleMessages.test.js +110 -0
- package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -0
- package/dist/src/ui/hooks/useExtensionUpdates.test.js +3 -3
- package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
- package/dist/src/ui/hooks/useFocus.test.d.ts +6 -0
- package/dist/src/ui/hooks/useFocus.test.js +115 -0
- package/dist/src/ui/hooks/useFocus.test.js.map +1 -0
- package/dist/src/ui/hooks/useFolderTrust.test.d.ts +6 -0
- package/dist/src/ui/hooks/useFolderTrust.test.js +164 -0
- package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -0
- package/dist/src/ui/hooks/useGeminiStream.js +33 -31
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.test.d.ts +6 -0
- package/dist/src/ui/hooks/useGeminiStream.test.js +1936 -0
- package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -0
- package/dist/src/ui/hooks/useKeypress.test.d.ts +6 -0
- package/dist/src/ui/hooks/useKeypress.test.js +234 -0
- package/dist/src/ui/hooks/useKeypress.test.js.map +1 -0
- package/dist/src/ui/hooks/useLoadingIndicator.test.js +5 -0
- package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/useMessageQueue.d.ts +1 -0
- package/dist/src/ui/hooks/useMessageQueue.js +14 -0
- package/dist/src/ui/hooks/useMessageQueue.js.map +1 -1
- package/dist/src/ui/hooks/useMessageQueue.test.js +121 -0
- package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.d.ts +1 -0
- package/dist/src/ui/hooks/usePhraseCycler.js +156 -5
- package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.test.d.ts +6 -0
- package/dist/src/ui/hooks/usePhraseCycler.test.js +155 -0
- package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -0
- package/dist/src/ui/hooks/useThemeCommand.d.ts +2 -1
- package/dist/src/ui/hooks/useThemeCommand.js +6 -0
- package/dist/src/ui/hooks/useThemeCommand.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +3 -0
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/hooks/vim.test.d.ts +6 -0
- package/dist/src/ui/hooks/vim.test.js +1389 -0
- package/dist/src/ui/hooks/vim.test.js.map +1 -0
- package/dist/src/ui/keyMatchers.test.js +9 -3
- package/dist/src/ui/keyMatchers.test.js.map +1 -1
- package/dist/src/ui/themes/theme.test.d.ts +6 -0
- package/dist/src/ui/themes/theme.test.js +85 -0
- package/dist/src/ui/themes/theme.test.js.map +1 -0
- package/dist/src/ui/types.d.ts +0 -1
- package/dist/src/ui/types.js.map +1 -1
- package/dist/src/ui/utils/CodeColorizer.d.ts +1 -1
- package/dist/src/ui/utils/CodeColorizer.js +4 -2
- package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
- package/dist/src/ui/utils/MarkdownDisplay.d.ts +1 -0
- package/dist/src/ui/utils/MarkdownDisplay.js +8 -1
- package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
- package/dist/src/ui/utils/commandUtils.js +18 -2
- package/dist/src/ui/utils/commandUtils.js.map +1 -1
- package/dist/src/ui/utils/commandUtils.test.js +61 -6
- package/dist/src/ui/utils/commandUtils.test.js.map +1 -1
- package/dist/src/ui/utils/updateCheck.d.ts +2 -1
- package/dist/src/ui/utils/updateCheck.js +4 -1
- package/dist/src/ui/utils/updateCheck.js.map +1 -1
- package/dist/src/ui/utils/updateCheck.test.js +25 -10
- package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
- package/dist/src/utils/errors.d.ts +1 -0
- package/dist/src/utils/errors.js +66 -5
- package/dist/src/utils/errors.js.map +1 -1
- package/dist/src/validateNonInterActiveAuth.test.d.ts +6 -0
- package/dist/src/validateNonInterActiveAuth.test.js +336 -0
- package/dist/src/validateNonInterActiveAuth.test.js.map +1 -0
- package/dist/src/zed-integration/zedIntegration.js +1 -2
- package/dist/src/zed-integration/zedIntegration.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/dist/google-gemini-cli-0.10.0-preview.0.tgz +0 -0
- package/dist/src/ui/components/WorkspaceMigrationDialog.d.ts +0 -11
- package/dist/src/ui/components/WorkspaceMigrationDialog.js +0 -44
- package/dist/src/ui/components/WorkspaceMigrationDialog.js.map +0 -1
- package/dist/src/ui/hooks/useWorkspaceMigration.d.ts +0 -13
- package/dist/src/ui/hooks/useWorkspaceMigration.js +0 -59
- package/dist/src/ui/hooks/useWorkspaceMigration.js.map +0 -1
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { EXTENSIONS_CONFIG_FILENAME, ExtensionStorage, INSTALL_METADATA_FILENAME, INSTALL_WARNING_MESSAGE, annotateActiveExtensions, disableExtension, enableExtension, installOrUpdateExtension, loadExtension, loadExtensionConfig, loadExtensions, uninstallExtension, } from './extension.js';
|
|
12
|
+
import { GEMINI_DIR, ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, } from '@google/gemini-cli-core';
|
|
13
|
+
import { SettingScope } from './settings.js';
|
|
14
|
+
import { isWorkspaceTrusted } from './trustedFolders.js';
|
|
15
|
+
import { createExtension } from '../test-utils/createExtension.js';
|
|
16
|
+
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
const mockGit = {
|
|
19
|
+
clone: vi.fn(),
|
|
20
|
+
getRemotes: vi.fn(),
|
|
21
|
+
fetch: vi.fn(),
|
|
22
|
+
checkout: vi.fn(),
|
|
23
|
+
listRemote: vi.fn(),
|
|
24
|
+
revparse: vi.fn(),
|
|
25
|
+
// Not a part of the actual API, but we need to use this to do the correct
|
|
26
|
+
// file system interactions.
|
|
27
|
+
path: vi.fn(),
|
|
28
|
+
};
|
|
29
|
+
const mockDownloadFromGithubRelease = vi.hoisted(() => vi.fn());
|
|
30
|
+
vi.mock('./extensions/github.js', async (importOriginal) => {
|
|
31
|
+
const original = await importOriginal();
|
|
32
|
+
return {
|
|
33
|
+
...original,
|
|
34
|
+
downloadFromGitHubRelease: mockDownloadFromGithubRelease,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
vi.mock('simple-git', () => ({
|
|
38
|
+
simpleGit: vi.fn((path) => {
|
|
39
|
+
mockGit.path.mockReturnValue(path);
|
|
40
|
+
return mockGit;
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
vi.mock('os', async (importOriginal) => {
|
|
44
|
+
const mockedOs = await importOriginal();
|
|
45
|
+
return {
|
|
46
|
+
...mockedOs,
|
|
47
|
+
homedir: vi.fn(),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
vi.mock('./trustedFolders.js', async (importOriginal) => {
|
|
51
|
+
const actual = await importOriginal();
|
|
52
|
+
return {
|
|
53
|
+
...actual,
|
|
54
|
+
isWorkspaceTrusted: vi.fn(),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
const mockLogExtensionEnable = vi.hoisted(() => vi.fn());
|
|
58
|
+
const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn());
|
|
59
|
+
const mockLogExtensionUninstall = vi.hoisted(() => vi.fn());
|
|
60
|
+
const mockLogExtensionUpdateEvent = vi.hoisted(() => vi.fn());
|
|
61
|
+
const mockLogExtensionDisable = vi.hoisted(() => vi.fn());
|
|
62
|
+
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
63
|
+
const actual = await importOriginal();
|
|
64
|
+
return {
|
|
65
|
+
...actual,
|
|
66
|
+
logExtensionEnable: mockLogExtensionEnable,
|
|
67
|
+
logExtensionInstallEvent: mockLogExtensionInstallEvent,
|
|
68
|
+
logExtensionUninstall: mockLogExtensionUninstall,
|
|
69
|
+
logExtensionUpdateEvent: mockLogExtensionUpdateEvent,
|
|
70
|
+
logExtensionDisable: mockLogExtensionDisable,
|
|
71
|
+
ExtensionEnableEvent: vi.fn(),
|
|
72
|
+
ExtensionInstallEvent: vi.fn(),
|
|
73
|
+
ExtensionUninstallEvent: vi.fn(),
|
|
74
|
+
ExtensionDisableEvent: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
vi.mock('child_process', async (importOriginal) => {
|
|
78
|
+
const actual = await importOriginal();
|
|
79
|
+
return {
|
|
80
|
+
...actual,
|
|
81
|
+
execSync: vi.fn(),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
|
|
85
|
+
describe('extension tests', () => {
|
|
86
|
+
let tempHomeDir;
|
|
87
|
+
let tempWorkspaceDir;
|
|
88
|
+
let userExtensionsDir;
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-home-'));
|
|
91
|
+
tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'gemini-cli-test-workspace-'));
|
|
92
|
+
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
93
|
+
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
|
94
|
+
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
|
95
|
+
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
|
96
|
+
isTrusted: true,
|
|
97
|
+
source: undefined,
|
|
98
|
+
});
|
|
99
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
|
100
|
+
});
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
|
103
|
+
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
|
|
104
|
+
vi.restoreAllMocks();
|
|
105
|
+
});
|
|
106
|
+
describe('loadExtensions', () => {
|
|
107
|
+
it('should include extension path in loaded extension', () => {
|
|
108
|
+
const extensionDir = path.join(userExtensionsDir, 'test-extension');
|
|
109
|
+
fs.mkdirSync(extensionDir, { recursive: true });
|
|
110
|
+
createExtension({
|
|
111
|
+
extensionsDir: userExtensionsDir,
|
|
112
|
+
name: 'test-extension',
|
|
113
|
+
version: '1.0.0',
|
|
114
|
+
});
|
|
115
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
116
|
+
expect(extensions).toHaveLength(1);
|
|
117
|
+
expect(extensions[0].path).toBe(extensionDir);
|
|
118
|
+
expect(extensions[0].name).toBe('test-extension');
|
|
119
|
+
});
|
|
120
|
+
it('should load context file path when GEMINI.md is present', () => {
|
|
121
|
+
createExtension({
|
|
122
|
+
extensionsDir: userExtensionsDir,
|
|
123
|
+
name: 'ext1',
|
|
124
|
+
version: '1.0.0',
|
|
125
|
+
addContextFile: true,
|
|
126
|
+
});
|
|
127
|
+
createExtension({
|
|
128
|
+
extensionsDir: userExtensionsDir,
|
|
129
|
+
name: 'ext2',
|
|
130
|
+
version: '2.0.0',
|
|
131
|
+
});
|
|
132
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
133
|
+
expect(extensions).toHaveLength(2);
|
|
134
|
+
const ext1 = extensions.find((e) => e.name === 'ext1');
|
|
135
|
+
const ext2 = extensions.find((e) => e.name === 'ext2');
|
|
136
|
+
expect(ext1?.contextFiles).toEqual([
|
|
137
|
+
path.join(userExtensionsDir, 'ext1', 'GEMINI.md'),
|
|
138
|
+
]);
|
|
139
|
+
expect(ext2?.contextFiles).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
it('should load context file path from the extension config', () => {
|
|
142
|
+
createExtension({
|
|
143
|
+
extensionsDir: userExtensionsDir,
|
|
144
|
+
name: 'ext1',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
addContextFile: false,
|
|
147
|
+
contextFileName: 'my-context-file.md',
|
|
148
|
+
});
|
|
149
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
150
|
+
expect(extensions).toHaveLength(1);
|
|
151
|
+
const ext1 = extensions.find((e) => e.name === 'ext1');
|
|
152
|
+
expect(ext1?.contextFiles).toEqual([
|
|
153
|
+
path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
it('should filter out disabled extensions', () => {
|
|
157
|
+
createExtension({
|
|
158
|
+
extensionsDir: userExtensionsDir,
|
|
159
|
+
name: 'disabled-extension',
|
|
160
|
+
version: '1.0.0',
|
|
161
|
+
});
|
|
162
|
+
createExtension({
|
|
163
|
+
extensionsDir: userExtensionsDir,
|
|
164
|
+
name: 'enabled-extension',
|
|
165
|
+
version: '2.0.0',
|
|
166
|
+
});
|
|
167
|
+
disableExtension('disabled-extension', SettingScope.User, tempWorkspaceDir);
|
|
168
|
+
const manager = new ExtensionEnablementManager();
|
|
169
|
+
const extensions = loadExtensions(manager);
|
|
170
|
+
const activeExtensions = annotateActiveExtensions(extensions, tempWorkspaceDir, manager).filter((e) => e.isActive);
|
|
171
|
+
expect(activeExtensions).toHaveLength(1);
|
|
172
|
+
expect(activeExtensions[0].name).toBe('enabled-extension');
|
|
173
|
+
});
|
|
174
|
+
it('should hydrate variables', () => {
|
|
175
|
+
createExtension({
|
|
176
|
+
extensionsDir: userExtensionsDir,
|
|
177
|
+
name: 'test-extension',
|
|
178
|
+
version: '1.0.0',
|
|
179
|
+
addContextFile: false,
|
|
180
|
+
contextFileName: undefined,
|
|
181
|
+
mcpServers: {
|
|
182
|
+
'test-server': {
|
|
183
|
+
cwd: '${extensionPath}${/}server',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
188
|
+
expect(extensions).toHaveLength(1);
|
|
189
|
+
const expectedCwd = path.join(userExtensionsDir, 'test-extension', 'server');
|
|
190
|
+
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd);
|
|
191
|
+
});
|
|
192
|
+
it('should load a linked extension correctly', async () => {
|
|
193
|
+
const sourceExtDir = createExtension({
|
|
194
|
+
extensionsDir: tempWorkspaceDir,
|
|
195
|
+
name: 'my-linked-extension',
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
contextFileName: 'context.md',
|
|
198
|
+
});
|
|
199
|
+
fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
|
|
200
|
+
const extensionName = await installOrUpdateExtension({
|
|
201
|
+
source: sourceExtDir,
|
|
202
|
+
type: 'link',
|
|
203
|
+
}, async (_) => true);
|
|
204
|
+
expect(extensionName).toEqual('my-linked-extension');
|
|
205
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
206
|
+
expect(extensions).toHaveLength(1);
|
|
207
|
+
const linkedExt = extensions[0];
|
|
208
|
+
expect(linkedExt.name).toBe('my-linked-extension');
|
|
209
|
+
expect(linkedExt.path).toBe(sourceExtDir);
|
|
210
|
+
expect(linkedExt.installMetadata).toEqual({
|
|
211
|
+
source: sourceExtDir,
|
|
212
|
+
type: 'link',
|
|
213
|
+
});
|
|
214
|
+
expect(linkedExt.contextFiles).toEqual([
|
|
215
|
+
path.join(sourceExtDir, 'context.md'),
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
it('should resolve environment variables in extension configuration', () => {
|
|
219
|
+
process.env['TEST_API_KEY'] = 'test-api-key-123';
|
|
220
|
+
process.env['TEST_DB_URL'] = 'postgresql://localhost:5432/testdb';
|
|
221
|
+
try {
|
|
222
|
+
const userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
223
|
+
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
|
224
|
+
const extDir = path.join(userExtensionsDir, 'test-extension');
|
|
225
|
+
fs.mkdirSync(extDir);
|
|
226
|
+
// Write config to a separate file for clarity and good practices
|
|
227
|
+
const configPath = path.join(extDir, EXTENSIONS_CONFIG_FILENAME);
|
|
228
|
+
const extensionConfig = {
|
|
229
|
+
name: 'test-extension',
|
|
230
|
+
version: '1.0.0',
|
|
231
|
+
mcpServers: {
|
|
232
|
+
'test-server': {
|
|
233
|
+
command: 'node',
|
|
234
|
+
args: ['server.js'],
|
|
235
|
+
env: {
|
|
236
|
+
API_KEY: '$TEST_API_KEY',
|
|
237
|
+
DATABASE_URL: '${TEST_DB_URL}',
|
|
238
|
+
STATIC_VALUE: 'no-substitution',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
fs.writeFileSync(configPath, JSON.stringify(extensionConfig));
|
|
244
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
245
|
+
expect(extensions).toHaveLength(1);
|
|
246
|
+
const extension = extensions[0];
|
|
247
|
+
expect(extension.name).toBe('test-extension');
|
|
248
|
+
expect(extension.mcpServers).toBeDefined();
|
|
249
|
+
const serverConfig = extension.mcpServers?.['test-server'];
|
|
250
|
+
expect(serverConfig).toBeDefined();
|
|
251
|
+
expect(serverConfig?.env).toBeDefined();
|
|
252
|
+
expect(serverConfig?.env?.['API_KEY']).toBe('test-api-key-123');
|
|
253
|
+
expect(serverConfig?.env?.['DATABASE_URL']).toBe('postgresql://localhost:5432/testdb');
|
|
254
|
+
expect(serverConfig?.env?.['STATIC_VALUE']).toBe('no-substitution');
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
delete process.env['TEST_API_KEY'];
|
|
258
|
+
delete process.env['TEST_DB_URL'];
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
it('should handle missing environment variables gracefully', () => {
|
|
262
|
+
const userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
263
|
+
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
|
264
|
+
const extDir = path.join(userExtensionsDir, 'test-extension');
|
|
265
|
+
fs.mkdirSync(extDir);
|
|
266
|
+
const extensionConfig = {
|
|
267
|
+
name: 'test-extension',
|
|
268
|
+
version: '1.0.0',
|
|
269
|
+
mcpServers: {
|
|
270
|
+
'test-server': {
|
|
271
|
+
command: 'node',
|
|
272
|
+
args: ['server.js'],
|
|
273
|
+
env: {
|
|
274
|
+
MISSING_VAR: '$UNDEFINED_ENV_VAR',
|
|
275
|
+
MISSING_VAR_BRACES: '${ALSO_UNDEFINED}',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
fs.writeFileSync(path.join(extDir, EXTENSIONS_CONFIG_FILENAME), JSON.stringify(extensionConfig));
|
|
281
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
282
|
+
expect(extensions).toHaveLength(1);
|
|
283
|
+
const extension = extensions[0];
|
|
284
|
+
const serverConfig = extension.mcpServers['test-server'];
|
|
285
|
+
expect(serverConfig.env).toBeDefined();
|
|
286
|
+
expect(serverConfig.env['MISSING_VAR']).toBe('$UNDEFINED_ENV_VAR');
|
|
287
|
+
expect(serverConfig.env['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
|
|
288
|
+
});
|
|
289
|
+
it('should skip extensions with invalid JSON and log a warning', () => {
|
|
290
|
+
const consoleSpy = vi
|
|
291
|
+
.spyOn(console, 'error')
|
|
292
|
+
.mockImplementation(() => { });
|
|
293
|
+
// Good extension
|
|
294
|
+
createExtension({
|
|
295
|
+
extensionsDir: userExtensionsDir,
|
|
296
|
+
name: 'good-ext',
|
|
297
|
+
version: '1.0.0',
|
|
298
|
+
});
|
|
299
|
+
// Bad extension
|
|
300
|
+
const badExtDir = path.join(userExtensionsDir, 'bad-ext');
|
|
301
|
+
fs.mkdirSync(badExtDir);
|
|
302
|
+
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
303
|
+
fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
|
|
304
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
305
|
+
expect(extensions).toHaveLength(1);
|
|
306
|
+
expect(extensions[0].name).toBe('good-ext');
|
|
307
|
+
expect(consoleSpy).toHaveBeenCalledOnce();
|
|
308
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
|
|
309
|
+
consoleSpy.mockRestore();
|
|
310
|
+
});
|
|
311
|
+
it('should skip extensions with missing name and log a warning', () => {
|
|
312
|
+
const consoleSpy = vi
|
|
313
|
+
.spyOn(console, 'error')
|
|
314
|
+
.mockImplementation(() => { });
|
|
315
|
+
// Good extension
|
|
316
|
+
createExtension({
|
|
317
|
+
extensionsDir: userExtensionsDir,
|
|
318
|
+
name: 'good-ext',
|
|
319
|
+
version: '1.0.0',
|
|
320
|
+
});
|
|
321
|
+
// Bad extension
|
|
322
|
+
const badExtDir = path.join(userExtensionsDir, 'bad-ext-no-name');
|
|
323
|
+
fs.mkdirSync(badExtDir);
|
|
324
|
+
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
325
|
+
fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
|
|
326
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
327
|
+
expect(extensions).toHaveLength(1);
|
|
328
|
+
expect(extensions[0].name).toBe('good-ext');
|
|
329
|
+
expect(consoleSpy).toHaveBeenCalledOnce();
|
|
330
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
|
|
331
|
+
consoleSpy.mockRestore();
|
|
332
|
+
});
|
|
333
|
+
it('should filter trust out of mcp servers', () => {
|
|
334
|
+
createExtension({
|
|
335
|
+
extensionsDir: userExtensionsDir,
|
|
336
|
+
name: 'test-extension',
|
|
337
|
+
version: '1.0.0',
|
|
338
|
+
mcpServers: {
|
|
339
|
+
'test-server': {
|
|
340
|
+
command: 'node',
|
|
341
|
+
args: ['server.js'],
|
|
342
|
+
trust: true,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
const extensions = loadExtensions(new ExtensionEnablementManager());
|
|
347
|
+
expect(extensions).toHaveLength(1);
|
|
348
|
+
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
it('should throw an error for invalid extension names', () => {
|
|
351
|
+
const consoleSpy = vi
|
|
352
|
+
.spyOn(console, 'error')
|
|
353
|
+
.mockImplementation(() => { });
|
|
354
|
+
const badExtDir = createExtension({
|
|
355
|
+
extensionsDir: userExtensionsDir,
|
|
356
|
+
name: 'bad_name',
|
|
357
|
+
version: '1.0.0',
|
|
358
|
+
});
|
|
359
|
+
const extension = loadExtension({
|
|
360
|
+
extensionDir: badExtDir,
|
|
361
|
+
workspaceDir: tempWorkspaceDir,
|
|
362
|
+
});
|
|
363
|
+
expect(extension).toBeNull();
|
|
364
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid extension name: "bad_name"'));
|
|
365
|
+
consoleSpy.mockRestore();
|
|
366
|
+
});
|
|
367
|
+
describe('id generation', () => {
|
|
368
|
+
it('should generate id from source for non-github git urls', () => {
|
|
369
|
+
const extensionDir = createExtension({
|
|
370
|
+
extensionsDir: userExtensionsDir,
|
|
371
|
+
name: 'my-ext',
|
|
372
|
+
version: '1.0.0',
|
|
373
|
+
installMetadata: {
|
|
374
|
+
type: 'git',
|
|
375
|
+
source: 'http://somehost.com/foo/bar',
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
const extension = loadExtension({
|
|
379
|
+
extensionDir,
|
|
380
|
+
workspaceDir: tempWorkspaceDir,
|
|
381
|
+
});
|
|
382
|
+
const expectedHash = createHash('sha256')
|
|
383
|
+
.update('http://somehost.com/foo/bar')
|
|
384
|
+
.digest('hex');
|
|
385
|
+
expect(extension?.id).toBe(expectedHash);
|
|
386
|
+
});
|
|
387
|
+
it('should generate id from owner/repo for github http urls', () => {
|
|
388
|
+
const extensionDir = createExtension({
|
|
389
|
+
extensionsDir: userExtensionsDir,
|
|
390
|
+
name: 'my-ext',
|
|
391
|
+
version: '1.0.0',
|
|
392
|
+
installMetadata: {
|
|
393
|
+
type: 'git',
|
|
394
|
+
source: 'http://github.com/foo/bar',
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
const extension = loadExtension({
|
|
398
|
+
extensionDir,
|
|
399
|
+
workspaceDir: tempWorkspaceDir,
|
|
400
|
+
});
|
|
401
|
+
const expectedHash = createHash('sha256')
|
|
402
|
+
.update('https://github.com/foo/bar')
|
|
403
|
+
.digest('hex');
|
|
404
|
+
expect(extension?.id).toBe(expectedHash);
|
|
405
|
+
});
|
|
406
|
+
it('should generate id from owner/repo for github ssh urls', () => {
|
|
407
|
+
const extensionDir = createExtension({
|
|
408
|
+
extensionsDir: userExtensionsDir,
|
|
409
|
+
name: 'my-ext',
|
|
410
|
+
version: '1.0.0',
|
|
411
|
+
installMetadata: {
|
|
412
|
+
type: 'git',
|
|
413
|
+
source: 'git@github.com:foo/bar',
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
const extension = loadExtension({
|
|
417
|
+
extensionDir,
|
|
418
|
+
workspaceDir: tempWorkspaceDir,
|
|
419
|
+
});
|
|
420
|
+
const expectedHash = createHash('sha256')
|
|
421
|
+
.update('https://github.com/foo/bar')
|
|
422
|
+
.digest('hex');
|
|
423
|
+
expect(extension?.id).toBe(expectedHash);
|
|
424
|
+
});
|
|
425
|
+
it('should generate id from source for github-release extension', () => {
|
|
426
|
+
const extensionDir = createExtension({
|
|
427
|
+
extensionsDir: userExtensionsDir,
|
|
428
|
+
name: 'my-ext',
|
|
429
|
+
version: '1.0.0',
|
|
430
|
+
installMetadata: {
|
|
431
|
+
type: 'github-release',
|
|
432
|
+
source: 'https://github.com/foo/bar',
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
const extension = loadExtension({
|
|
436
|
+
extensionDir,
|
|
437
|
+
workspaceDir: tempWorkspaceDir,
|
|
438
|
+
});
|
|
439
|
+
const expectedHash = createHash('sha256')
|
|
440
|
+
.update('https://github.com/foo/bar')
|
|
441
|
+
.digest('hex');
|
|
442
|
+
expect(extension?.id).toBe(expectedHash);
|
|
443
|
+
});
|
|
444
|
+
it('should generate id from the original source for local extension', () => {
|
|
445
|
+
const extensionDir = createExtension({
|
|
446
|
+
extensionsDir: userExtensionsDir,
|
|
447
|
+
name: 'local-ext-name',
|
|
448
|
+
version: '1.0.0',
|
|
449
|
+
installMetadata: {
|
|
450
|
+
type: 'local',
|
|
451
|
+
source: '/some/path',
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
const extension = loadExtension({
|
|
455
|
+
extensionDir,
|
|
456
|
+
workspaceDir: tempWorkspaceDir,
|
|
457
|
+
});
|
|
458
|
+
const expectedHash = createHash('sha256')
|
|
459
|
+
.update('/some/path')
|
|
460
|
+
.digest('hex');
|
|
461
|
+
expect(extension?.id).toBe(expectedHash);
|
|
462
|
+
});
|
|
463
|
+
it('should generate id from the original source for linked extensions', async () => {
|
|
464
|
+
const extDevelopmentDir = path.join(tempHomeDir, 'local_extensions');
|
|
465
|
+
const actualExtensionDir = createExtension({
|
|
466
|
+
extensionsDir: extDevelopmentDir,
|
|
467
|
+
name: 'link-ext-name',
|
|
468
|
+
version: '1.0.0',
|
|
469
|
+
});
|
|
470
|
+
const extensionName = await installOrUpdateExtension({
|
|
471
|
+
type: 'link',
|
|
472
|
+
source: actualExtensionDir,
|
|
473
|
+
}, async () => true, tempWorkspaceDir);
|
|
474
|
+
const extension = loadExtension({
|
|
475
|
+
extensionDir: new ExtensionStorage(extensionName).getExtensionDir(),
|
|
476
|
+
workspaceDir: tempWorkspaceDir,
|
|
477
|
+
});
|
|
478
|
+
const expectedHash = createHash('sha256')
|
|
479
|
+
.update(actualExtensionDir)
|
|
480
|
+
.digest('hex');
|
|
481
|
+
expect(extension?.id).toBe(expectedHash);
|
|
482
|
+
});
|
|
483
|
+
it('should generate id from name for extension with no install metadata', () => {
|
|
484
|
+
const extensionDir = createExtension({
|
|
485
|
+
extensionsDir: userExtensionsDir,
|
|
486
|
+
name: 'no-meta-name',
|
|
487
|
+
version: '1.0.0',
|
|
488
|
+
});
|
|
489
|
+
const extension = loadExtension({
|
|
490
|
+
extensionDir,
|
|
491
|
+
workspaceDir: tempWorkspaceDir,
|
|
492
|
+
});
|
|
493
|
+
const expectedHash = createHash('sha256')
|
|
494
|
+
.update('no-meta-name')
|
|
495
|
+
.digest('hex');
|
|
496
|
+
expect(extension?.id).toBe(expectedHash);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
describe('annotateActiveExtensions', () => {
|
|
501
|
+
const extensions = [
|
|
502
|
+
{
|
|
503
|
+
path: '/path/to/ext1',
|
|
504
|
+
name: 'ext1',
|
|
505
|
+
version: '1.0.0',
|
|
506
|
+
contextFiles: [],
|
|
507
|
+
isActive: true,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
path: '/path/to/ext2',
|
|
511
|
+
name: 'ext2',
|
|
512
|
+
version: '1.0.0',
|
|
513
|
+
contextFiles: [],
|
|
514
|
+
isActive: true,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
path: '/path/to/ext3',
|
|
518
|
+
name: 'ext3',
|
|
519
|
+
version: '1.0.0',
|
|
520
|
+
contextFiles: [],
|
|
521
|
+
isActive: true,
|
|
522
|
+
},
|
|
523
|
+
];
|
|
524
|
+
it('should mark all extensions as active if no enabled extensions are provided', () => {
|
|
525
|
+
const activeExtensions = annotateActiveExtensions(extensions, '/path/to/workspace', new ExtensionEnablementManager());
|
|
526
|
+
expect(activeExtensions).toHaveLength(3);
|
|
527
|
+
expect(activeExtensions.every((e) => e.isActive)).toBe(true);
|
|
528
|
+
});
|
|
529
|
+
it('should mark only the enabled extensions as active', () => {
|
|
530
|
+
const activeExtensions = annotateActiveExtensions(extensions, '/path/to/workspace', new ExtensionEnablementManager(['ext1', 'ext3']));
|
|
531
|
+
expect(activeExtensions).toHaveLength(3);
|
|
532
|
+
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(true);
|
|
533
|
+
expect(activeExtensions.find((e) => e.name === 'ext2')?.isActive).toBe(false);
|
|
534
|
+
expect(activeExtensions.find((e) => e.name === 'ext3')?.isActive).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
it('should mark all extensions as inactive when "none" is provided', () => {
|
|
537
|
+
const activeExtensions = annotateActiveExtensions(extensions, '/path/to/workspace', new ExtensionEnablementManager(['none']));
|
|
538
|
+
expect(activeExtensions).toHaveLength(3);
|
|
539
|
+
expect(activeExtensions.every((e) => !e.isActive)).toBe(true);
|
|
540
|
+
});
|
|
541
|
+
it('should handle case-insensitivity', () => {
|
|
542
|
+
const activeExtensions = annotateActiveExtensions(extensions, '/path/to/workspace', new ExtensionEnablementManager(['EXT1']));
|
|
543
|
+
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
it('should log an error for unknown extensions', () => {
|
|
546
|
+
const consoleSpy = vi
|
|
547
|
+
.spyOn(console, 'error')
|
|
548
|
+
.mockImplementation(() => { });
|
|
549
|
+
annotateActiveExtensions(extensions, '/path/to/workspace', new ExtensionEnablementManager(['ext4']));
|
|
550
|
+
expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
|
|
551
|
+
consoleSpy.mockRestore();
|
|
552
|
+
});
|
|
553
|
+
describe('autoUpdate', () => {
|
|
554
|
+
it('should be false if autoUpdate is not set in install metadata', () => {
|
|
555
|
+
const activeExtensions = annotateActiveExtensions(extensions, tempHomeDir, new ExtensionEnablementManager());
|
|
556
|
+
expect(activeExtensions.every((e) => e.installMetadata?.autoUpdate === false)).toBe(false);
|
|
557
|
+
});
|
|
558
|
+
it('should be true if autoUpdate is true in install metadata', () => {
|
|
559
|
+
const extensionsWithAutoUpdate = extensions.map((e) => ({
|
|
560
|
+
...e,
|
|
561
|
+
installMetadata: {
|
|
562
|
+
...e.installMetadata,
|
|
563
|
+
autoUpdate: true,
|
|
564
|
+
},
|
|
565
|
+
}));
|
|
566
|
+
const activeExtensions = annotateActiveExtensions(extensionsWithAutoUpdate, tempHomeDir, new ExtensionEnablementManager());
|
|
567
|
+
expect(activeExtensions.every((e) => e.installMetadata?.autoUpdate === true)).toBe(true);
|
|
568
|
+
});
|
|
569
|
+
it('should respect the per-extension settings from install metadata', () => {
|
|
570
|
+
const extensionsWithAutoUpdate = [
|
|
571
|
+
{
|
|
572
|
+
path: '/path/to/ext1',
|
|
573
|
+
name: 'ext1',
|
|
574
|
+
version: '1.0.0',
|
|
575
|
+
contextFiles: [],
|
|
576
|
+
installMetadata: {
|
|
577
|
+
source: 'test',
|
|
578
|
+
type: 'local',
|
|
579
|
+
autoUpdate: true,
|
|
580
|
+
},
|
|
581
|
+
isActive: true,
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
path: '/path/to/ext2',
|
|
585
|
+
name: 'ext2',
|
|
586
|
+
version: '1.0.0',
|
|
587
|
+
contextFiles: [],
|
|
588
|
+
installMetadata: {
|
|
589
|
+
source: 'test',
|
|
590
|
+
type: 'local',
|
|
591
|
+
autoUpdate: false,
|
|
592
|
+
},
|
|
593
|
+
isActive: true,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
path: '/path/to/ext3',
|
|
597
|
+
name: 'ext3',
|
|
598
|
+
version: '1.0.0',
|
|
599
|
+
contextFiles: [],
|
|
600
|
+
isActive: true,
|
|
601
|
+
},
|
|
602
|
+
];
|
|
603
|
+
const activeExtensions = annotateActiveExtensions(extensionsWithAutoUpdate, tempHomeDir, new ExtensionEnablementManager());
|
|
604
|
+
expect(activeExtensions.find((e) => e.name === 'ext1')?.installMetadata
|
|
605
|
+
?.autoUpdate).toBe(true);
|
|
606
|
+
expect(activeExtensions.find((e) => e.name === 'ext2')?.installMetadata
|
|
607
|
+
?.autoUpdate).toBe(false);
|
|
608
|
+
expect(activeExtensions.find((e) => e.name === 'ext3')?.installMetadata
|
|
609
|
+
?.autoUpdate).toBe(undefined);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
describe('installExtension', () => {
|
|
614
|
+
it('should install an extension from a local path', async () => {
|
|
615
|
+
const sourceExtDir = createExtension({
|
|
616
|
+
extensionsDir: tempHomeDir,
|
|
617
|
+
name: 'my-local-extension',
|
|
618
|
+
version: '1.0.0',
|
|
619
|
+
});
|
|
620
|
+
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
|
621
|
+
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
622
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true);
|
|
623
|
+
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
624
|
+
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
625
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
626
|
+
expect(metadata).toEqual({
|
|
627
|
+
source: sourceExtDir,
|
|
628
|
+
type: 'local',
|
|
629
|
+
});
|
|
630
|
+
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
|
631
|
+
});
|
|
632
|
+
it('should throw an error if the extension already exists', async () => {
|
|
633
|
+
const sourceExtDir = createExtension({
|
|
634
|
+
extensionsDir: tempHomeDir,
|
|
635
|
+
name: 'my-local-extension',
|
|
636
|
+
version: '1.0.0',
|
|
637
|
+
});
|
|
638
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true);
|
|
639
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow('Extension "my-local-extension" is already installed. Please uninstall it first.');
|
|
640
|
+
});
|
|
641
|
+
it('should throw an error and cleanup if gemini-extension.json is missing', async () => {
|
|
642
|
+
const sourceExtDir = path.join(tempHomeDir, 'bad-extension');
|
|
643
|
+
fs.mkdirSync(sourceExtDir, { recursive: true });
|
|
644
|
+
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
645
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow(`Configuration file not found at ${configPath}`);
|
|
646
|
+
const targetExtDir = path.join(userExtensionsDir, 'bad-extension');
|
|
647
|
+
expect(fs.existsSync(targetExtDir)).toBe(false);
|
|
648
|
+
});
|
|
649
|
+
it('should throw an error for invalid JSON in gemini-extension.json', async () => {
|
|
650
|
+
const sourceExtDir = path.join(tempHomeDir, 'bad-json-ext');
|
|
651
|
+
fs.mkdirSync(sourceExtDir, { recursive: true });
|
|
652
|
+
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
653
|
+
fs.writeFileSync(configPath, '{ "name": "bad-json", "version": "1.0.0"'); // Malformed JSON
|
|
654
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow(new RegExp(`^Failed to load extension config from ${configPath.replace(/\\/g, '\\\\')}`));
|
|
655
|
+
});
|
|
656
|
+
it('should throw an error for missing name in gemini-extension.json', async () => {
|
|
657
|
+
const sourceExtDir = createExtension({
|
|
658
|
+
extensionsDir: tempHomeDir,
|
|
659
|
+
name: 'missing-name-ext',
|
|
660
|
+
version: '1.0.0',
|
|
661
|
+
});
|
|
662
|
+
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
663
|
+
// Overwrite with invalid config
|
|
664
|
+
fs.writeFileSync(configPath, JSON.stringify({ version: '1.0.0' }));
|
|
665
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow(`Invalid configuration in ${configPath}: missing "name"`);
|
|
666
|
+
});
|
|
667
|
+
it('should install an extension from a git URL', async () => {
|
|
668
|
+
const gitUrl = 'https://somehost.com/somerepo.git';
|
|
669
|
+
const extensionName = 'some-extension';
|
|
670
|
+
const targetExtDir = path.join(userExtensionsDir, extensionName);
|
|
671
|
+
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
672
|
+
mockGit.clone.mockImplementation(async (_, destination) => {
|
|
673
|
+
fs.mkdirSync(path.join(mockGit.path(), destination), {
|
|
674
|
+
recursive: true,
|
|
675
|
+
});
|
|
676
|
+
fs.writeFileSync(path.join(mockGit.path(), destination, EXTENSIONS_CONFIG_FILENAME), JSON.stringify({ name: extensionName, version: '1.0.0' }));
|
|
677
|
+
});
|
|
678
|
+
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
|
679
|
+
mockDownloadFromGithubRelease.mockResolvedValue({
|
|
680
|
+
success: false,
|
|
681
|
+
failureReason: 'no release data',
|
|
682
|
+
type: 'github-release',
|
|
683
|
+
});
|
|
684
|
+
await installOrUpdateExtension({ source: gitUrl, type: 'git' }, async (_) => true);
|
|
685
|
+
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
686
|
+
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
687
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
688
|
+
expect(metadata).toEqual({
|
|
689
|
+
source: gitUrl,
|
|
690
|
+
type: 'git',
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
it('should install a linked extension', async () => {
|
|
694
|
+
const sourceExtDir = createExtension({
|
|
695
|
+
extensionsDir: tempHomeDir,
|
|
696
|
+
name: 'my-linked-extension',
|
|
697
|
+
version: '1.0.0',
|
|
698
|
+
});
|
|
699
|
+
const targetExtDir = path.join(userExtensionsDir, 'my-linked-extension');
|
|
700
|
+
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
701
|
+
const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
702
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'link' }, async (_) => true);
|
|
703
|
+
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
704
|
+
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
705
|
+
expect(fs.existsSync(configPath)).toBe(false);
|
|
706
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
707
|
+
expect(metadata).toEqual({
|
|
708
|
+
source: sourceExtDir,
|
|
709
|
+
type: 'link',
|
|
710
|
+
});
|
|
711
|
+
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
|
712
|
+
});
|
|
713
|
+
describe.each([true, false])('with previous extension config: %s', (isUpdate) => {
|
|
714
|
+
let sourceExtDir;
|
|
715
|
+
beforeEach(async () => {
|
|
716
|
+
sourceExtDir = createExtension({
|
|
717
|
+
extensionsDir: tempHomeDir,
|
|
718
|
+
name: 'my-local-extension',
|
|
719
|
+
version: '1.1.0',
|
|
720
|
+
});
|
|
721
|
+
if (isUpdate) {
|
|
722
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true);
|
|
723
|
+
}
|
|
724
|
+
// Clears out any calls to mocks from the above function calls.
|
|
725
|
+
vi.clearAllMocks();
|
|
726
|
+
});
|
|
727
|
+
it(`should log an ${isUpdate ? 'update' : 'install'} event to clearcut on success`, async () => {
|
|
728
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true, undefined, isUpdate
|
|
729
|
+
? {
|
|
730
|
+
name: 'my-local-extension',
|
|
731
|
+
version: '1.0.0',
|
|
732
|
+
}
|
|
733
|
+
: undefined);
|
|
734
|
+
if (isUpdate) {
|
|
735
|
+
expect(mockLogExtensionUpdateEvent).toHaveBeenCalled();
|
|
736
|
+
expect(mockLogExtensionInstallEvent).not.toHaveBeenCalled();
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
expect(mockLogExtensionInstallEvent).toHaveBeenCalled();
|
|
740
|
+
expect(mockLogExtensionUpdateEvent).not.toHaveBeenCalled();
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
it(`should ${isUpdate ? 'not ' : ''} alter the extension enablement configuration`, async () => {
|
|
744
|
+
const enablementManager = new ExtensionEnablementManager();
|
|
745
|
+
enablementManager.enable('my-local-extension', true, '/some/scope');
|
|
746
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true, undefined, isUpdate
|
|
747
|
+
? {
|
|
748
|
+
name: 'my-local-extension',
|
|
749
|
+
version: '1.0.0',
|
|
750
|
+
}
|
|
751
|
+
: undefined);
|
|
752
|
+
const config = enablementManager.readConfig()['my-local-extension'];
|
|
753
|
+
if (isUpdate) {
|
|
754
|
+
expect(config).not.toBeUndefined();
|
|
755
|
+
expect(config.overrides).toContain('/some/scope/*');
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
expect(config).not.toContain('/some/scope/*');
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
it('should show users information on their ansi escaped mcp servers when installing', async () => {
|
|
763
|
+
const sourceExtDir = createExtension({
|
|
764
|
+
extensionsDir: tempHomeDir,
|
|
765
|
+
name: 'my-local-extension',
|
|
766
|
+
version: '1.0.0',
|
|
767
|
+
mcpServers: {
|
|
768
|
+
'test-server': {
|
|
769
|
+
command: 'node dobadthing \u001b[12D\u001b[K',
|
|
770
|
+
args: ['server.js'],
|
|
771
|
+
description: 'a local mcp server',
|
|
772
|
+
},
|
|
773
|
+
'test-server-2': {
|
|
774
|
+
description: 'a remote mcp server',
|
|
775
|
+
httpUrl: 'https://google.com',
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
const mockRequestConsent = vi.fn();
|
|
780
|
+
mockRequestConsent.mockResolvedValue(true);
|
|
781
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, mockRequestConsent)).resolves.toBe('my-local-extension');
|
|
782
|
+
expect(mockRequestConsent).toHaveBeenCalledWith(`Installing extension "my-local-extension".
|
|
783
|
+
${INSTALL_WARNING_MESSAGE}
|
|
784
|
+
This extension will run the following MCP servers:
|
|
785
|
+
* test-server (local): node dobadthing \\u001b[12D\\u001b[K server.js
|
|
786
|
+
* test-server-2 (remote): https://google.com`);
|
|
787
|
+
});
|
|
788
|
+
it('should continue installation if user accepts prompt for local extension with mcp servers', async () => {
|
|
789
|
+
const sourceExtDir = createExtension({
|
|
790
|
+
extensionsDir: tempHomeDir,
|
|
791
|
+
name: 'my-local-extension',
|
|
792
|
+
version: '1.0.0',
|
|
793
|
+
mcpServers: {
|
|
794
|
+
'test-server': {
|
|
795
|
+
command: 'node',
|
|
796
|
+
args: ['server.js'],
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async () => true)).resolves.toBe('my-local-extension');
|
|
801
|
+
});
|
|
802
|
+
it('should cancel installation if user declines prompt for local extension with mcp servers', async () => {
|
|
803
|
+
const sourceExtDir = createExtension({
|
|
804
|
+
extensionsDir: tempHomeDir,
|
|
805
|
+
name: 'my-local-extension',
|
|
806
|
+
version: '1.0.0',
|
|
807
|
+
mcpServers: {
|
|
808
|
+
'test-server': {
|
|
809
|
+
command: 'node',
|
|
810
|
+
args: ['server.js'],
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async () => false)).rejects.toThrow('Installation cancelled for "my-local-extension".');
|
|
815
|
+
});
|
|
816
|
+
it('should save the autoUpdate flag to the install metadata', async () => {
|
|
817
|
+
const sourceExtDir = createExtension({
|
|
818
|
+
extensionsDir: tempHomeDir,
|
|
819
|
+
name: 'my-local-extension',
|
|
820
|
+
version: '1.0.0',
|
|
821
|
+
});
|
|
822
|
+
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
|
823
|
+
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
824
|
+
await installOrUpdateExtension({
|
|
825
|
+
source: sourceExtDir,
|
|
826
|
+
type: 'local',
|
|
827
|
+
autoUpdate: true,
|
|
828
|
+
}, async (_) => true);
|
|
829
|
+
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
830
|
+
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
831
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
832
|
+
expect(metadata).toEqual({
|
|
833
|
+
source: sourceExtDir,
|
|
834
|
+
type: 'local',
|
|
835
|
+
autoUpdate: true,
|
|
836
|
+
});
|
|
837
|
+
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
|
838
|
+
});
|
|
839
|
+
it('should ignore consent flow if not required', async () => {
|
|
840
|
+
const sourceExtDir = createExtension({
|
|
841
|
+
extensionsDir: tempHomeDir,
|
|
842
|
+
name: 'my-local-extension',
|
|
843
|
+
version: '1.0.0',
|
|
844
|
+
mcpServers: {
|
|
845
|
+
'test-server': {
|
|
846
|
+
command: 'node',
|
|
847
|
+
args: ['server.js'],
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
const mockRequestConsent = vi.fn();
|
|
852
|
+
// Install it and force consent first.
|
|
853
|
+
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async () => true);
|
|
854
|
+
// Now update it without changing anything.
|
|
855
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, mockRequestConsent, process.cwd(),
|
|
856
|
+
// Provide its own existing config as the previous config.
|
|
857
|
+
await loadExtensionConfig({
|
|
858
|
+
extensionDir: sourceExtDir,
|
|
859
|
+
workspaceDir: process.cwd(),
|
|
860
|
+
}))).resolves.toBe('my-local-extension');
|
|
861
|
+
expect(mockRequestConsent).not.toHaveBeenCalled();
|
|
862
|
+
});
|
|
863
|
+
it('should throw an error for invalid extension names', async () => {
|
|
864
|
+
const sourceExtDir = createExtension({
|
|
865
|
+
extensionsDir: tempHomeDir,
|
|
866
|
+
name: 'bad_name',
|
|
867
|
+
version: '1.0.0',
|
|
868
|
+
});
|
|
869
|
+
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow('Invalid extension name: "bad_name"');
|
|
870
|
+
});
|
|
871
|
+
describe('installing from github', () => {
|
|
872
|
+
const gitUrl = 'https://github.com/google/gemini-test-extension.git';
|
|
873
|
+
const extensionName = 'gemini-test-extension';
|
|
874
|
+
beforeEach(() => {
|
|
875
|
+
// Mock the git clone behavior for github installs that fallback to it.
|
|
876
|
+
mockGit.clone.mockImplementation(async (_, destination) => {
|
|
877
|
+
fs.mkdirSync(path.join(mockGit.path(), destination), {
|
|
878
|
+
recursive: true,
|
|
879
|
+
});
|
|
880
|
+
fs.writeFileSync(path.join(mockGit.path(), destination, EXTENSIONS_CONFIG_FILENAME), JSON.stringify({ name: extensionName, version: '1.0.0' }));
|
|
881
|
+
});
|
|
882
|
+
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
|
883
|
+
});
|
|
884
|
+
afterEach(() => {
|
|
885
|
+
vi.restoreAllMocks();
|
|
886
|
+
});
|
|
887
|
+
it('should install from a github release successfully', async () => {
|
|
888
|
+
const targetExtDir = path.join(userExtensionsDir, extensionName);
|
|
889
|
+
mockDownloadFromGithubRelease.mockResolvedValue({
|
|
890
|
+
success: true,
|
|
891
|
+
tagName: 'v1.0.0',
|
|
892
|
+
type: 'github-release',
|
|
893
|
+
});
|
|
894
|
+
const tempDir = path.join(tempHomeDir, 'temp-ext');
|
|
895
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
896
|
+
createExtension({
|
|
897
|
+
extensionsDir: tempDir,
|
|
898
|
+
name: extensionName,
|
|
899
|
+
version: '1.0.0',
|
|
900
|
+
});
|
|
901
|
+
vi.spyOn(ExtensionStorage, 'createTmpDir').mockResolvedValue(join(tempDir, extensionName));
|
|
902
|
+
await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, async () => true);
|
|
903
|
+
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
904
|
+
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
905
|
+
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
906
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
907
|
+
expect(metadata).toEqual({
|
|
908
|
+
source: gitUrl,
|
|
909
|
+
type: 'github-release',
|
|
910
|
+
releaseTag: 'v1.0.0',
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
it('should fallback to git clone if github release download fails and user consents', async () => {
|
|
914
|
+
mockDownloadFromGithubRelease.mockResolvedValue({
|
|
915
|
+
success: false,
|
|
916
|
+
failureReason: 'failed to download asset',
|
|
917
|
+
errorMessage: 'download failed',
|
|
918
|
+
type: 'github-release',
|
|
919
|
+
});
|
|
920
|
+
const requestConsent = vi.fn().mockResolvedValue(true);
|
|
921
|
+
await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, // Use github-release to force consent
|
|
922
|
+
requestConsent);
|
|
923
|
+
// It gets called once to ask for a git clone, and once to consent to
|
|
924
|
+
// the actual extension features.
|
|
925
|
+
expect(requestConsent).toHaveBeenCalledTimes(2);
|
|
926
|
+
expect(requestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
927
|
+
expect(mockGit.clone).toHaveBeenCalled();
|
|
928
|
+
const metadataPath = path.join(userExtensionsDir, extensionName, INSTALL_METADATA_FILENAME);
|
|
929
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
930
|
+
expect(metadata.type).toBe('git');
|
|
931
|
+
});
|
|
932
|
+
it('should throw an error if github release download fails and user denies consent', async () => {
|
|
933
|
+
mockDownloadFromGithubRelease.mockResolvedValue({
|
|
934
|
+
success: false,
|
|
935
|
+
errorMessage: 'download failed',
|
|
936
|
+
type: 'github-release',
|
|
937
|
+
});
|
|
938
|
+
const requestConsent = vi.fn().mockResolvedValue(false);
|
|
939
|
+
await expect(installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, requestConsent)).rejects.toThrow(`Failed to install extension ${gitUrl}: download failed`);
|
|
940
|
+
expect(requestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
941
|
+
expect(mockGit.clone).not.toHaveBeenCalled();
|
|
942
|
+
});
|
|
943
|
+
it('should fallback to git clone without consent if no release data is found on first install', async () => {
|
|
944
|
+
mockDownloadFromGithubRelease.mockResolvedValue({
|
|
945
|
+
success: false,
|
|
946
|
+
failureReason: 'no release data',
|
|
947
|
+
type: 'github-release',
|
|
948
|
+
});
|
|
949
|
+
const requestConsent = vi.fn().mockResolvedValue(true);
|
|
950
|
+
await installOrUpdateExtension({ source: gitUrl, type: 'git' }, requestConsent);
|
|
951
|
+
// We should not see the request to use git clone, this is a repo that
|
|
952
|
+
// has no github releases so it is the only install method.
|
|
953
|
+
expect(requestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Installing extension "gemini-test-extension"'));
|
|
954
|
+
expect(mockGit.clone).toHaveBeenCalled();
|
|
955
|
+
const metadataPath = path.join(userExtensionsDir, extensionName, INSTALL_METADATA_FILENAME);
|
|
956
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
957
|
+
expect(metadata.type).toBe('git');
|
|
958
|
+
});
|
|
959
|
+
it('should ask for consent if no release data is found for an existing github-release extension', async () => {
|
|
960
|
+
mockDownloadFromGithubRelease.mockResolvedValue({
|
|
961
|
+
success: false,
|
|
962
|
+
failureReason: 'no release data',
|
|
963
|
+
errorMessage: 'No release data found',
|
|
964
|
+
type: 'github-release',
|
|
965
|
+
});
|
|
966
|
+
const requestConsent = vi.fn().mockResolvedValue(true);
|
|
967
|
+
await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, // Note the type
|
|
968
|
+
requestConsent);
|
|
969
|
+
expect(requestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
970
|
+
expect(mockGit.clone).toHaveBeenCalled();
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
describe('uninstallExtension', () => {
|
|
975
|
+
it('should uninstall an extension by name', async () => {
|
|
976
|
+
const sourceExtDir = createExtension({
|
|
977
|
+
extensionsDir: userExtensionsDir,
|
|
978
|
+
name: 'my-local-extension',
|
|
979
|
+
version: '1.0.0',
|
|
980
|
+
});
|
|
981
|
+
await uninstallExtension('my-local-extension', false);
|
|
982
|
+
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
983
|
+
});
|
|
984
|
+
it('should uninstall an extension by name and retain existing extensions', async () => {
|
|
985
|
+
const sourceExtDir = createExtension({
|
|
986
|
+
extensionsDir: userExtensionsDir,
|
|
987
|
+
name: 'my-local-extension',
|
|
988
|
+
version: '1.0.0',
|
|
989
|
+
});
|
|
990
|
+
const otherExtDir = createExtension({
|
|
991
|
+
extensionsDir: userExtensionsDir,
|
|
992
|
+
name: 'other-extension',
|
|
993
|
+
version: '1.0.0',
|
|
994
|
+
});
|
|
995
|
+
await uninstallExtension('my-local-extension', false);
|
|
996
|
+
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
997
|
+
expect(loadExtensions(new ExtensionEnablementManager())).toHaveLength(1);
|
|
998
|
+
expect(fs.existsSync(otherExtDir)).toBe(true);
|
|
999
|
+
});
|
|
1000
|
+
it('should throw an error if the extension does not exist', async () => {
|
|
1001
|
+
await expect(uninstallExtension('nonexistent-extension', false)).rejects.toThrow('Extension not found.');
|
|
1002
|
+
});
|
|
1003
|
+
describe.each([true, false])('with isUpdate: %s', (isUpdate) => {
|
|
1004
|
+
it(`should ${isUpdate ? 'not ' : ''}log uninstall event`, async () => {
|
|
1005
|
+
createExtension({
|
|
1006
|
+
extensionsDir: userExtensionsDir,
|
|
1007
|
+
name: 'my-local-extension',
|
|
1008
|
+
version: '1.0.0',
|
|
1009
|
+
});
|
|
1010
|
+
await uninstallExtension('my-local-extension', isUpdate);
|
|
1011
|
+
if (isUpdate) {
|
|
1012
|
+
expect(mockLogExtensionUninstall).not.toHaveBeenCalled();
|
|
1013
|
+
expect(ExtensionUninstallEvent).not.toHaveBeenCalled();
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
expect(mockLogExtensionUninstall).toHaveBeenCalled();
|
|
1017
|
+
expect(ExtensionUninstallEvent).toHaveBeenCalledWith('my-local-extension', 'success');
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
it(`should ${isUpdate ? 'not ' : ''} alter the extension enablement configuration`, async () => {
|
|
1021
|
+
createExtension({
|
|
1022
|
+
extensionsDir: userExtensionsDir,
|
|
1023
|
+
name: 'test-extension',
|
|
1024
|
+
version: '1.0.0',
|
|
1025
|
+
});
|
|
1026
|
+
const enablementManager = new ExtensionEnablementManager();
|
|
1027
|
+
enablementManager.enable('test-extension', true, '/some/scope');
|
|
1028
|
+
await uninstallExtension('test-extension', isUpdate);
|
|
1029
|
+
const config = enablementManager.readConfig()['test-extension'];
|
|
1030
|
+
if (isUpdate) {
|
|
1031
|
+
expect(config).not.toBeUndefined();
|
|
1032
|
+
expect(config.overrides).toEqual(['/some/scope/*']);
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
expect(config).toBeUndefined();
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
it('should uninstall an extension by its source URL', async () => {
|
|
1040
|
+
const gitUrl = 'https://github.com/google/gemini-sql-extension.git';
|
|
1041
|
+
const sourceExtDir = createExtension({
|
|
1042
|
+
extensionsDir: userExtensionsDir,
|
|
1043
|
+
name: 'gemini-sql-extension',
|
|
1044
|
+
version: '1.0.0',
|
|
1045
|
+
installMetadata: {
|
|
1046
|
+
source: gitUrl,
|
|
1047
|
+
type: 'git',
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
await uninstallExtension(gitUrl, false);
|
|
1051
|
+
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
1052
|
+
expect(mockLogExtensionUninstall).toHaveBeenCalled();
|
|
1053
|
+
expect(ExtensionUninstallEvent).toHaveBeenCalledWith('gemini-sql-extension', 'success');
|
|
1054
|
+
});
|
|
1055
|
+
it('should fail to uninstall by URL if an extension has no install metadata', async () => {
|
|
1056
|
+
createExtension({
|
|
1057
|
+
extensionsDir: userExtensionsDir,
|
|
1058
|
+
name: 'no-metadata-extension',
|
|
1059
|
+
version: '1.0.0',
|
|
1060
|
+
// No installMetadata provided
|
|
1061
|
+
});
|
|
1062
|
+
await expect(uninstallExtension('https://github.com/google/no-metadata-extension', false)).rejects.toThrow('Extension not found.');
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
describe('disableExtension', () => {
|
|
1066
|
+
it('should disable an extension at the user scope', () => {
|
|
1067
|
+
createExtension({
|
|
1068
|
+
extensionsDir: userExtensionsDir,
|
|
1069
|
+
name: 'my-extension',
|
|
1070
|
+
version: '1.0.0',
|
|
1071
|
+
});
|
|
1072
|
+
disableExtension('my-extension', SettingScope.User);
|
|
1073
|
+
expect(isEnabled({
|
|
1074
|
+
name: 'my-extension',
|
|
1075
|
+
enabledForPath: tempWorkspaceDir,
|
|
1076
|
+
})).toBe(false);
|
|
1077
|
+
});
|
|
1078
|
+
it('should disable an extension at the workspace scope', () => {
|
|
1079
|
+
createExtension({
|
|
1080
|
+
extensionsDir: userExtensionsDir,
|
|
1081
|
+
name: 'my-extension',
|
|
1082
|
+
version: '1.0.0',
|
|
1083
|
+
});
|
|
1084
|
+
disableExtension('my-extension', SettingScope.Workspace, tempWorkspaceDir);
|
|
1085
|
+
expect(isEnabled({
|
|
1086
|
+
name: 'my-extension',
|
|
1087
|
+
enabledForPath: tempHomeDir,
|
|
1088
|
+
})).toBe(true);
|
|
1089
|
+
expect(isEnabled({
|
|
1090
|
+
name: 'my-extension',
|
|
1091
|
+
enabledForPath: tempWorkspaceDir,
|
|
1092
|
+
})).toBe(false);
|
|
1093
|
+
});
|
|
1094
|
+
it('should handle disabling the same extension twice', () => {
|
|
1095
|
+
createExtension({
|
|
1096
|
+
extensionsDir: userExtensionsDir,
|
|
1097
|
+
name: 'my-extension',
|
|
1098
|
+
version: '1.0.0',
|
|
1099
|
+
});
|
|
1100
|
+
disableExtension('my-extension', SettingScope.User);
|
|
1101
|
+
disableExtension('my-extension', SettingScope.User);
|
|
1102
|
+
expect(isEnabled({
|
|
1103
|
+
name: 'my-extension',
|
|
1104
|
+
enabledForPath: tempWorkspaceDir,
|
|
1105
|
+
})).toBe(false);
|
|
1106
|
+
});
|
|
1107
|
+
it('should throw an error if you request system scope', () => {
|
|
1108
|
+
expect(() => disableExtension('my-extension', SettingScope.System)).toThrow('System and SystemDefaults scopes are not supported.');
|
|
1109
|
+
});
|
|
1110
|
+
it('should log a disable event', () => {
|
|
1111
|
+
createExtension({
|
|
1112
|
+
extensionsDir: userExtensionsDir,
|
|
1113
|
+
name: 'ext1',
|
|
1114
|
+
version: '1.0.0',
|
|
1115
|
+
});
|
|
1116
|
+
disableExtension('ext1', SettingScope.Workspace);
|
|
1117
|
+
expect(mockLogExtensionDisable).toHaveBeenCalled();
|
|
1118
|
+
expect(ExtensionDisableEvent).toHaveBeenCalledWith('ext1', SettingScope.Workspace);
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
describe('enableExtension', () => {
|
|
1122
|
+
afterAll(() => {
|
|
1123
|
+
vi.restoreAllMocks();
|
|
1124
|
+
});
|
|
1125
|
+
const getActiveExtensions = () => {
|
|
1126
|
+
const manager = new ExtensionEnablementManager();
|
|
1127
|
+
const extensions = loadExtensions(manager);
|
|
1128
|
+
const activeExtensions = annotateActiveExtensions(extensions, tempWorkspaceDir, manager);
|
|
1129
|
+
return activeExtensions.filter((e) => e.isActive);
|
|
1130
|
+
};
|
|
1131
|
+
it('should enable an extension at the user scope', () => {
|
|
1132
|
+
createExtension({
|
|
1133
|
+
extensionsDir: userExtensionsDir,
|
|
1134
|
+
name: 'ext1',
|
|
1135
|
+
version: '1.0.0',
|
|
1136
|
+
});
|
|
1137
|
+
disableExtension('ext1', SettingScope.User);
|
|
1138
|
+
let activeExtensions = getActiveExtensions();
|
|
1139
|
+
expect(activeExtensions).toHaveLength(0);
|
|
1140
|
+
enableExtension('ext1', SettingScope.User);
|
|
1141
|
+
activeExtensions = getActiveExtensions();
|
|
1142
|
+
expect(activeExtensions).toHaveLength(1);
|
|
1143
|
+
expect(activeExtensions[0].name).toBe('ext1');
|
|
1144
|
+
});
|
|
1145
|
+
it('should enable an extension at the workspace scope', () => {
|
|
1146
|
+
createExtension({
|
|
1147
|
+
extensionsDir: userExtensionsDir,
|
|
1148
|
+
name: 'ext1',
|
|
1149
|
+
version: '1.0.0',
|
|
1150
|
+
});
|
|
1151
|
+
disableExtension('ext1', SettingScope.Workspace);
|
|
1152
|
+
let activeExtensions = getActiveExtensions();
|
|
1153
|
+
expect(activeExtensions).toHaveLength(0);
|
|
1154
|
+
enableExtension('ext1', SettingScope.Workspace);
|
|
1155
|
+
activeExtensions = getActiveExtensions();
|
|
1156
|
+
expect(activeExtensions).toHaveLength(1);
|
|
1157
|
+
expect(activeExtensions[0].name).toBe('ext1');
|
|
1158
|
+
});
|
|
1159
|
+
it('should log an enable event', () => {
|
|
1160
|
+
createExtension({
|
|
1161
|
+
extensionsDir: userExtensionsDir,
|
|
1162
|
+
name: 'ext1',
|
|
1163
|
+
version: '1.0.0',
|
|
1164
|
+
});
|
|
1165
|
+
disableExtension('ext1', SettingScope.Workspace);
|
|
1166
|
+
enableExtension('ext1', SettingScope.Workspace);
|
|
1167
|
+
expect(mockLogExtensionEnable).toHaveBeenCalled();
|
|
1168
|
+
expect(ExtensionEnableEvent).toHaveBeenCalledWith('ext1', SettingScope.Workspace);
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
function isEnabled(options) {
|
|
1173
|
+
const manager = new ExtensionEnablementManager();
|
|
1174
|
+
return manager.isEnabled(options.name, options.enabledForPath);
|
|
1175
|
+
}
|
|
1176
|
+
//# sourceMappingURL=extension.test.js.map
|