Dinh Long Nguyen a30eb7f968
feat: Jan Web (reusing Jan Desktop UI) (#6298)
* 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>
2025-09-05 01:47:46 +07:00

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)
})
})
})