/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { ExtensionManager } from './extension-manager.js'; import { createTestMergedSettings } from './settings.js'; import { loadAgentsFromDirectory, loadSkillsFromDir, } from '@google/gemini-cli-core'; let currentTempHome = ''; vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: () => currentTempHome, debugLogger: { log: vi.fn(), error: vi.fn(), warn: vi.fn(), }, loadAgentsFromDirectory: vi.fn().mockImplementation(async () => ({ agents: [], errors: [], })), loadSkillsFromDir: vi.fn().mockImplementation(async () => []), }; }); describe('ExtensionManager Settings Scope', () => { const extensionName = 'test-extension'; let tempWorkspace: string; let extensionsDir: string; let extensionDir: string; beforeEach(async () => { vi.mocked(loadAgentsFromDirectory).mockResolvedValue({ agents: [], errors: [], }); vi.mocked(loadSkillsFromDir).mockResolvedValue([]); currentTempHome = fs.mkdtempSync( path.join(os.tmpdir(), 'gemini-cli-test-home-'), ); tempWorkspace = fs.mkdtempSync( path.join(os.tmpdir(), 'gemini-cli-test-workspace-'), ); extensionsDir = path.join(currentTempHome, '.gemini', 'extensions'); extensionDir = path.join(extensionsDir, extensionName); fs.mkdirSync(extensionDir, { recursive: true }); // Create gemini-extension.json const extensionConfig = { name: extensionName, version: '1.0.0', settings: [ { name: 'Test Setting', envVar: 'TEST_SETTING', description: 'A test setting', }, ], }; fs.writeFileSync( path.join(extensionDir, 'gemini-extension.json'), JSON.stringify(extensionConfig), ); // Create install metadata const installMetadata = { source: extensionDir, type: 'local', }; fs.writeFileSync( path.join(extensionDir, 'install-metadata.json'), JSON.stringify(installMetadata), ); }); afterEach(() => { // Clean up files if needed, or rely on temp dir cleanup vi.clearAllMocks(); }); it('should prioritize workspace settings over user settings and report correct scope', async () => { // 1. Set User Setting const userSettingsPath = path.join(extensionDir, '.env'); fs.writeFileSync(userSettingsPath, 'TEST_SETTING=user-value'); // 2. Set Workspace Setting const workspaceSettingsPath = path.join(tempWorkspace, '.env'); fs.writeFileSync(workspaceSettingsPath, 'TEST_SETTING=workspace-value'); const extensionManager = new ExtensionManager({ workspaceDir: tempWorkspace, requestConsent: async () => true, requestSetting: async () => '', settings: createTestMergedSettings({ telemetry: { enabled: false }, experimental: { extensionConfig: true }, security: { folderTrust: { enabled: false } }, }), }); const extensions = await extensionManager.loadExtensions(); const extension = extensions.find((e) => e.name === extensionName); expect(extension).toBeDefined(); // Verify resolved settings const setting = extension?.resolvedSettings?.find( (s) => s.envVar === 'TEST_SETTING', ); expect(setting).toBeDefined(); expect(setting?.value).toBe('workspace-value'); expect(setting?.scope).toBe('workspace'); expect(setting?.source).toBe(workspaceSettingsPath); // Verify output string contains (Workspace - ) const output = extensionManager.toOutputString(extension!); expect(output).toContain( `Test Setting: workspace-value (Workspace - ${workspaceSettingsPath})`, ); }); it('should fallback to user settings if workspace setting is missing', async () => { // 1. Set User Setting const userSettingsPath = path.join(extensionDir, '.env'); fs.writeFileSync(userSettingsPath, 'TEST_SETTING=user-value'); // 2. No Workspace Setting const extensionManager = new ExtensionManager({ workspaceDir: tempWorkspace, requestConsent: async () => true, requestSetting: async () => '', settings: createTestMergedSettings({ telemetry: { enabled: false }, experimental: { extensionConfig: true }, security: { folderTrust: { enabled: false } }, }), }); const extensions = await extensionManager.loadExtensions(); const extension = extensions.find((e) => e.name === extensionName); expect(extension).toBeDefined(); // Verify resolved settings const setting = extension?.resolvedSettings?.find( (s) => s.envVar === 'TEST_SETTING', ); expect(setting).toBeDefined(); expect(setting?.value).toBe('user-value'); expect(setting?.scope).toBe('user'); expect(setting?.source?.endsWith(path.join(extensionName, '.env'))).toBe( true, ); // Verify output string contains (User - ) const output = extensionManager.toOutputString(extension!); expect(output).toContain( `Test Setting: user-value (User - ${userSettingsPath})`, ); }); it('should report unset if neither is present', async () => { // No settings files const extensionManager = new ExtensionManager({ workspaceDir: tempWorkspace, requestConsent: async () => true, requestSetting: async () => '', settings: createTestMergedSettings({ telemetry: { enabled: false }, experimental: { extensionConfig: true }, security: { folderTrust: { enabled: false } }, }), }); const extensions = await extensionManager.loadExtensions(); const extension = extensions.find((e) => e.name === extensionName); expect(extension).toBeDefined(); // Verify resolved settings const setting = extension?.resolvedSettings?.find( (s) => s.envVar === 'TEST_SETTING', ); expect(setting).toBeDefined(); expect(setting?.value).toBeUndefined(); expect(setting?.scope).toBeUndefined(); // Verify output string does not contain scope const output = extensionManager.toOutputString(extension!); expect(output).toContain('Test Setting: [not set]'); expect(output).not.toContain('Test Setting: [not set] (User)'); expect(output).not.toContain('Test Setting: [not set] (Workspace)'); }); });