Add ExtensionLoader interface, use that on Config object (#12116)

This commit is contained in:
Jacob MacDonald
2025-10-28 09:04:30 -07:00
committed by GitHub
parent 25f27509c0
commit 1b302deeff
35 changed files with 619 additions and 505 deletions

View File

@@ -12,6 +12,7 @@ import {
beforeEach,
afterEach,
type Mock,
type MockedObject,
} from 'vitest';
import { render, cleanup } from 'ink-testing-library';
import { AppContainer } from './AppContainer.js';
@@ -131,11 +132,13 @@ import { useKeypress, type Key } from './hooks/useKeypress.js';
import { measureElement } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { ShellExecutionService } from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
let mockSettings: LoadedSettings;
let mockInitResult: InitializationResult;
let mockExtensionManager: MockedObject<ExtensionManager>;
// Create typed mocks for all hooks
const mockedUseQuotaAndFallback = useQuotaAndFallback as Mock;
@@ -282,6 +285,15 @@ describe('AppContainer State Management', () => {
// Mock config's getTargetDir to return consistent workspace directory
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
mockExtensionManager = vi.mockObject({
getExtensions: vi.fn().mockReturnValue([]),
setRequestConsent: vi.fn(),
setRequestSetting: vi.fn(),
} as unknown as ExtensionManager);
vi.spyOn(mockConfig, 'getExtensionLoader').mockReturnValue(
mockExtensionManager,
);
// Mock LoadedSettings
mockSettings = {
merged: {

View File

@@ -98,7 +98,7 @@ import {
useExtensionUpdates,
} from './hooks/useExtensionUpdates.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { ExtensionManager } from '../config/extension-manager.js';
import { type ExtensionManager } from '../config/extension-manager.js';
import { requestConsentInteractive } from '../config/extensions/consent.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@@ -168,21 +168,12 @@ export const AppContainer = (props: AppContainerProps) => {
null,
);
const extensions = config.getExtensions();
const [extensionManager] = useState<ExtensionManager>(
new ExtensionManager({
enabledExtensionOverrides: config.getEnabledExtensions(),
workspaceDir: config.getWorkingDir(),
requestConsent: (description) =>
requestConsentInteractive(
description,
addConfirmUpdateExtensionRequest,
),
// TODO: Support requesting settings in the interactive CLI
requestSetting: null,
loadedSettings: settings,
}),
const extensionManager = config.getExtensionLoader() as ExtensionManager;
// We are in the interactive CLI, update how we request consent and settings.
extensionManager.setRequestConsent((description) =>
requestConsentInteractive(description, addConfirmUpdateExtensionRequest),
);
extensionManager.setRequestSetting();
const { addConfirmUpdateExtensionRequest, confirmUpdateExtensionRequests } =
useConfirmUpdateRequests();
@@ -190,7 +181,7 @@ export const AppContainer = (props: AppContainerProps) => {
extensionsUpdateState,
extensionsUpdateStateInternal,
dispatchExtensionStateUpdate,
} = useExtensionUpdates(extensions, extensionManager, historyManager.addItem);
} = useExtensionUpdates(extensionManager, historyManager.addItem);
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
const openPermissionsDialog = useCallback(
@@ -548,7 +539,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensions(),
config.getExtensionLoader(),
config.isTrustedFolder(),
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(),

View File

@@ -103,7 +103,7 @@ export const directoryCommand: SlashCommand = {
],
config.getDebugMode(),
config.getFileService(),
config.getExtensions(),
config.getExtensionLoader(),
config.getFolderTrust(),
context.services.settings.merged.context?.importFormat ||
'tree', // Use setting or default to 'tree'

View File

@@ -13,6 +13,7 @@ import { MessageType } from '../types.js';
import type { LoadedSettings } from '../../config/settings.js';
import {
getErrorMessage,
SimpleExtensionLoader,
type FileDiscoveryService,
} from '@google/gemini-cli-core';
import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js';
@@ -72,6 +73,7 @@ describe('memoryCommand', () => {
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
},
},
});
@@ -176,6 +178,7 @@ describe('memoryCommand', () => {
getWorkingDir: () => '/test/dir',
getDebugMode: () => false,
getFileService: () => ({}) as FileDiscoveryService,
getExtensionLoader: () => new SimpleExtensionLoader([]),
getExtensions: () => [],
shouldLoadMemoryFromIncludeDirectories: () => false,
getWorkspaceContext: () => ({

View File

@@ -91,7 +91,7 @@ export const memoryCommand: SlashCommand = {
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensions(),
config.getExtensionLoader(),
config.isTrustedFolder(),
settings.merged.context?.importFormat || 'tree',
config.getFileFilteringOptions(),

View File

@@ -10,7 +10,7 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { createExtension } from '../../test-utils/createExtension.js';
import { useExtensionUpdates } from './useExtensionUpdates.js';
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
import { GEMINI_DIR } from '@google/gemini-cli-core';
import { render } from 'ink-testing-library';
import { MessageType } from '../types.js';
import {
@@ -57,7 +57,7 @@ describe('useExtensionUpdates', () => {
workspaceDir: tempHomeDir,
requestConsent: vi.fn(),
requestSetting: vi.fn(),
loadedSettings: loadSettings(),
settings: loadSettings().merged,
});
});
@@ -66,11 +66,10 @@ describe('useExtensionUpdates', () => {
});
it('should check for updates and log a message if an update is available', async () => {
const extensions = [
vi.spyOn(extensionManager, 'getExtensions').mockReturnValue([
{
name: 'test-extension',
id: 'test-extension-id',
type: 'git',
version: '1.0.0',
path: '/some/path',
isActive: true,
@@ -81,7 +80,7 @@ describe('useExtensionUpdates', () => {
},
contextFiles: [],
},
];
]);
const addItem = vi.fn();
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
@@ -97,11 +96,7 @@ describe('useExtensionUpdates', () => {
);
function TestComponent() {
useExtensionUpdates(
extensions as GeminiCLIExtension[],
extensionManager,
addItem,
);
useExtensionUpdates(extensionManager, addItem);
return null;
}
@@ -119,7 +114,7 @@ describe('useExtensionUpdates', () => {
});
it('should check for updates and automatically update if autoUpdate is true', async () => {
const extensionDir = createExtension({
createExtension({
extensionsDir: userExtensionsDir,
name: 'test-extension',
version: '1.0.0',
@@ -129,7 +124,6 @@ describe('useExtensionUpdates', () => {
autoUpdate: true,
},
});
const extension = extensionManager.loadExtension(extensionDir)!;
const addItem = vi.fn();
@@ -151,8 +145,9 @@ describe('useExtensionUpdates', () => {
name: '',
});
extensionManager.loadExtensions();
function TestComponent() {
useExtensionUpdates([extension], extensionManager, addItem);
useExtensionUpdates(extensionManager, addItem);
return null;
}
@@ -173,7 +168,7 @@ describe('useExtensionUpdates', () => {
});
it('should batch update notifications for multiple extensions', async () => {
const extensionDir1 = createExtension({
createExtension({
extensionsDir: userExtensionsDir,
name: 'test-extension-1',
version: '1.0.0',
@@ -183,7 +178,7 @@ describe('useExtensionUpdates', () => {
autoUpdate: true,
},
});
const extensionDir2 = createExtension({
createExtension({
extensionsDir: userExtensionsDir,
name: 'test-extension-2',
version: '2.0.0',
@@ -194,10 +189,7 @@ describe('useExtensionUpdates', () => {
},
});
const extensions = [
extensionManager.loadExtension(extensionDir1)!,
extensionManager.loadExtension(extensionDir2)!,
];
extensionManager.loadExtensions();
const addItem = vi.fn();
@@ -233,7 +225,7 @@ describe('useExtensionUpdates', () => {
});
function TestComponent() {
useExtensionUpdates(extensions, extensionManager, addItem);
useExtensionUpdates(extensionManager, addItem);
return null;
}
@@ -262,11 +254,10 @@ describe('useExtensionUpdates', () => {
});
it('should batch update notifications for multiple extensions with autoUpdate: false', async () => {
const extensions = [
vi.spyOn(extensionManager, 'getExtensions').mockReturnValue([
{
name: 'test-extension-1',
id: 'test-extension-1-id',
type: 'git',
version: '1.0.0',
path: '/some/path1',
isActive: true,
@@ -281,7 +272,6 @@ describe('useExtensionUpdates', () => {
name: 'test-extension-2',
id: 'test-extension-2-id',
type: 'git',
version: '2.0.0',
path: '/some/path2',
isActive: true,
@@ -292,7 +282,7 @@ describe('useExtensionUpdates', () => {
},
contextFiles: [],
},
];
]);
const addItem = vi.fn();
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
@@ -318,11 +308,7 @@ describe('useExtensionUpdates', () => {
);
function TestComponent() {
useExtensionUpdates(
extensions as GeminiCLIExtension[],
extensionManager,
addItem,
);
useExtensionUpdates(extensionManager, addItem);
return null;
}

View File

@@ -78,7 +78,6 @@ export const useConfirmUpdateRequests = () => {
};
export const useExtensionUpdates = (
extensions: GeminiCLIExtension[],
extensionManager: ExtensionManager,
addItem: UseHistoryManagerReturn['addItem'],
) => {
@@ -86,6 +85,7 @@ export const useExtensionUpdates = (
extensionUpdatesReducer,
initialExtensionUpdatesState,
);
const extensions = extensionManager.getExtensions();
useEffect(() => {
const extensionsToCheck = extensions.filter((extension) => {