* add platform guards * add service management * fix types * move to zustand for servicehub * update App Updater * update tauri missing move * update app updater * refactor: move PlatformFeatures to separate const file 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * change tauri fetch name * update implementation * update extension fetch * make web version run properly * disabled unused web settings * fix all tests * fix lint * fix tests * add mock for extension * fix build * update make and mise * fix tsconfig for web-extensions * fix loader type * cleanup * fix test * update error handling + mcp should be working * Update mcp init * use separate is_web_app build property * Remove fixed model catalog url * fix additional tests * fix download issue (event emitter not implemented correctly) * Update Title html * fix app logs * update root tsx render timing --------- Co-authored-by: Claude <noreply@anthropic.com>
433 lines
13 KiB
TypeScript
433 lines
13 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { TauriMCPService } from '../mcp/tauri'
|
|
import { MCPTool } from '@/types/completion'
|
|
|
|
// Mock the global window.core.api
|
|
const mockCore = {
|
|
api: {
|
|
saveMcpConfigs: vi.fn(),
|
|
restartMcpServers: vi.fn(),
|
|
getMcpConfigs: vi.fn(),
|
|
getTools: vi.fn(),
|
|
getConnectedServers: vi.fn(),
|
|
callTool: vi.fn(),
|
|
},
|
|
}
|
|
|
|
// Set up global window mock
|
|
Object.defineProperty(global, 'window', {
|
|
value: {
|
|
core: mockCore,
|
|
},
|
|
writable: true,
|
|
})
|
|
|
|
describe('TauriMCPService', () => {
|
|
let mcpService: TauriMCPService
|
|
|
|
beforeEach(() => {
|
|
mcpService = new TauriMCPService()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('updateMCPConfig', () => {
|
|
it('should call saveMcpConfigs with correct configs', async () => {
|
|
const testConfig = '{"server1": {"path": "/path/to/server"}, "server2": {"command": "node server.js"}}'
|
|
mockCore.api.saveMcpConfigs.mockResolvedValue(undefined)
|
|
|
|
await mcpService.updateMCPConfig(testConfig)
|
|
|
|
expect(mockCore.api.saveMcpConfigs).toHaveBeenCalledWith({
|
|
configs: testConfig,
|
|
})
|
|
})
|
|
|
|
it('should handle empty config string', async () => {
|
|
const emptyConfig = ''
|
|
mockCore.api.saveMcpConfigs.mockResolvedValue(undefined)
|
|
|
|
await mcpService.updateMCPConfig(emptyConfig)
|
|
|
|
expect(mockCore.api.saveMcpConfigs).toHaveBeenCalledWith({
|
|
configs: emptyConfig,
|
|
})
|
|
})
|
|
|
|
it('should handle API rejection', async () => {
|
|
const testConfig = '{"server1": {}}'
|
|
const mockError = new Error('Failed to save config')
|
|
mockCore.api.saveMcpConfigs.mockRejectedValue(mockError)
|
|
|
|
await expect(mcpService.updateMCPConfig(testConfig)).rejects.toThrow('Failed to save config')
|
|
expect(mockCore.api.saveMcpConfigs).toHaveBeenCalledWith({
|
|
configs: testConfig,
|
|
})
|
|
})
|
|
|
|
it('should handle undefined window.core.api gracefully', async () => {
|
|
// Temporarily set window.core to undefined
|
|
const originalCore = window.core
|
|
// @ts-ignore
|
|
window.core = undefined
|
|
|
|
const testConfig = '{"server1": {}}'
|
|
|
|
await expect(mcpService.updateMCPConfig(testConfig)).resolves.toBeUndefined()
|
|
|
|
// Restore original core
|
|
window.core = originalCore
|
|
})
|
|
})
|
|
|
|
describe('restartMCPServers', () => {
|
|
it('should call restartMcpServers API', async () => {
|
|
mockCore.api.restartMcpServers.mockResolvedValue(undefined)
|
|
|
|
await mcpService.restartMCPServers()
|
|
|
|
expect(mockCore.api.restartMcpServers).toHaveBeenCalledWith()
|
|
})
|
|
|
|
it('should handle API rejection', async () => {
|
|
const mockError = new Error('Failed to restart servers')
|
|
mockCore.api.restartMcpServers.mockRejectedValue(mockError)
|
|
|
|
await expect(mcpService.restartMCPServers()).rejects.toThrow('Failed to restart servers')
|
|
expect(mockCore.api.restartMcpServers).toHaveBeenCalledWith()
|
|
})
|
|
|
|
it('should handle undefined window.core.api gracefully', async () => {
|
|
const originalCore = window.core
|
|
// @ts-ignore
|
|
window.core = undefined
|
|
|
|
await expect(mcpService.restartMCPServers()).resolves.toBeUndefined()
|
|
|
|
window.core = originalCore
|
|
})
|
|
})
|
|
|
|
describe('getMCPConfig', () => {
|
|
it('should get and parse MCP config correctly', async () => {
|
|
const mockConfigString = '{"server1": {"path": "/path/to/server"}, "server2": {"command": "node server.js"}}'
|
|
const expectedConfig = {
|
|
server1: { path: '/path/to/server' },
|
|
server2: { command: 'node server.js' },
|
|
}
|
|
|
|
mockCore.api.getMcpConfigs.mockResolvedValue(mockConfigString)
|
|
|
|
const result = await mcpService.getMCPConfig()
|
|
|
|
expect(mockCore.api.getMcpConfigs).toHaveBeenCalledWith()
|
|
expect(result).toEqual(expectedConfig)
|
|
})
|
|
|
|
it('should return empty object when config is null', async () => {
|
|
mockCore.api.getMcpConfigs.mockResolvedValue(null)
|
|
|
|
const result = await mcpService.getMCPConfig()
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('should return empty object when config is undefined', async () => {
|
|
mockCore.api.getMcpConfigs.mockResolvedValue(undefined)
|
|
|
|
const result = await mcpService.getMCPConfig()
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('should return empty object when config is empty string', async () => {
|
|
mockCore.api.getMcpConfigs.mockResolvedValue('')
|
|
|
|
const result = await mcpService.getMCPConfig()
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('should handle invalid JSON gracefully', async () => {
|
|
const invalidJson = '{"invalid": json}'
|
|
mockCore.api.getMcpConfigs.mockResolvedValue(invalidJson)
|
|
|
|
await expect(mcpService.getMCPConfig()).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle API rejection', async () => {
|
|
const mockError = new Error('Failed to get config')
|
|
mockCore.api.getMcpConfigs.mockRejectedValue(mockError)
|
|
|
|
await expect(mcpService.getMCPConfig()).rejects.toThrow('Failed to get config')
|
|
})
|
|
})
|
|
|
|
describe('getTools', () => {
|
|
it('should return list of MCP tools', async () => {
|
|
const mockTools: MCPTool[] = [
|
|
{
|
|
name: 'file_read',
|
|
description: 'Read a file from the filesystem',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
path: { type: 'string' },
|
|
},
|
|
required: ['path'],
|
|
},
|
|
},
|
|
{
|
|
name: 'file_write',
|
|
description: 'Write content to a file',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
path: { type: 'string' },
|
|
content: { type: 'string' },
|
|
},
|
|
required: ['path', 'content'],
|
|
},
|
|
},
|
|
]
|
|
|
|
mockCore.api.getTools.mockResolvedValue(mockTools)
|
|
|
|
const result = await mcpService.getTools()
|
|
|
|
expect(mockCore.api.getTools).toHaveBeenCalledWith()
|
|
expect(result).toEqual(mockTools)
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].name).toBe('file_read')
|
|
expect(result[1].name).toBe('file_write')
|
|
})
|
|
|
|
it('should return empty array when no tools available', async () => {
|
|
mockCore.api.getTools.mockResolvedValue([])
|
|
|
|
const result = await mcpService.getTools()
|
|
|
|
expect(result).toEqual([])
|
|
expect(Array.isArray(result)).toBe(true)
|
|
})
|
|
|
|
it('should handle API rejection', async () => {
|
|
const mockError = new Error('Failed to get tools')
|
|
mockCore.api.getTools.mockRejectedValue(mockError)
|
|
|
|
await expect(mcpService.getTools()).rejects.toThrow('Failed to get tools')
|
|
})
|
|
|
|
it('should handle undefined window.core.api', async () => {
|
|
const originalCore = window.core
|
|
// @ts-ignore
|
|
window.core = undefined
|
|
|
|
const result = await mcpService.getTools()
|
|
|
|
expect(result).toBeUndefined()
|
|
|
|
window.core = originalCore
|
|
})
|
|
})
|
|
|
|
describe('getConnectedServers', () => {
|
|
it('should return list of connected server names', async () => {
|
|
const mockServers = ['filesystem', 'database', 'search']
|
|
mockCore.api.getConnectedServers.mockResolvedValue(mockServers)
|
|
|
|
const result = await mcpService.getConnectedServers()
|
|
|
|
expect(mockCore.api.getConnectedServers).toHaveBeenCalledWith()
|
|
expect(result).toEqual(mockServers)
|
|
expect(result).toHaveLength(3)
|
|
})
|
|
|
|
it('should return empty array when no servers connected', async () => {
|
|
mockCore.api.getConnectedServers.mockResolvedValue([])
|
|
|
|
const result = await mcpService.getConnectedServers()
|
|
|
|
expect(result).toEqual([])
|
|
expect(Array.isArray(result)).toBe(true)
|
|
})
|
|
|
|
it('should handle API rejection', async () => {
|
|
const mockError = new Error('Failed to get connected servers')
|
|
mockCore.api.getConnectedServers.mockRejectedValue(mockError)
|
|
|
|
await expect(mcpService.getConnectedServers()).rejects.toThrow('Failed to get connected servers')
|
|
})
|
|
|
|
it('should handle undefined window.core.api', async () => {
|
|
const originalCore = window.core
|
|
// @ts-ignore
|
|
window.core = undefined
|
|
|
|
const result = await mcpService.getConnectedServers()
|
|
|
|
expect(result).toBeUndefined()
|
|
|
|
window.core = originalCore
|
|
})
|
|
})
|
|
|
|
describe('callTool', () => {
|
|
it('should call tool with correct arguments and return result', async () => {
|
|
const toolArgs = {
|
|
toolName: 'file_read',
|
|
arguments: { path: '/path/to/file.txt' },
|
|
}
|
|
|
|
const mockResult = {
|
|
error: '',
|
|
content: [{ text: 'File content here' }],
|
|
}
|
|
|
|
mockCore.api.callTool.mockResolvedValue(mockResult)
|
|
|
|
const result = await mcpService.callTool(toolArgs)
|
|
|
|
expect(mockCore.api.callTool).toHaveBeenCalledWith(toolArgs)
|
|
expect(result).toEqual(mockResult)
|
|
})
|
|
|
|
it('should handle tool call with error', async () => {
|
|
const toolArgs = {
|
|
toolName: 'file_read',
|
|
arguments: { path: '/nonexistent/file.txt' },
|
|
}
|
|
|
|
const mockResult = {
|
|
error: 'File not found',
|
|
content: [],
|
|
}
|
|
|
|
mockCore.api.callTool.mockResolvedValue(mockResult)
|
|
|
|
const result = await mcpService.callTool(toolArgs)
|
|
|
|
expect(result.error).toBe('File not found')
|
|
expect(result.content).toEqual([])
|
|
})
|
|
|
|
it('should handle complex tool arguments', async () => {
|
|
const toolArgs = {
|
|
toolName: 'database_query',
|
|
arguments: {
|
|
query: 'SELECT * FROM users WHERE age > ?',
|
|
params: [18],
|
|
limit: 100,
|
|
},
|
|
}
|
|
|
|
const mockResult = {
|
|
error: '',
|
|
content: [{ text: 'Query results...' }],
|
|
}
|
|
|
|
mockCore.api.callTool.mockResolvedValue(mockResult)
|
|
|
|
const result = await mcpService.callTool(toolArgs)
|
|
|
|
expect(mockCore.api.callTool).toHaveBeenCalledWith(toolArgs)
|
|
expect(result).toEqual(mockResult)
|
|
})
|
|
|
|
it('should handle API rejection', async () => {
|
|
const toolArgs = {
|
|
toolName: 'file_read',
|
|
arguments: { path: '/path/to/file.txt' },
|
|
}
|
|
|
|
const mockError = new Error('Tool execution failed')
|
|
mockCore.api.callTool.mockRejectedValue(mockError)
|
|
|
|
await expect(mcpService.callTool(toolArgs)).rejects.toThrow('Tool execution failed')
|
|
})
|
|
|
|
it('should handle undefined window.core.api', async () => {
|
|
const originalCore = window.core
|
|
// @ts-ignore
|
|
window.core = undefined
|
|
|
|
const toolArgs = {
|
|
toolName: 'test_tool',
|
|
arguments: {},
|
|
}
|
|
|
|
const result = await mcpService.callTool(toolArgs)
|
|
|
|
expect(result).toBeUndefined()
|
|
|
|
window.core = originalCore
|
|
})
|
|
|
|
it('should handle empty arguments object', async () => {
|
|
const toolArgs = {
|
|
toolName: 'simple_tool',
|
|
arguments: {},
|
|
}
|
|
|
|
const mockResult = {
|
|
error: '',
|
|
content: [{ text: 'Success' }],
|
|
}
|
|
|
|
mockCore.api.callTool.mockResolvedValue(mockResult)
|
|
|
|
const result = await mcpService.callTool(toolArgs)
|
|
|
|
expect(mockCore.api.callTool).toHaveBeenCalledWith(toolArgs)
|
|
expect(result).toEqual(mockResult)
|
|
})
|
|
})
|
|
|
|
describe('integration tests', () => {
|
|
it('should handle full MCP workflow: config -> restart -> get tools -> call tool', async () => {
|
|
const config = '{"filesystem": {"command": "filesystem-server"}}'
|
|
const tools: MCPTool[] = [
|
|
{
|
|
name: 'read_file',
|
|
description: 'Read file',
|
|
inputSchema: { type: 'object' },
|
|
},
|
|
]
|
|
const servers = ['filesystem']
|
|
const toolResult = {
|
|
error: '',
|
|
content: [{ text: 'File content' }],
|
|
}
|
|
|
|
mockCore.api.saveMcpConfigs.mockResolvedValue(undefined)
|
|
mockCore.api.restartMcpServers.mockResolvedValue(undefined)
|
|
mockCore.api.getTools.mockResolvedValue(tools)
|
|
mockCore.api.getConnectedServers.mockResolvedValue(servers)
|
|
mockCore.api.callTool.mockResolvedValue(toolResult)
|
|
|
|
// Execute workflow
|
|
await mcpService.updateMCPConfig(config)
|
|
await mcpService.restartMCPServers()
|
|
const availableTools = await mcpService.getTools()
|
|
const connectedServers = await mcpService.getConnectedServers()
|
|
const result = await mcpService.callTool({
|
|
toolName: 'read_file',
|
|
arguments: { path: '/test.txt' },
|
|
})
|
|
|
|
// Verify all calls were made correctly
|
|
expect(mockCore.api.saveMcpConfigs).toHaveBeenCalledWith({ configs: config })
|
|
expect(mockCore.api.restartMcpServers).toHaveBeenCalled()
|
|
expect(mockCore.api.getTools).toHaveBeenCalled()
|
|
expect(mockCore.api.getConnectedServers).toHaveBeenCalled()
|
|
expect(mockCore.api.callTool).toHaveBeenCalledWith({
|
|
toolName: 'read_file',
|
|
arguments: { path: '/test.txt' },
|
|
})
|
|
|
|
// Verify results
|
|
expect(availableTools).toEqual(tools)
|
|
expect(connectedServers).toEqual(servers)
|
|
expect(result).toEqual(toolResult)
|
|
})
|
|
})
|
|
}) |