Download packages/cli/src/config/extensions/extensionUpdates.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 9.51 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/config/extensions/extensionUpdates.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/config/extensions/extensionUpdates.test.ts
-
curl -L -o extensionUpdates.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/config/extensions/extensionUpdates.test.ts
9.51 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | |
| import * as fs from 'node:fs'; | |
| import { getMissingSettings } from './extensionSettings.js'; | |
| import type { ExtensionConfig } from '../extension.js'; | |
| import { | |
| debugLogger, | |
| type ExtensionInstallMetadata, | |
| type GeminiCLIExtension, | |
| coreEvents, | |
| } from '@google/gemini-cli-core'; | |
| import { ExtensionManager } from '../extension-manager.js'; | |
| import { createTestMergedSettings } from '../settings.js'; | |
| import { isWorkspaceTrusted } from '../trustedFolders.js'; | |
| // --- Mocks --- | |
| vi.mock('node:fs', async (importOriginal) => { | |
| const actual = await importOriginal<typeof import('node:fs')>(); | |
| return { | |
| ...actual, | |
| existsSync: vi.fn(), | |
| statSync: vi.fn(), | |
| lstatSync: vi.fn(), | |
| realpathSync: vi.fn((p) => p), | |
| promises: { | |
| ...actual.promises, | |
| mkdir: vi.fn(), | |
| readdir: vi.fn(), | |
| writeFile: vi.fn(), | |
| rm: vi.fn(), | |
| cp: vi.fn(), | |
| readFile: vi.fn(), | |
| lstat: vi.fn(), | |
| chmod: vi.fn(), | |
| }, | |
| }; | |
| }); | |
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); | |
| return { | |
| ...actual, | |
| KeychainTokenStorage: vi.fn(), | |
| debugLogger: { | |
| warn: vi.fn(), | |
| error: vi.fn(), | |
| log: vi.fn(), | |
| }, | |
| coreEvents: { | |
| emitFeedback: vi.fn(), | |
| on: vi.fn(), | |
| off: vi.fn(), | |
| emitConsoleLog: vi.fn(), | |
| }, | |
| loadSkillsFromDir: vi.fn().mockResolvedValue([]), | |
| loadAgentsFromDirectory: vi | |
| .fn() | |
| .mockResolvedValue({ agents: [], errors: [] }), | |
| logExtensionInstallEvent: vi.fn().mockResolvedValue(undefined), | |
| logExtensionUpdateEvent: vi.fn().mockResolvedValue(undefined), | |
| logExtensionUninstall: vi.fn().mockResolvedValue(undefined), | |
| logExtensionEnable: vi.fn().mockResolvedValue(undefined), | |
| logExtensionDisable: vi.fn().mockResolvedValue(undefined), | |
| Config: vi.fn().mockImplementation(() => ({ | |
| getEnableExtensionReloading: vi.fn().mockReturnValue(true), | |
| })), | |
| KeychainService: class { | |
| isAvailable = vi.fn().mockResolvedValue(true); | |
| getPassword = vi.fn().mockResolvedValue('test-key'); | |
| setPassword = vi.fn().mockResolvedValue(undefined); | |
| }, | |
| ExtensionIntegrityManager: class { | |
| verify = vi.fn().mockResolvedValue('verified'); | |
| store = vi.fn().mockResolvedValue(undefined); | |
| }, | |
| IntegrityDataStatus: { | |
| VERIFIED: 'verified', | |
| MISSING: 'missing', | |
| INVALID: 'invalid', | |
| }, | |
| }; | |
| }); | |
| vi.mock('./consent.js', () => ({ | |
| maybeRequestConsentOrFail: vi.fn().mockResolvedValue(undefined), | |
| })); | |
| vi.mock('./extensionSettings.js', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('./extensionSettings.js')>(); | |
| return { | |
| ...actual, | |
| getEnvContents: vi.fn().mockResolvedValue({}), | |
| getMissingSettings: vi.fn(), // We will mock this implementation per test | |
| }; | |
| }); | |
| vi.mock('../trustedFolders.js', () => ({ | |
| isWorkspaceTrusted: vi.fn().mockReturnValue({ isTrusted: true }), // Default to trusted to simplify flow | |
| loadTrustedFolders: vi.fn().mockReturnValue({ | |
| setValue: vi.fn().mockResolvedValue(undefined), | |
| }), | |
| TrustLevel: { TRUST_FOLDER: 'TRUST_FOLDER' }, | |
| })); | |
| // Mock ExtensionStorage to avoid real FS paths | |
| vi.mock('./storage.js', () => ({ | |
| ExtensionStorage: class { | |
| constructor(public name: string) {} | |
| getExtensionDir() { | |
| return `/mock/extensions/${this.name}`; | |
| } | |
| static getUserExtensionsDir() { | |
| return '/mock/extensions'; | |
| } | |
| static createTmpDir() { | |
| return Promise.resolve('/mock/tmp'); | |
| } | |
| }, | |
| })); | |
| vi.mock('os', async (importOriginal) => { | |
| const mockedOs = await importOriginal<typeof import('node:os')>(); | |
| return { | |
| ...mockedOs, | |
| homedir: vi.fn().mockReturnValue('/mock/home'), | |
| }; | |
| }); | |
| describe('extensionUpdates', () => { | |
| let tempWorkspaceDir: string; | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| // Default fs mocks | |
| vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined); | |
| vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined); | |
| vi.mocked(fs.promises.rm).mockResolvedValue(undefined); | |
| vi.mocked(fs.promises.cp).mockResolvedValue(undefined); | |
| vi.mocked(fs.promises.readdir).mockResolvedValue([]); | |
| vi.mocked(fs.promises.lstat).mockResolvedValue({ | |
| isDirectory: () => true, | |
| mode: 0o755, | |
| } as unknown as fs.Stats); | |
| vi.mocked(fs.promises.chmod).mockResolvedValue(undefined); | |
| vi.mocked(isWorkspaceTrusted).mockReturnValue({ | |
| isTrusted: true, | |
| source: 'file', | |
| }); | |
| vi.mocked(getMissingSettings).mockResolvedValue([]); | |
| // Allow directories to exist by default to satisfy Config/WorkspaceContext checks | |
| vi.mocked(fs.existsSync).mockReturnValue(true); | |
| vi.mocked(fs.statSync).mockReturnValue({ | |
| isDirectory: () => true, | |
| } as unknown as fs.Stats); | |
| vi.mocked(fs.lstatSync).mockReturnValue({ | |
| isDirectory: () => true, | |
| } as unknown as fs.Stats); | |
| vi.mocked(fs.realpathSync).mockImplementation((p) => p as string); | |
| tempWorkspaceDir = '/mock/workspace'; | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| describe('ExtensionManager integration', () => { | |
| it('should warn about missing settings after update', async () => { | |
| // 1. Setup Data | |
| const newConfig: ExtensionConfig = { | |
| name: 'test-ext', | |
| version: '1.1.0', | |
| settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }], | |
| }; | |
| const previousConfig: ExtensionConfig = { | |
| name: 'test-ext', | |
| version: '1.0.0', | |
| settings: [], | |
| }; | |
| const installMetadata: ExtensionInstallMetadata = { | |
| source: '/mock/source', | |
| type: 'local', | |
| autoUpdate: true, | |
| }; | |
| // 2. Setup Manager | |
| const manager = new ExtensionManager({ | |
| workspaceDir: tempWorkspaceDir, | |
| settings: createTestMergedSettings({ | |
| telemetry: { enabled: false }, | |
| experimental: { extensionConfig: true }, | |
| }), | |
| requestConsent: vi.fn().mockResolvedValue(true), | |
| requestSetting: null, | |
| }); | |
| // 3. Mock Internal Manager Methods | |
| vi.spyOn(manager, 'loadExtensionConfig').mockResolvedValue(newConfig); | |
| vi.spyOn(manager, 'getExtensions').mockReturnValue([ | |
| { | |
| name: 'test-ext', | |
| version: '1.0.0', | |
| installMetadata, | |
| path: '/mock/extensions/test-ext', | |
| contextFiles: [], | |
| mcpServers: {}, | |
| hooks: undefined, | |
| isActive: true, | |
| id: 'test-id', | |
| settings: [], | |
| resolvedSettings: [], | |
| skills: [], | |
| } as unknown as GeminiCLIExtension, | |
| ]); | |
| vi.spyOn(manager, 'uninstallExtension').mockResolvedValue(undefined); | |
| // Mock loadExtension to return something so the method doesn't crash at the end | |
| vi.spyOn(manager, 'loadExtension').mockResolvedValue({ | |
| name: 'test-ext', | |
| version: '1.1.0', | |
| } as unknown as GeminiCLIExtension); | |
| // 4. Mock External Helpers | |
| // This is the key fix: we explicitly mock `getMissingSettings` to return | |
| // the result we expect, avoiding any real FS or logic execution during the update. | |
| vi.mocked(getMissingSettings).mockResolvedValue([ | |
| { | |
| name: 's1', | |
| description: 'd1', | |
| envVar: 'VAR1', | |
| }, | |
| ]); | |
| // 5. Execute | |
| await manager.installOrUpdateExtension(installMetadata, previousConfig); | |
| // 6. Assert | |
| expect(debugLogger.warn).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| 'Extension "test-ext" has missing settings: s1', | |
| ), | |
| ); | |
| expect(coreEvents.emitFeedback).toHaveBeenCalledWith( | |
| 'warning', | |
| expect.stringContaining( | |
| 'Please run "gemini extensions config test-ext [setting-name]"', | |
| ), | |
| ); | |
| }); | |
| it('should store integrity data after update', async () => { | |
| const newConfig: ExtensionConfig = { | |
| name: 'test-ext', | |
| version: '1.1.0', | |
| }; | |
| const previousConfig: ExtensionConfig = { | |
| name: 'test-ext', | |
| version: '1.0.0', | |
| }; | |
| const installMetadata: ExtensionInstallMetadata = { | |
| source: '/mock/source', | |
| type: 'local', | |
| }; | |
| const manager = new ExtensionManager({ | |
| workspaceDir: tempWorkspaceDir, | |
| settings: createTestMergedSettings(), | |
| requestConsent: vi.fn().mockResolvedValue(true), | |
| requestSetting: null, | |
| }); | |
| await manager.loadExtensions(); | |
| vi.spyOn(manager, 'loadExtensionConfig').mockResolvedValue(newConfig); | |
| vi.spyOn(manager, 'getExtensions').mockReturnValue([ | |
| { | |
| name: 'test-ext', | |
| version: '1.0.0', | |
| installMetadata, | |
| path: '/mock/extensions/test-ext', | |
| isActive: true, | |
| } as unknown as GeminiCLIExtension, | |
| ]); | |
| vi.spyOn(manager, 'uninstallExtension').mockResolvedValue(undefined); | |
| vi.spyOn(manager, 'loadExtension').mockResolvedValue({ | |
| name: 'test-ext', | |
| version: '1.1.0', | |
| } as unknown as GeminiCLIExtension); | |
| const storeSpy = vi.spyOn(manager, 'storeExtensionIntegrity'); | |
| await manager.installOrUpdateExtension(installMetadata, previousConfig); | |
| expect(storeSpy).toHaveBeenCalledWith('test-ext', installMetadata); | |
| }); | |
| }); | |
| }); | |