Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { IpcRendererEvent } from 'electron';
import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import { ErrorUI } from './components/ErrorBoundary';
import { ExtensionInstallModal } from './components/modals/ExtensionInstallModal';
import { useExtensionInstallModal } from './hooks/useExtensionInstallModal';
import { ExtensionInstallModal } from './components/ExtensionInstallModal';
import { ToastContainer } from 'react-toastify';
import { GoosehintsModal } from './components/GoosehintsModal';
import AnnouncementModal from './components/AnnouncementModal';
Expand Down Expand Up @@ -366,8 +365,6 @@ export default function App() {

const { getExtensions, addExtension, read } = useConfig();
const initAttemptedRef = useRef(false);
const { modalState, modalConfig, dismissModal, confirmInstall } =
useExtensionInstallModal(addExtension);

// Create a setView function for useChat hook - we'll use window.history instead of navigate
const setView = (view: View, viewOptions: ViewOptions = {}) => {
Expand Down Expand Up @@ -664,15 +661,6 @@ export default function App() {
};
}, []);

const handleExtensionConfirm = async () => {
const result = await confirmInstall();
if (result.success) {
console.log('Extension installation completed successfully');
} else {
console.error('Extension installation failed:', result.error);
}
};

if (fatalError) {
return <ErrorUI error={new Error(fatalError)} />;
}
Expand Down Expand Up @@ -704,14 +692,7 @@ export default function App() {
closeOnClick
pauseOnHover
/>
<ExtensionInstallModal
isOpen={modalState.isOpen}
modalType={modalState.modalType}
config={modalConfig}
onConfirm={handleExtensionConfirm}
onCancel={dismissModal}
isSubmitting={modalState.isPending}
/>
<ExtensionInstallModal addExtension={addExtension} />
<div className="relative w-screen h-screen overflow-hidden bg-background-muted flex flex-col">
<div className="titlebar-drag-region" />
<Routes>
Expand Down
161 changes: 161 additions & 0 deletions ui/desktop/src/components/ExtensionInstallModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import { ExtensionInstallModal } from './ExtensionInstallModal';
import { addExtensionFromDeepLink } from './settings/extensions/deeplink';

vi.mock('./settings/extensions/deeplink', () => ({
addExtensionFromDeepLink: vi.fn(),
}));

const mockElectron = {
getConfig: vi.fn(),
getAllowedExtensions: vi.fn(),
logInfo: vi.fn(),
on: vi.fn(),
off: vi.fn(),
};

(window as any).electron = mockElectron;

describe('ExtensionInstallModal', () => {
const mockAddExtension = vi.fn();

const getAddExtensionEventHandler = () => {
const addExtensionCall = mockElectron.on.mock.calls.find((call) => call[0] === 'add-extension');
expect(addExtensionCall).toBeDefined();
return addExtensionCall![1];
};

beforeEach(() => {
vi.clearAllMocks();
mockElectron.getConfig.mockReturnValue({
GOOSE_ALLOWLIST_WARNING: false,
});
});

afterEach(() => {
vi.clearAllMocks();
});

describe('Extension Request Handling', () => {
it('should handle trusted extension (default behaviour, no allowlist)', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue([]);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);

const eventHandler = getAddExtensionEventHandler();

await act(async () => {
await eventHandler({}, 'goose://extension?cmd=npx&arg=test-extension&name=TestExt');
});

expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Confirm Extension Installation')).toBeInTheDocument();
expect(screen.getByText(/TestExt extension/)).toBeInTheDocument();
expect(screen.getAllByRole('button')).toHaveLength(3);
});

it('should handle trusted extension (from allowlist)', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue(['npx test-extension']);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);

const eventHandler = getAddExtensionEventHandler();

await act(async () => {
await eventHandler({}, 'goose://extension?cmd=npx&arg=test-extension&name=AllowedExt');
});

expect(screen.getByText('Confirm Extension Installation')).toBeInTheDocument();
expect(screen.getAllByRole('button')).toHaveLength(3);
});

it('should handle warning mode', async () => {
mockElectron.getConfig.mockReturnValue({
GOOSE_ALLOWLIST_WARNING: true,
});
mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);

const eventHandler = getAddExtensionEventHandler();

await act(async () => {
await eventHandler(
{},
'goose://extension?cmd=npx&arg=untrusted-extension&name=UntrustedExt'
);
});

expect(screen.getByText('Install Untrusted Extension?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Install Anyway' })).toBeInTheDocument();
expect(screen.getAllByRole('button')).toHaveLength(3);
});

it('should handle blocked extension', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);

const eventHandler = getAddExtensionEventHandler();

await act(async () => {
await eventHandler({}, 'goose://extension?cmd=npx&arg=blocked-extension&name=BlockedExt');
});

expect(screen.getByText('Extension Installation Blocked')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument();
expect(screen.getAllByRole('button')).toHaveLength(2);
expect(screen.getByText(/Contact your administrator/)).toBeInTheDocument();
});
});

describe('Modal Actions', () => {
it('should dismiss modal correctly', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue([]);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);

const eventHandler = getAddExtensionEventHandler();

await act(async () => {
await eventHandler({}, 'goose://extension?cmd=npx&arg=test&name=Test');
});

expect(screen.getByRole('dialog')).toBeInTheDocument();

await act(async () => {
screen.getByRole('button', { name: 'No' }).click();
});

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('should handle successful extension installation', async () => {
vi.mocked(addExtensionFromDeepLink).mockResolvedValue(undefined);
mockElectron.getAllowedExtensions.mockResolvedValue([]);

render(<ExtensionInstallModal addExtension={mockAddExtension} />);

const eventHandler = getAddExtensionEventHandler();

await act(async () => {
await eventHandler({}, 'goose://extension?cmd=npx&arg=test&name=Test');
});

await act(async () => {
screen.getByRole('button', { name: 'Yes' }).click();
});

expect(addExtensionFromDeepLink).toHaveBeenCalledWith(
'goose://extension?cmd=npx&arg=test&name=Test',
mockAddExtension,
expect.any(Function)
);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
});
Loading
Loading