import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CLI_NOT_FOUND_MARKER } from '@shared/constants/cli'; interface StoreState { mcpBrowseCatalog: { id: string; name: string; description: string; source: 'official' | 'glama'; installSpec: null; envVars: []; tools: []; requiresAuth: boolean; }[]; mcpBrowseNextCursor?: string; mcpBrowseLoading: boolean; mcpBrowseError: string | null; mcpBrowse: ReturnType; mcpInstalledServers: { name: string; scope: 'local' | 'user' | 'project' }[]; mcpInstalledServersByProjectPath?: Record< string, { name: string; scope: 'local' | 'user' | 'project' }[] >; fetchMcpGitHubStars: ReturnType; mcpDiagnostics: Record; mcpDiagnosticsByProjectPath?: Record>; mcpDiagnosticsLoading: boolean; mcpDiagnosticsLoadingByProjectPath?: Record; mcpDiagnosticsError: string | null; mcpDiagnosticsErrorByProjectPath?: Record; mcpDiagnosticsLastCheckedAt: number | null; mcpDiagnosticsLastCheckedAtByProjectPath?: Record; runMcpDiagnostics: ReturnType; cliStatusLoading: boolean; cliStatus?: { flavor?: 'claude' | 'agent_teams_orchestrator'; installed?: boolean; binaryPath?: string | null; launchError?: string | null; } | null; } const storeState = {} as StoreState; vi.mock('@renderer/store', () => ({ useStore: (selector: (state: StoreState) => unknown) => selector(storeState), })); vi.mock('zustand/react/shallow', () => ({ useShallow: (selector: unknown) => selector, })); 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', disabled, }: React.PropsWithChildren<{ onClick?: () => void; type?: 'button' | 'submit' | 'reset'; disabled?: boolean; }>) => React.createElement( 'button', { type, disabled, onClick, }, children ), })); vi.mock('@renderer/components/ui/select', () => ({ Select: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children), SelectTrigger: ({ children }: React.PropsWithChildren) => React.createElement('button', { type: 'button' }, children), SelectValue: () => React.createElement('span', null, 'select-value'), SelectContent: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children), SelectItem: ({ children }: React.PropsWithChildren<{ value: string }>) => React.createElement('button', { type: 'button' }, children), })); vi.mock('@renderer/components/extensions/common/SearchInput', () => ({ SearchInput: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => React.createElement('input', { value, onChange: (event: React.ChangeEvent) => onChange(event.target.value), }), })); vi.mock('@renderer/components/extensions/mcp/McpServerCard', () => ({ McpServerCard: ({ server }: { server: { id: string; name: string } }) => React.createElement( 'div', { 'data-testid': 'mcp-card', 'data-server-id': server.id }, server.name ), })); vi.mock('@renderer/components/extensions/mcp/McpServerDetailDialog', () => ({ McpServerDetailDialog: ({ open }: { open: boolean }) => open ? React.createElement('div', { 'data-testid': 'mcp-detail' }) : null, })); vi.mock('@renderer/utils/formatters', () => ({ formatRelativeTime: () => 'just now', })); vi.mock('lucide-react', () => { const Icon = (props: React.SVGProps) => React.createElement('svg', props); return { AlertTriangle: Icon, RefreshCw: Icon, Search: Icon, Server: Icon, }; }); import { McpServersPanel } from '@renderer/components/extensions/mcp/McpServersPanel'; describe('McpServersPanel initial browse loading', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.mcpBrowseCatalog = []; storeState.mcpBrowseNextCursor = undefined; storeState.mcpBrowseLoading = false; storeState.mcpBrowseError = null; storeState.mcpBrowse = vi.fn(); storeState.mcpInstalledServers = []; storeState.mcpInstalledServersByProjectPath = undefined; storeState.fetchMcpGitHubStars = vi.fn(); storeState.mcpDiagnostics = {}; storeState.mcpDiagnosticsByProjectPath = undefined; storeState.mcpDiagnosticsLoading = false; storeState.mcpDiagnosticsLoadingByProjectPath = undefined; storeState.mcpDiagnosticsError = null; storeState.mcpDiagnosticsErrorByProjectPath = undefined; storeState.mcpDiagnosticsLastCheckedAt = null; storeState.mcpDiagnosticsLastCheckedAtByProjectPath = undefined; storeState.runMcpDiagnostics = vi.fn(); storeState.cliStatusLoading = false; storeState.cliStatus = { flavor: 'claude', installed: true, binaryPath: '/usr/local/bin/claude', launchError: null, }; }); afterEach(() => { document.body.innerHTML = ''; vi.unstubAllGlobals(); }); it('loads the catalog once on first mount when browse state is empty', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(storeState.mcpBrowse).toHaveBeenCalledTimes(1); expect(storeState.runMcpDiagnostics).toHaveBeenCalledTimes(1); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('does not auto-retry browse after an error with an empty catalog', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(storeState.mcpBrowse).toHaveBeenCalledTimes(1); storeState.mcpBrowseError = 'Registry unavailable'; await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(storeState.mcpBrowse).toHaveBeenCalledTimes(1); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('uses truthful diagnostics copy instead of suggesting a hard-coded CLI command', async () => { storeState.mcpBrowseCatalog = [ { id: 'context7', name: 'Context7', description: 'Docs MCP', source: 'official', installSpec: null, envVars: [], tools: [], requiresAuth: false, }, ]; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Run diagnostics from this page'); expect(host.textContent).not.toContain('claude-multimodel mcp diagnose'); expect(host.textContent).not.toContain('claude mcp list'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('uses runtime-aware missing-runtime copy for multimodel diagnostics failures', async () => { storeState.mcpDiagnosticsError = `${CLI_NOT_FOUND_MARKER}: missing runtime`; storeState.cliStatus = { flavor: 'agent_teams_orchestrator', }; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Multimodel runtime not available'); expect(host.textContent).toContain('MCP health checks require Multimodel runtime'); expect(host.textContent).not.toContain('Claude CLI not installed'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('does not auto-run diagnostics when the configured runtime is unavailable', async () => { storeState.cliStatus = { flavor: 'agent_teams_orchestrator', installed: false, binaryPath: null, launchError: null, }; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(storeState.runMcpDiagnostics).not.toHaveBeenCalled(); expect(host.textContent).toContain( 'The configured runtime is required. Install or repair it from the Dashboard.' ); const checkStatusButton = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('Check Status') ); expect(checkStatusButton).toBeDefined(); expect((checkStatusButton!).disabled).toBe(true); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('waits for runtime hydration before auto-running diagnostics', async () => { storeState.cliStatus = null; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(storeState.runMcpDiagnostics).not.toHaveBeenCalled(); expect(host.textContent).toContain('Checking runtime availability...'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('renders provider-neutral waiting copy while diagnostics are still running', async () => { storeState.mcpDiagnosticsLoading = true; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Waiting for diagnostics results...'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('uses the effective runtime status override for diagnostics gating during background refresh', async () => { storeState.cliStatus = null; storeState.cliStatusLoading = true; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), cliStatus: { flavor: 'agent_teams_orchestrator', displayName: 'Multimodel runtime', installed: true, authLoggedIn: false, binaryPath: '/usr/local/bin/agent-teams', launchError: null, providers: [], }, cliStatusLoading: false, }) ); await Promise.resolve(); }); expect(storeState.runMcpDiagnostics).toHaveBeenCalledTimes(1); expect(host.textContent).not.toContain('Checking runtime availability...'); expect(host.textContent).not.toContain('The configured runtime is required.'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('does not block diagnostics when a usable runtime status already exists during background refresh', async () => { storeState.cliStatus = { flavor: 'agent_teams_orchestrator', installed: true, binaryPath: '/usr/local/bin/agent-teams', launchError: null, }; storeState.cliStatusLoading = true; const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(McpServersPanel, { projectPath: null, mcpSearchQuery: '', mcpSearch: vi.fn(), mcpSearchResults: [], mcpSearchLoading: false, mcpSearchWarnings: [], selectedMcpServerId: null, setSelectedMcpServerId: vi.fn(), }) ); await Promise.resolve(); }); expect(storeState.runMcpDiagnostics).toHaveBeenCalledTimes(1); expect(host.textContent).not.toContain('Checking runtime status...'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); });