Download packages/cli/src/commands/extensions/uninstall.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/commands/extensions/uninstall.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/commands/extensions/uninstall.test.ts
-
curl -L -o uninstall.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/commands/extensions/uninstall.test.ts
12.4 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { | |
| vi, | |
| describe, | |
| it, | |
| expect, | |
| beforeEach, | |
| afterEach, | |
| type Mock, | |
| } from 'vitest'; | |
| import { format } from 'node:util'; | |
| import { type Argv } from 'yargs'; | |
| import { handleUninstall, uninstallCommand } from './uninstall.js'; | |
| import { ExtensionManager } from '../../config/extension-manager.js'; | |
| import { loadSettings, type LoadedSettings } from '../../config/settings.js'; | |
| import { getErrorMessage } from '@google/gemini-cli-core'; | |
| // NOTE: This file uses vi.hoisted() mocks to enable testing of sequential | |
| // mock behaviors (mockResolvedValueOnce/mockRejectedValueOnce chaining). | |
| // The hoisted mocks persist across vi.clearAllMocks() calls, which is necessary | |
| // for testing partial failure scenarios in the multiple extension uninstall feature. | |
| // Hoisted mocks - these survive vi.clearAllMocks() | |
| const mockUninstallExtension = vi.hoisted(() => vi.fn()); | |
| const mockLoadExtensions = vi.hoisted(() => vi.fn()); | |
| const mockGetExtensions = vi.hoisted(() => vi.fn()); | |
| // Mock dependencies with hoisted functions | |
| vi.mock('../../config/extension-manager.js', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('../../config/extension-manager.js')>(); | |
| return { | |
| ...actual, | |
| ExtensionManager: vi.fn().mockImplementation(() => ({ | |
| uninstallExtension: mockUninstallExtension, | |
| loadExtensions: mockLoadExtensions, | |
| getExtensions: mockGetExtensions, | |
| setRequestConsent: vi.fn(), | |
| setRequestSetting: vi.fn(), | |
| })), | |
| }; | |
| }); | |
| // Mock dependencies | |
| const emitConsoleLog = vi.hoisted(() => vi.fn()); | |
| const debugLogger = vi.hoisted(() => ({ | |
| log: vi.fn((message, ...args) => { | |
| emitConsoleLog('log', format(message, ...args)); | |
| }), | |
| error: vi.fn((message, ...args) => { | |
| emitConsoleLog('error', format(message, ...args)); | |
| }), | |
| })); | |
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); | |
| return { | |
| ...actual, | |
| coreEvents: { | |
| emitConsoleLog, | |
| }, | |
| debugLogger, | |
| getErrorMessage: vi.fn(), | |
| }; | |
| }); | |
| vi.mock('../../config/settings.js'); | |
| vi.mock('../../config/extensions/consent.js', () => ({ | |
| requestConsentNonInteractive: vi.fn(), | |
| })); | |
| vi.mock('../../config/extensions/extensionSettings.js', () => ({ | |
| promptForSetting: vi.fn(), | |
| })); | |
| vi.mock('../utils.js', () => ({ | |
| exitCli: vi.fn(), | |
| })); | |
| describe('extensions uninstall command', () => { | |
| const mockLoadSettings = vi.mocked(loadSettings); | |
| const mockGetErrorMessage = vi.mocked(getErrorMessage); | |
| const mockExtensionManager = vi.mocked(ExtensionManager); | |
| beforeEach(async () => { | |
| mockLoadSettings.mockReturnValue({ | |
| merged: {}, | |
| } as unknown as LoadedSettings); | |
| }); | |
| afterEach(() => { | |
| mockLoadExtensions.mockClear(); | |
| mockUninstallExtension.mockClear(); | |
| mockGetExtensions.mockClear(); | |
| vi.clearAllMocks(); | |
| }); | |
| describe('handleUninstall', () => { | |
| it('should uninstall a single extension', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| mockUninstallExtension.mockResolvedValue(undefined); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| await handleUninstall({ names: ['my-extension'] }); | |
| expect(mockExtensionManager).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| workspaceDir: '/test/dir', | |
| }), | |
| ); | |
| expect(mockLoadExtensions).toHaveBeenCalled(); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith( | |
| 'my-extension', | |
| false, | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "my-extension" successfully uninstalled.', | |
| ); | |
| mockCwd.mockRestore(); | |
| }); | |
| it('should uninstall multiple extensions', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| mockUninstallExtension.mockResolvedValue(undefined); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| await handleUninstall({ names: ['ext1', 'ext2', 'ext3'] }); | |
| expect(mockUninstallExtension).toHaveBeenCalledTimes(3); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith('ext1', false); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith('ext2', false); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith('ext3', false); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext1" successfully uninstalled.', | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext2" successfully uninstalled.', | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext3" successfully uninstalled.', | |
| ); | |
| mockCwd.mockRestore(); | |
| }); | |
| it('should uninstall all extensions when --all flag is used', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| mockUninstallExtension.mockResolvedValue(undefined); | |
| mockGetExtensions.mockReturnValue([{ name: 'ext1' }, { name: 'ext2' }]); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| await handleUninstall({ all: true }); | |
| expect(mockUninstallExtension).toHaveBeenCalledTimes(2); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith('ext1', false); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith('ext2', false); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext1" successfully uninstalled.', | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext2" successfully uninstalled.', | |
| ); | |
| mockCwd.mockRestore(); | |
| }); | |
| it('should log a message if no extensions are installed and --all flag is used', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| mockGetExtensions.mockReturnValue([]); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| await handleUninstall({ all: true }); | |
| expect(mockUninstallExtension).not.toHaveBeenCalled(); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'No extensions currently installed.', | |
| ); | |
| mockCwd.mockRestore(); | |
| }); | |
| it('should report errors for failed uninstalls but continue with others', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| const mockProcessExit = vi | |
| .spyOn(process, 'exit') | |
| .mockImplementation((() => {}) as ( | |
| code?: string | number | null | undefined, | |
| ) => never); | |
| const error = new Error('Extension not found'); | |
| // Chain sequential mock behaviors - this works with hoisted mocks | |
| mockUninstallExtension | |
| .mockResolvedValueOnce(undefined) | |
| .mockRejectedValueOnce(error) | |
| .mockResolvedValueOnce(undefined); | |
| mockGetErrorMessage.mockReturnValue('Extension not found'); | |
| await handleUninstall({ names: ['ext1', 'ext2', 'ext3'] }); | |
| expect(mockUninstallExtension).toHaveBeenCalledTimes(3); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext1" successfully uninstalled.', | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'error', | |
| 'Failed to uninstall "ext2": Extension not found', | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'log', | |
| 'Extension "ext3" successfully uninstalled.', | |
| ); | |
| expect(mockProcessExit).toHaveBeenCalledWith(1); | |
| mockProcessExit.mockRestore(); | |
| mockCwd.mockRestore(); | |
| }); | |
| it('should exit with error code if all uninstalls fail', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| const mockProcessExit = vi | |
| .spyOn(process, 'exit') | |
| .mockImplementation((() => {}) as ( | |
| code?: string | number | null | undefined, | |
| ) => never); | |
| const error = new Error('Extension not found'); | |
| mockUninstallExtension.mockRejectedValue(error); | |
| mockGetErrorMessage.mockReturnValue('Extension not found'); | |
| await handleUninstall({ names: ['ext1', 'ext2'] }); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'error', | |
| 'Failed to uninstall "ext1": Extension not found', | |
| ); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'error', | |
| 'Failed to uninstall "ext2": Extension not found', | |
| ); | |
| expect(mockProcessExit).toHaveBeenCalledWith(1); | |
| mockProcessExit.mockRestore(); | |
| mockCwd.mockRestore(); | |
| }); | |
| it('should log an error message and exit with code 1 when initialization fails', async () => { | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| const mockProcessExit = vi | |
| .spyOn(process, 'exit') | |
| .mockImplementation((() => {}) as ( | |
| code?: string | number | null | undefined, | |
| ) => never); | |
| const error = new Error('Initialization failed'); | |
| mockLoadExtensions.mockRejectedValue(error); | |
| mockGetErrorMessage.mockReturnValue('Initialization failed message'); | |
| await handleUninstall({ names: ['my-extension'] }); | |
| expect(emitConsoleLog).toHaveBeenCalledWith( | |
| 'error', | |
| 'Initialization failed message', | |
| ); | |
| expect(mockProcessExit).toHaveBeenCalledWith(1); | |
| mockProcessExit.mockRestore(); | |
| mockCwd.mockRestore(); | |
| }); | |
| }); | |
| describe('uninstallCommand', () => { | |
| const command = uninstallCommand; | |
| it('should have correct command and describe', () => { | |
| expect(command.command).toBe('uninstall [names..]'); | |
| expect(command.describe).toBe('Uninstalls one or more extensions.'); | |
| }); | |
| describe('builder', () => { | |
| interface MockYargs { | |
| positional: Mock; | |
| option: Mock; | |
| check: Mock; | |
| } | |
| let yargsMock: MockYargs; | |
| beforeEach(() => { | |
| yargsMock = { | |
| positional: vi.fn().mockReturnThis(), | |
| option: vi.fn().mockReturnThis(), | |
| check: vi.fn().mockReturnThis(), | |
| }; | |
| }); | |
| it('should configure arguments and options', () => { | |
| (command.builder as (yargs: Argv) => Argv)( | |
| yargsMock as unknown as Argv, | |
| ); | |
| expect(yargsMock.positional).toHaveBeenCalledWith('names', { | |
| describe: | |
| 'The name(s) or source path(s) of the extension(s) to uninstall.', | |
| type: 'string', | |
| array: true, | |
| }); | |
| expect(yargsMock.option).toHaveBeenCalledWith('all', { | |
| type: 'boolean', | |
| describe: 'Uninstall all installed extensions.', | |
| default: false, | |
| }); | |
| expect(yargsMock.check).toHaveBeenCalled(); | |
| }); | |
| it('check function should throw for missing names and no --all flag', () => { | |
| (command.builder as (yargs: Argv) => Argv)( | |
| yargsMock as unknown as Argv, | |
| ); | |
| const checkCallback = yargsMock.check.mock.calls[0][0]; | |
| expect(() => checkCallback({ names: [], all: false })).toThrow( | |
| 'Please include at least one extension name to uninstall as a positional argument, or use the --all flag.', | |
| ); | |
| }); | |
| it('check function should pass if --all flag is used even without names', () => { | |
| (command.builder as (yargs: Argv) => Argv)( | |
| yargsMock as unknown as Argv, | |
| ); | |
| const checkCallback = yargsMock.check.mock.calls[0][0]; | |
| expect(() => checkCallback({ names: [], all: true })).not.toThrow(); | |
| }); | |
| }); | |
| it('handler should call handleUninstall', async () => { | |
| mockLoadExtensions.mockResolvedValue(undefined); | |
| mockUninstallExtension.mockResolvedValue(undefined); | |
| const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); | |
| interface TestArgv { | |
| names?: string[]; | |
| all?: boolean; | |
| _: string[]; | |
| $0: string; | |
| } | |
| const argv: TestArgv = { | |
| names: ['my-extension'], | |
| all: false, | |
| _: [], | |
| $0: '', | |
| }; | |
| await (command.handler as unknown as (args: TestArgv) => Promise<void>)( | |
| argv, | |
| ); | |
| expect(mockUninstallExtension).toHaveBeenCalledWith( | |
| 'my-extension', | |
| false, | |
| ); | |
| mockCwd.mockRestore(); | |
| }); | |
| }); | |
| }); | |