/** * MCPManager getPrompt / readResource 测试(v0.8.1 P1-4) * * prompts/resources 对话内可用化的主进程侧契约: * 1. getPrompt:messages 展平为文本(text 块拼接、多消息 \n\n 连接)、 * 未连接 server 显式抛错; * 2. readResource:text 内容返回、二进制(blob)显式拒绝、 * 空 contents 返回 null。 * * 通过反射注入受控 server 状态(绕开真实 SDK 连接),与 IPC 层解耦。 */ import { describe, it, expect, vi } from 'vitest'; vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); import { MCPManager } from '../mcp-manager.service'; import type { ToolRegistry } from '../../harness/tools/registry'; function makeManager(): MCPManager { const fakeDB = { prepare: () => ({ run: () => ({ changes: 0 }) }), } as unknown as ConstructorParameters[0] extends () => infer D ? D : never; const registry = { registerMCP: vi.fn(() => true), unregisterMCPTools: vi.fn(), } as unknown as ToolRegistry; return new MCPManager(() => fakeDB, registry); } /** 反射注入受控 server 状态(mock client) */ function injectServer(manager: MCPManager, client: Record>): void { const states = ( manager as unknown as { servers: Map< string, { config: { name: string }; status: string; client: unknown; tools: unknown[]; resources: unknown[]; prompts: unknown[]; } >; } ).servers; states.set('test-server', { config: { name: 'test-server' } as never, status: 'connected', client: client as never, tools: [], resources: [], prompts: [], }); } describe('MCPManager — getPrompt(v0.8.1 P1-4)', () => { it('messages 展平为文本,多消息以空行连接', async () => { const manager = makeManager(); injectServer(manager, { getPrompt: vi.fn().mockResolvedValue({ description: 'demo prompt', messages: [ { role: 'user', content: { type: 'text', text: 'Line A' } }, { role: 'user', content: { type: 'text', text: 'Line B' } }, ], }), }); const result = await manager.getPrompt('test-server', 'demo'); expect(result).not.toBeNull(); expect(result!.text).toBe('Line A\n\nLine B'); expect(result!.description).toBe('demo prompt'); }); it('server 未连接 → 显式抛错(调用方以 error 返回)', async () => { const manager = makeManager(); await expect(manager.getPrompt('nope', 'demo')).rejects.toThrow(/not connected/); }); }); describe('MCPManager — readResource(v0.8.1 P1-4)', () => { it('text 内容正常返回', async () => { const manager = makeManager(); injectServer(manager, { readResource: vi.fn().mockResolvedValue({ contents: [{ uri: 'file:///a.txt', text: 'hello resource', mimeType: 'text/plain' }], }), }); const result = await manager.readResource('test-server', 'file:///a.txt'); expect(result).toEqual({ text: 'hello resource', mimeType: 'text/plain' }); }); it('二进制(blob)内容显式拒绝', async () => { const manager = makeManager(); injectServer(manager, { readResource: vi.fn().mockResolvedValue({ contents: [{ uri: 'file:///a.png', blob: 'aGVsbG8=' }], }), }); await expect(manager.readResource('test-server', 'file:///a.png')).rejects.toThrow(/binary/); }); it('空 contents → null', async () => { const manager = makeManager(); injectServer(manager, { readResource: vi.fn().mockResolvedValue({ contents: [] }), }); expect(await manager.readResource('test-server', 'file:///none')).toBeNull(); }); });