diff --git a/packages/cli/src/ui/commands/extensionsCommand.test.ts b/packages/cli/src/ui/commands/extensionsCommand.test.ts index cc862b6c42..c873050490 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.test.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.test.ts @@ -21,6 +21,10 @@ import { ConfigExtensionDialog, type ConfigExtensionDialogProps, } from '../components/ConfigExtensionDialog.js'; +import { + ExtensionRegistryView, + type ExtensionRegistryViewProps, +} from '../components/views/ExtensionRegistryView.js'; import { type CommandContext, type SlashCommand } from './types.js'; import { @@ -39,6 +43,8 @@ import { } from '../../config/extension-manager.js'; import { SettingScope } from '../../config/settings.js'; import { stat } from 'node:fs/promises'; +import { type RegistryExtension } from '../../config/extensionRegistryClient.js'; +import { waitFor } from '../../test-utils/async.js'; vi.mock('../../config/extension-manager.js', async (importOriginal) => { const actual = @@ -167,6 +173,7 @@ describe('extensionsCommand', () => { }, ui: { dispatchExtensionStateUpdate: mockDispatchExtensionState, + removeComponent: vi.fn(), }, }); }); @@ -429,6 +436,61 @@ describe('extensionsCommand', () => { throw new Error('Explore action not found'); } + it('should return ExtensionRegistryView custom dialog when experimental.extensionRegistry is true', async () => { + mockContext.services.settings.merged.experimental.extensionRegistry = true; + + const result = await exploreAction(mockContext, ''); + + expect(result).toBeDefined(); + if (result?.type !== 'custom_dialog') { + throw new Error('Expected custom_dialog'); + } + + const component = + result.component as ReactElement; + expect(component.type).toBe(ExtensionRegistryView); + expect(component.props.extensionManager).toBe(mockExtensionLoader); + }); + + it('should handle onSelect and onClose in ExtensionRegistryView', async () => { + mockContext.services.settings.merged.experimental.extensionRegistry = true; + + const result = await exploreAction(mockContext, ''); + if (result?.type !== 'custom_dialog') { + throw new Error('Expected custom_dialog'); + } + + const component = + result.component as ReactElement; + + const extension = { + extensionName: 'test-ext', + url: 'https://github.com/test/ext.git', + } as RegistryExtension; + + vi.mocked(inferInstallMetadata).mockResolvedValue({ + source: extension.url, + type: 'git', + }); + mockInstallExtension.mockResolvedValue({ name: extension.url }); + + // Call onSelect + component.props.onSelect?.(extension); + + await waitFor(() => { + expect(inferInstallMetadata).toHaveBeenCalledWith(extension.url); + expect(mockInstallExtension).toHaveBeenCalledWith({ + source: extension.url, + type: 'git', + }); + }); + expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(1); + + // Call onClose + component.props.onClose?.(); + expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(2); + }); + it("should add an info message and call 'open' in a non-sandbox environment", async () => { // Ensure no special environment variables that would affect behavior vi.stubEnv('NODE_ENV', ''); diff --git a/packages/cli/src/ui/commands/extensionsCommand.ts b/packages/cli/src/ui/commands/extensionsCommand.ts index 0a8a8d74e3..842a680a14 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.ts @@ -280,7 +280,9 @@ async function exploreAction( type: 'custom_dialog' as const, component: React.createElement(ExtensionRegistryView, { onSelect: (extension) => { - debugLogger.debug(`Selected extension: ${extension.extensionName}`); + debugLogger.log(`Selected extension: ${extension.extensionName}`); + void installAction(context, extension.url); + context.ui.removeComponent(); }, onClose: () => context.ui.removeComponent(), extensionManager, diff --git a/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx b/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx index 1f6fba96ea..394eba3a2a 100644 --- a/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx +++ b/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx @@ -24,7 +24,7 @@ import { useRegistrySearch } from '../../hooks/useRegistrySearch.js'; import { useUIState } from '../../contexts/UIStateContext.js'; -interface ExtensionRegistryViewProps { +export interface ExtensionRegistryViewProps { onSelect?: (extension: RegistryExtension) => void; onClose?: () => void; extensionManager: ExtensionManager;