import { describe, it, expect, beforeEach, vi } from 'vitest' import { renderHook, act } from '@testing-library/react' import { useMCPServers } from '../useMCPServers' import type { MCPServerConfig } from '../useMCPServers' // Mock the ServiceHub const mockUpdateMCPConfig = vi.fn().mockResolvedValue(undefined) const mockRestartMCPServers = vi.fn().mockResolvedValue(undefined) vi.mock('@/hooks/useServiceHub', () => ({ getServiceHub: () => ({ mcp: () => ({ updateMCPConfig: mockUpdateMCPConfig, restartMCPServers: mockRestartMCPServers, }), }), })) describe('useMCPServers', () => { beforeEach(() => { vi.clearAllMocks() // Reset store state to defaults useMCPServers.setState({ open: true, mcpServers: {}, loading: false, deletedServerKeys: [], }) }) it('should initialize with default values', () => { const { result } = renderHook(() => useMCPServers()) expect(result.current.open).toBe(true) expect(result.current.mcpServers).toEqual({}) expect(result.current.loading).toBe(false) expect(result.current.deletedServerKeys).toEqual([]) expect(typeof result.current.getServerConfig).toBe('function') expect(typeof result.current.setLeftPanel).toBe('function') expect(typeof result.current.addServer).toBe('function') expect(typeof result.current.editServer).toBe('function') expect(typeof result.current.deleteServer).toBe('function') expect(typeof result.current.setServers).toBe('function') expect(typeof result.current.syncServers).toBe('function') expect(typeof result.current.syncServersAndRestart).toBe('function') }) describe('setLeftPanel', () => { it('should set left panel open state', () => { const { result } = renderHook(() => useMCPServers()) act(() => { result.current.setLeftPanel(false) }) expect(result.current.open).toBe(false) act(() => { result.current.setLeftPanel(true) }) expect(result.current.open).toBe(true) }) }) describe('getServerConfig', () => { it('should return server config if exists', () => { const { result } = renderHook(() => useMCPServers()) const serverConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: { NODE_ENV: 'production' }, active: true, } act(() => { result.current.addServer('test-server', serverConfig) }) const config = result.current.getServerConfig('test-server') expect(config).toEqual(serverConfig) }) it('should return undefined if server does not exist', () => { const { result } = renderHook(() => useMCPServers()) const config = result.current.getServerConfig('nonexistent-server') expect(config).toBeUndefined() }) }) describe('addServer', () => { it('should add a new server', () => { const { result } = renderHook(() => useMCPServers()) const serverConfig: MCPServerConfig = { command: 'python', args: ['main.py', '--port', '8080'], env: { PYTHONPATH: '/app' }, active: true, } act(() => { result.current.addServer('python-server', serverConfig) }) expect(result.current.mcpServers).toEqual({ 'python-server': serverConfig, }) }) it('should update existing server with same key', () => { const { result } = renderHook(() => useMCPServers()) const initialConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: {}, active: false, } const updatedConfig: MCPServerConfig = { command: 'node', args: ['server.js', '--production'], env: { NODE_ENV: 'production' }, active: true, } act(() => { result.current.addServer('node-server', initialConfig) }) expect(result.current.mcpServers['node-server']).toEqual(initialConfig) act(() => { result.current.addServer('node-server', updatedConfig) }) expect(result.current.mcpServers['node-server']).toEqual(updatedConfig) }) it('should add multiple servers', () => { const { result } = renderHook(() => useMCPServers()) const serverA: MCPServerConfig = { command: 'node', args: ['serverA.js'], env: {}, } const serverB: MCPServerConfig = { command: 'python', args: ['serverB.py'], env: { PYTHONPATH: '/app' }, } act(() => { result.current.addServer('server-a', serverA) result.current.addServer('server-b', serverB) }) expect(result.current.mcpServers).toEqual({ 'server-a': serverA, 'server-b': serverB, }) }) }) describe('editServer', () => { it('should edit existing server', () => { const { result } = renderHook(() => useMCPServers()) const initialConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: {}, active: false, } const updatedConfig: MCPServerConfig = { command: 'node', args: ['server.js', '--debug'], env: { DEBUG: 'true' }, active: true, } act(() => { result.current.addServer('test-server', initialConfig) }) act(() => { result.current.editServer('test-server', updatedConfig) }) expect(result.current.mcpServers['test-server']).toEqual(updatedConfig) }) it('should not modify state if server does not exist', () => { const { result } = renderHook(() => useMCPServers()) const initialState = result.current.mcpServers const config: MCPServerConfig = { command: 'node', args: ['server.js'], env: {}, } act(() => { result.current.editServer('nonexistent-server', config) }) expect(result.current.mcpServers).toEqual(initialState) }) }) describe('setServers', () => { it('should merge servers with existing ones', () => { const { result } = renderHook(() => useMCPServers()) const existingServer: MCPServerConfig = { command: 'node', args: ['existing.js'], env: {}, } const newServers = { 'new-server-1': { command: 'python', args: ['new1.py'], env: { PYTHONPATH: '/app1' }, }, 'new-server-2': { command: 'python', args: ['new2.py'], env: { PYTHONPATH: '/app2' }, }, } act(() => { result.current.addServer('existing-server', existingServer) }) act(() => { result.current.setServers(newServers) }) expect(result.current.mcpServers).toEqual({ 'existing-server': existingServer, ...newServers, }) }) it('should overwrite existing servers with same keys', () => { const { result } = renderHook(() => useMCPServers()) const originalServer: MCPServerConfig = { command: 'node', args: ['original.js'], env: {}, } const updatedServer: MCPServerConfig = { command: 'node', args: ['updated.js'], env: { NODE_ENV: 'production' }, } act(() => { result.current.addServer('test-server', originalServer) }) act(() => { result.current.setServers({ 'test-server': updatedServer }) }) expect(result.current.mcpServers['test-server']).toEqual(updatedServer) }) }) describe('deleteServer', () => { it('should delete existing server', () => { const { result } = renderHook(() => useMCPServers()) const serverConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: {}, } act(() => { result.current.addServer('test-server', serverConfig) }) expect(result.current.mcpServers['test-server']).toEqual(serverConfig) act(() => { result.current.deleteServer('test-server') }) expect(result.current.mcpServers['test-server']).toBeUndefined() expect(result.current.deletedServerKeys).toContain('test-server') }) it('should add server key to deletedServerKeys even if server does not exist', () => { const { result } = renderHook(() => useMCPServers()) act(() => { result.current.deleteServer('nonexistent-server') }) expect(result.current.deletedServerKeys).toContain('nonexistent-server') }) it('should handle multiple deletions', () => { const { result } = renderHook(() => useMCPServers()) const serverA: MCPServerConfig = { command: 'node', args: ['serverA.js'], env: {}, } const serverB: MCPServerConfig = { command: 'python', args: ['serverB.py'], env: {}, } act(() => { result.current.addServer('server-a', serverA) result.current.addServer('server-b', serverB) }) act(() => { result.current.deleteServer('server-a') result.current.deleteServer('server-b') }) expect(result.current.mcpServers).toEqual({}) expect(result.current.deletedServerKeys).toEqual(['server-a', 'server-b']) }) }) describe('syncServers', () => { it('should call updateMCPConfig with current servers', async () => { const { result } = renderHook(() => useMCPServers()) const serverConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: { NODE_ENV: 'production' }, } act(() => { result.current.addServer('test-server', serverConfig) }) await act(async () => { await result.current.syncServers() }) expect(mockUpdateMCPConfig).toHaveBeenCalledWith( JSON.stringify({ mcpServers: { 'test-server': serverConfig, }, }) ) }) it('should call updateMCPConfig with empty servers object', async () => { const { result } = renderHook(() => useMCPServers()) await act(async () => { await result.current.syncServers() }) expect(mockUpdateMCPConfig).toHaveBeenCalledWith( JSON.stringify({ mcpServers: {}, }) ) }) }) describe('syncServersAndRestart', () => { it('should call updateMCPConfig and then mockRestartMCPServers', async () => { const { result } = renderHook(() => useMCPServers()) const serverConfig: MCPServerConfig = { command: 'python', args: ['server.py'], env: { PYTHONPATH: '/app' }, } act(() => { result.current.addServer('python-server', serverConfig) }) await act(async () => { await result.current.syncServersAndRestart() }) expect(mockUpdateMCPConfig).toHaveBeenCalledWith( JSON.stringify({ mcpServers: { 'python-server': serverConfig, }, }) ) expect(mockRestartMCPServers).toHaveBeenCalled() }) }) describe('state management', () => { it('should maintain state across multiple hook instances', () => { const { result: result1 } = renderHook(() => useMCPServers()) const { result: result2 } = renderHook(() => useMCPServers()) const serverConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: {}, } act(() => { result1.current.addServer('shared-server', serverConfig) }) expect(result2.current.mcpServers['shared-server']).toEqual(serverConfig) }) }) describe('complex scenarios', () => { it('should handle complete server lifecycle', () => { const { result } = renderHook(() => useMCPServers()) const initialConfig: MCPServerConfig = { command: 'node', args: ['server.js'], env: {}, active: false, } const updatedConfig: MCPServerConfig = { command: 'node', args: ['server.js', '--production'], env: { NODE_ENV: 'production' }, active: true, } // Add server act(() => { result.current.addServer('lifecycle-server', initialConfig) }) expect(result.current.mcpServers['lifecycle-server']).toEqual(initialConfig) // Edit server act(() => { result.current.editServer('lifecycle-server', updatedConfig) }) expect(result.current.mcpServers['lifecycle-server']).toEqual(updatedConfig) // Delete server act(() => { result.current.deleteServer('lifecycle-server') }) expect(result.current.mcpServers['lifecycle-server']).toBeUndefined() expect(result.current.deletedServerKeys).toContain('lifecycle-server') }) }) })