import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getMcpOperationKey } from '@shared/utils/extensionNormalizers'; import type { InstalledMcpEntry, McpCatalogItem } from '@shared/types/extensions'; interface StoreState { mcpInstallProgress: Record; installMcpServer: ReturnType; uninstallMcpServer: ReturnType; installErrors: Record; mcpGitHubStars: Record; cliStatus?: { flavor: 'claude' | 'agent_teams_orchestrator' } | null; } const storeState = {} as StoreState; vi.mock('@renderer/store', () => ({ useStore: (selector: (state: StoreState) => unknown) => selector(storeState), })); vi.mock('@renderer/api', () => ({ api: { openExternal: vi.fn(), }, })); vi.mock('@renderer/components/ui/badge', () => ({ Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children), })); vi.mock('@renderer/components/ui/button', () => ({ Button: ({ children, onClick, type = 'button', }: React.PropsWithChildren<{ onClick?: (event: React.MouseEvent) => void; type?: 'button' | 'submit' | 'reset'; }>) => React.createElement( 'button', { type, onClick, }, children ), })); vi.mock('@renderer/components/ui/tooltip', () => ({ Tooltip: ({ children }: React.PropsWithChildren) => React.createElement(React.Fragment, null, children), TooltipTrigger: ({ children }: React.PropsWithChildren) => React.createElement(React.Fragment, null, children), TooltipContent: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children), })); vi.mock('@renderer/components/extensions/common/InstallButton', () => ({ InstallButton: ({ state, errorMessage, }: { state?: string; errorMessage?: string; }) => React.createElement( 'button', { type: 'button', 'data-testid': 'install-button', 'data-state': state, 'data-error': errorMessage, }, 'Install' ), })); vi.mock('@renderer/components/extensions/common/SourceBadge', () => ({ SourceBadge: ({ source }: { source: string }) => React.createElement('span', null, source), })); vi.mock('@renderer/utils/formatters', () => ({ formatCompactNumber: (value: number) => String(value), formatRelativeTime: () => 'recently', })); vi.mock('lucide-react', () => { const Icon = (props: React.SVGProps) => React.createElement('svg', props); return { Clock: Icon, Cloud: Icon, Globe: Icon, KeyRound: Icon, Lock: Icon, Monitor: Icon, Star: Icon, Tag: Icon, Wrench: Icon, Github: Icon, }; }); import { McpServerCard } from '@renderer/components/extensions/mcp/McpServerCard'; function makeServer(): McpCatalogItem { return { id: 'io.github.upstash/context7', name: 'Context7', description: 'Docs server', source: 'official', installSpec: { type: 'stdio', npmPackage: '@upstash/context7-mcp', }, envVars: [], tools: [], requiresAuth: false, authHeaders: [], }; } describe('McpServerCard direct action safety', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.mcpInstallProgress = {}; storeState.installMcpServer = vi.fn(); storeState.uninstallMcpServer = vi.fn(); storeState.installErrors = {}; storeState.mcpGitHubStars = {}; storeState.cliStatus = null; }); afterEach(() => { document.body.innerHTML = ''; vi.unstubAllGlobals(); }); it('falls back to Manage for installed entries that cannot be safely uninstalled directly', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const onClick = vi.fn(); const installedEntry: InstalledMcpEntry = { name: 'context7-local', scope: 'local', }; await act(async () => { root.render( React.createElement(McpServerCard, { server: makeServer(), isInstalled: true, installedEntry, diagnostic: null, diagnosticsLoading: false, onClick, }) ); await Promise.resolve(); }); expect(host.querySelector('[data-testid="install-button"]')).toBeNull(); const manageButton = Array.from(host.querySelectorAll('button')).find( (button) => button.textContent === 'Manage' ) as HTMLButtonElement | undefined; expect(manageButton).toBeDefined(); await act(async () => { manageButton?.click(); await Promise.resolve(); }); expect(onClick).toHaveBeenCalledWith('io.github.upstash/context7'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('keeps direct actions for standard user-scope installs', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const installedEntry: InstalledMcpEntry = { name: 'context7', scope: 'user', }; await act(async () => { root.render( React.createElement(McpServerCard, { server: makeServer(), isInstalled: true, installedEntry, installedEntries: [installedEntry], diagnostic: null, diagnosticsLoading: false, onClick: vi.fn(), }) ); await Promise.resolve(); }); expect(host.querySelector('[data-testid="install-button"]')).not.toBeNull(); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('falls back to Manage when the same server is installed in multiple scopes', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const installedEntries: InstalledMcpEntry[] = [ { name: 'context7', scope: 'user' }, { name: 'context7', scope: 'project' }, ]; await act(async () => { root.render( React.createElement(McpServerCard, { server: makeServer(), isInstalled: true, installedEntry: installedEntries[1], installedEntries, diagnostic: null, diagnosticsLoading: false, onClick: vi.fn(), }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Installed in 2 scopes'); expect(host.querySelector('[data-testid="install-button"]')).toBeNull(); expect(host.textContent).toContain('Manage'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('reads direct-action state from the user-scope operation key', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const installedEntry: InstalledMcpEntry = { name: 'context7', scope: 'user', }; storeState.mcpInstallProgress = { [getMcpOperationKey('io.github.upstash/context7', 'project', '/tmp/project')]: 'error', [getMcpOperationKey('io.github.upstash/context7', 'user')]: 'pending', }; storeState.installErrors = { [getMcpOperationKey('io.github.upstash/context7', 'project', '/tmp/project')]: 'Project failed', [getMcpOperationKey('io.github.upstash/context7', 'user')]: 'User failed', }; await act(async () => { root.render( React.createElement(McpServerCard, { server: makeServer(), isInstalled: true, installedEntry, installedEntries: [installedEntry], diagnostic: null, diagnosticsLoading: false, onClick: vi.fn(), }) ); await Promise.resolve(); }); const installButton = host.querySelector('[data-testid="install-button"]') as HTMLButtonElement; expect(installButton.dataset.state).toBe('pending'); expect(installButton.dataset.error).toBe('User failed'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('keeps direct actions for standard global installs in multimodel mode', async () => { storeState.cliStatus = { flavor: 'agent_teams_orchestrator' }; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const installedEntry: InstalledMcpEntry = { name: 'context7', scope: 'global', }; await act(async () => { root.render( React.createElement(McpServerCard, { server: makeServer(), isInstalled: true, installedEntry, installedEntries: [installedEntry], diagnostic: null, diagnosticsLoading: false, onClick: vi.fn(), }) ); await Promise.resolve(); }); expect(host.querySelector('[data-testid="install-button"]')).not.toBeNull(); await act(async () => { root.unmount(); await Promise.resolve(); }); }); });