feat: add persistent desktop assistant workspace
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
698a15ad14
commit
6ef1795b81
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ResolvedMcpServer } from './capability-service'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const client = {
|
||||
connect: vi.fn(),
|
||||
listTools: vi.fn(),
|
||||
getServerVersion: vi.fn(),
|
||||
close: vi.fn()
|
||||
}
|
||||
return {
|
||||
client,
|
||||
Client: vi.fn(function Client() {
|
||||
return client
|
||||
}),
|
||||
StdioClientTransport: vi.fn(function StdioClientTransport(
|
||||
options: unknown
|
||||
) {
|
||||
return { kind: 'stdio', options }
|
||||
}),
|
||||
StreamableHTTPClientTransport: vi.fn(
|
||||
function StreamableHTTPClientTransport(
|
||||
url: URL,
|
||||
options: unknown
|
||||
) {
|
||||
return { kind: 'http', url, options }
|
||||
}
|
||||
),
|
||||
SSEClientTransport: vi.fn(function SSEClientTransport(
|
||||
url: URL,
|
||||
options: unknown
|
||||
) {
|
||||
return { kind: 'sse', url, options }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: mocks.Client
|
||||
}))
|
||||
vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: mocks.StdioClientTransport
|
||||
}))
|
||||
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
|
||||
StreamableHTTPClientTransport: mocks.StreamableHTTPClientTransport
|
||||
}))
|
||||
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
|
||||
SSEClientTransport: mocks.SSEClientTransport
|
||||
}))
|
||||
|
||||
import { testMcpServer } from './mcp-tester'
|
||||
|
||||
const common = {
|
||||
id: 'd2ef774b-146c-4467-a909-6feb112a9c2c',
|
||||
name: 'Test MCP',
|
||||
description: '',
|
||||
enabled: true,
|
||||
assignments: ['model'] as Array<'model' | 'opencode' | 'continue'>,
|
||||
secretConfigured: false
|
||||
}
|
||||
|
||||
describe('testMcpServer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.client.connect.mockResolvedValue(undefined)
|
||||
mocks.client.listTools.mockResolvedValue({
|
||||
tools: [
|
||||
{
|
||||
name: 'search',
|
||||
description: 'Search documents'
|
||||
}
|
||||
]
|
||||
})
|
||||
mocks.client.getServerVersion.mockReturnValue({
|
||||
name: 'test-server',
|
||||
version: '1.0.0'
|
||||
})
|
||||
mocks.client.close.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
it('uses separated stdio command arguments and closes the client', async () => {
|
||||
const result = await testMcpServer({
|
||||
...common,
|
||||
transport: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js', '--safe']
|
||||
} satisfies ResolvedMcpServer)
|
||||
|
||||
expect(mocks.StdioClientTransport).toHaveBeenCalledWith({
|
||||
command: 'node',
|
||||
args: ['server.js', '--safe'],
|
||||
stderr: 'ignore',
|
||||
maxBufferSize: 2 * 1024 * 1024
|
||||
})
|
||||
expect(mocks.client.connect).toHaveBeenCalledOnce()
|
||||
expect(mocks.client.listTools).toHaveBeenCalledOnce()
|
||||
expect(mocks.client.close).toHaveBeenCalledOnce()
|
||||
expect(result).toEqual({
|
||||
serverName: 'test-server',
|
||||
serverVersion: '1.0.0',
|
||||
toolCount: 1,
|
||||
tools: [{ name: 'search', description: 'Search documents' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('injects a bearer token only into the remote transport', async () => {
|
||||
await testMcpServer({
|
||||
...common,
|
||||
transport: 'http',
|
||||
url: 'https://mcp.example.com/mcp',
|
||||
secretConfigured: true,
|
||||
secret: 'test-secret'
|
||||
} satisfies ResolvedMcpServer)
|
||||
|
||||
expect(mocks.StreamableHTTPClientTransport).toHaveBeenCalledOnce()
|
||||
const [url, options] =
|
||||
mocks.StreamableHTTPClientTransport.mock.calls[0] ?? []
|
||||
expect(url).toEqual(new URL('https://mcp.example.com/mcp'))
|
||||
expect(options).toMatchObject({
|
||||
requestInit: {
|
||||
headers: { Authorization: 'Bearer test-secret' }
|
||||
},
|
||||
reconnectionOptions: { maxRetries: 0 }
|
||||
})
|
||||
expect(options).toHaveProperty('fetch')
|
||||
})
|
||||
|
||||
it('closes the client and returns a controlled error on failure', async () => {
|
||||
mocks.client.connect.mockRejectedValue(
|
||||
new Error('server included sensitive diagnostics')
|
||||
)
|
||||
|
||||
await expect(
|
||||
testMcpServer({
|
||||
...common,
|
||||
transport: 'sse',
|
||||
url: 'https://mcp.example.com/sse'
|
||||
} satisfies ResolvedMcpServer)
|
||||
).rejects.toThrow(
|
||||
'MCP Server 连接失败,请检查地址、命令和服务状态'
|
||||
)
|
||||
expect(mocks.client.close).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user