/** * Gateway Mock 工具 * 模拟后端 Gateway 服务,用于独立测试前端功能 * 基于实际 API 端点: http://127.0.0.1:50051 */ import { Page, WebSocketRoute } from '@playwright/test'; /** * Mock 响应数据模板 - 基于实际 API 响应格式 */ export const mockResponses = { // 健康检查 health: { status: 'ok', version: '0.4.0-mock', }, // 模型列表 - 来自 /api/models models: [ { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', provider: 'anthropic' }, { id: 'claude-3-sonnet-20240229', name: 'Claude 3 Sonnet', provider: 'anthropic' }, { id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', provider: 'anthropic' }, { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' }, { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' }, ], // Agent/分身列表 - 来自 /api/agents agents: [ { id: 'default-agent', name: 'ZCLAW', role: 'AI Assistant', nickname: 'ZCLAW', model: 'claude-sonnet-4-20250514', createdAt: new Date().toISOString(), bootstrapReady: true, onboardingCompleted: true, }, ], // Hands 列表 - 来自 /api/hands hands: [ { id: 'browser', name: 'Browser', description: '浏览器自动化能力包', status: 'idle', requirements_met: true, category: 'automation', icon: '🌐', tool_count: 4, metric_count: 0, }, { id: 'collector', name: 'Collector', description: '数据收集聚合能力包', status: 'idle', requirements_met: true, category: 'data', icon: '📊', tool_count: 3, metric_count: 2, }, { id: 'researcher', name: 'Researcher', description: '深度研究能力包', status: 'idle', requirements_met: true, category: 'research', icon: '🔍', tool_count: 5, metric_count: 1, }, { id: 'predictor', name: 'Predictor', description: '预测分析能力包', status: 'setup_needed', requirements_met: false, category: 'analytics', icon: '📈', tool_count: 2, metric_count: 3, }, ], // 工作流列表 - 来自 /api/workflows workflows: [ { id: 'wf-default', name: '示例工作流', description: '演示用工作流', steps: [], status: 'idle', createdAt: new Date().toISOString(), }, ], // 触发器列表 - 来自 /api/triggers triggers: [ { id: 'trigger-1', type: 'webhook', name: '示例触发器', enabled: true, }, ], // 技能列表 - 来自 /api/skills skills: [ { id: 'skill-code-review', name: '代码审查', description: '自动审查代码质量', category: 'development', triggers: ['review', 'audit'], installed: true, }, { id: 'skill-doc-gen', name: '文档生成', description: '自动生成代码文档', category: 'development', triggers: ['doc', 'document'], installed: false, }, ], // 审批列表 - 来自 /api/approvals approvals: [], // 会话列表 - 来自 /api/sessions sessions: [], // 用量统计 - 来自 /api/stats/usage usageStats: { totalSessions: 10, totalMessages: 100, totalTokens: 50000, byModel: { 'claude-sonnet-4-20250514': { messages: 60, inputTokens: 20000, outputTokens: 15000 }, 'claude-3-haiku-20240307': { messages: 40, inputTokens: 10000, outputTokens: 5000 }, }, }, // 插件状态 - 来自 /api/plugins/status pluginStatus: [ { id: 'mcp-filesystem', name: 'Filesystem MCP', status: 'active', version: '1.0.0' }, { id: 'mcp-github', name: 'GitHub MCP', status: 'inactive' }, ], // 快速配置 - 来自 /api/config quickConfig: { userName: 'User', userRole: 'Developer', defaultModel: 'claude-sonnet-4-20250514', dataDir: './data', workspaceDir: './workspace', }, // 工作区信息 - 来自 /api/workspace workspace: { path: './workspace', exists: true, }, // 安全状态 - 来自 /api/security/status securityStatus: { status: 'secure', lastAudit: new Date().toISOString(), }, // 调度任务 - 来自 /api/scheduler/tasks scheduledTasks: [], // 能力列表 - 来自 /api/capabilities capabilities: { hands: ['browser', 'collector', 'researcher', 'predictor'], models: ['claude-sonnet-4-20250514', 'claude-3-haiku-20240307'], plugins: ['mcp-filesystem', 'mcp-github'], }, }; /** * 创建 Agent 消息响应 - 来自 POST /api/agents/{id}/message */ export function createAgentMessageResponse(content: string): object { return { response: content, input_tokens: 100, output_tokens: 50, }; } /** * 创建流式响应数据块 - 用于 WebSocket */ export function createStreamChunks(text: string, chunkSize = 10): Array<{ delta: string; phase: string }> { const chunks: Array<{ delta: string; phase: string }> = []; const words = text.split(' '); // 开始标记 chunks.push({ delta: '', phase: 'start' }); // 内容块 let current = ''; for (const word of words) { current += (current ? ' ' : '') + word; if (current.length >= chunkSize) { chunks.push({ delta: current, phase: 'delta' }); current = ''; } } if (current) { chunks.push({ delta: current, phase: 'delta' }); } // 结束标记 chunks.push({ delta: '', phase: 'end' }); return chunks; } /** * Gateway Mock 配置 */ export interface MockGatewayConfig { /** 是否模拟延迟 */ simulateDelay?: boolean; /** 延迟时间 (ms) */ delayMs?: number; /** 是否模拟错误 */ simulateError?: boolean; /** 错误率 (0-1) */ errorRate?: number; /** 自定义响应覆盖 */ customResponses?: Partial; } /** * 设置完整的 Gateway Mock * 基于实际 API 端点: http://127.0.0.1:50051/api/* */ export async function setupMockGateway( page: Page, config: MockGatewayConfig = {} ): Promise { const { simulateDelay = false, delayMs = 100, simulateError = false, errorRate = 0, customResponses = {}, } = config; // 合并默认响应和自定义响应 const responses = { ...mockResponses, ...customResponses }; // ======================================== // 系统端点 // ======================================== // Mock 健康检查 - GET /api/health await page.route('**/api/health', async (route) => { if (simulateDelay) await delay(delayMs); if (shouldError(simulateError, errorRate)) { await route.fulfill({ status: 500, body: 'Internal Server Error' }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.health), }); }); // Mock 模型列表 - GET /api/models await page.route('**/api/models', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.models), }); }); // Mock 能力列表 - GET /api/capabilities await page.route('**/api/capabilities', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.capabilities), }); }); // Mock 工作区 - GET /api/workspace await page.route('**/api/workspace', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.workspace), }); }); // ======================================== // Agent 端点 (分身管理) // ======================================== // Mock Agent 列表 - GET /api/agents await page.route('**/api/agents', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); if (method === 'GET') { // 返回格式: { agents: [...] } 或 { clones: [...] } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ agents: responses.agents }), }); } else if (method === 'POST') { // 创建 Agent const body = route.request().postDataJSON(); const newAgent = { id: `agent-${Date.now()}`, name: body.name || body.manifest_toml?.match(/name\s*=\s*"([^"]+)"/)?.[1] || 'New Agent', role: body.role || 'Assistant', createdAt: new Date().toISOString(), bootstrapReady: false, }; responses.agents.push(newAgent as any); await route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(newAgent), }); } }); // Mock Agent 详情/更新/删除 - /api/agents/{id} await page.route('**/api/agents/*', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); const url = route.request().url(); // 排除 message 和 ws 端点 if (url.includes('/message') || url.includes('/ws')) { await route.continue(); return; } if (method === 'PUT') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true }), }); } else if (method === 'DELETE') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true }), }); } else { await route.continue(); } }); // Mock Agent 消息 - POST /api/agents/{id}/message await page.route('**/api/agents/*/message', async (route) => { if (simulateDelay) await delay(delayMs); if (route.request().method() === 'POST') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(createAgentMessageResponse('这是一个模拟响应。')), }); } }); // ======================================== // Hands 端点 // ======================================== // Mock Hands 列表 - GET /api/hands await page.route('**/api/hands', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ hands: responses.hands }), }); }); // Mock Hand 详情 - GET /api/hands/{name} await page.route('**/api/hands/*', async (route) => { if (simulateDelay) await delay(delayMs); const url = route.request().url(); // 排除 activate, runs 等子端点 if (url.includes('/activate') || url.includes('/runs')) { await route.continue(); return; } const handName = url.split('/hands/')[1]?.split('?')[0].split('/')[0]; const hand = responses.hands.find(h => h.name.toLowerCase() === handName?.toLowerCase()); if (hand) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(hand), }); } else { await route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: 'Hand not found' }), }); } }); // Mock Hand 激活/触发 - POST /api/hands/{name}/activate await page.route('**/api/hands/*/activate', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ runId: `run-${Date.now()}`, status: 'running', }), }); }); // Mock Hand 运行状态 - GET /api/hands/{name}/runs/{runId} await page.route('**/api/hands/*/runs/**', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); const url = route.request().url(); if (url.includes('/approve') && method === 'POST') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'approved' }), }); } else if (url.includes('/cancel') && method === 'POST') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'cancelled' }), }); } else if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ runId: `run-${Date.now()}`, status: 'completed', startedAt: new Date().toISOString(), completedAt: new Date().toISOString(), }), }); } else { // Fallback for any other requests await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok' }), }); } }); // Mock Hand 运行历史 - GET /api/hands/{name}/runs await page.route('**/api/hands/*/runs', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ runs: [] }), }); }); // Mock Hand 审批 - POST /api/hands/{name}/runs/{runId}/approve await page.route('**/api/hands/*/runs/*/approve', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'approved' }), }); }); // Mock Hand 取消 - POST /api/hands/{name}/runs/{runId}/cancel await page.route('**/api/hands/*/runs/*/cancel', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'cancelled' }), }); }); // ======================================== // Workflow 端点 // ======================================== // Mock 工作流列表 - GET /api/workflows await page.route('**/api/workflows', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ workflows: responses.workflows }), }); } else if (method === 'POST') { const body = route.request().postDataJSON(); const newWorkflow = { id: `wf-${Date.now()}`, ...body, status: 'idle', createdAt: new Date().toISOString(), }; responses.workflows.push(newWorkflow as any); await route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(newWorkflow), }); } }); // Mock 工作流执行 - POST /api/workflows/{id}/execute await page.route('**/api/workflows/*/execute', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ runId: `wf-run-${Date.now()}`, status: 'running', }), }); }); // ======================================== // Trigger 端点 // ======================================== // Mock 触发器列表 - GET /api/triggers await page.route('**/api/triggers', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ triggers: responses.triggers }), }); } else if (method === 'POST') { const body = route.request().postDataJSON(); const newTrigger = { id: `trigger-${Date.now()}`, ...body, }; responses.triggers.push(newTrigger as any); await route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(newTrigger), }); } }); // ======================================== // Approval 端点 // ======================================== // Mock 审批列表 - GET /api/approvals await page.route('**/api/approvals', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ approvals: responses.approvals }), }); }); // Mock 审批响应 - POST /api/approvals/{id}/respond await page.route('**/api/approvals/*/respond', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'responded' }), }); }); // ======================================== // Session 端点 // ======================================== // Mock 会话列表 - GET /api/sessions await page.route('**/api/sessions', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ sessions: responses.sessions }), }); }); // ======================================== // Skill 端点 // ======================================== // Mock 技能列表 - GET /api/skills await page.route('**/api/skills', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ skills: responses.skills }), }); }); // ======================================== // Stats 端点 // ======================================== // Mock 用量统计 - GET /api/stats/usage await page.route('**/api/stats/usage', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.usageStats), }); }); // Mock 会话统计 - GET /api/stats/sessions await page.route('**/api/stats/sessions', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ total: 10, active: 2 }), }); }); // ======================================== // Config 端点 // ======================================== // Mock 配置 - GET/PUT /api/config await page.route('**/api/config', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.quickConfig), }); } else if (method === 'PUT') { const body = route.request().postDataJSON(); Object.assign(responses.quickConfig, body); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.quickConfig), }); } }); // ======================================== // Plugin 端点 // ======================================== // Mock 插件状态 - GET /api/plugins/status await page.route('**/api/plugins/status', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.pluginStatus), }); }); // ======================================== // Security 端点 // ======================================== // Mock 安全状态 - GET /api/security/status await page.route('**/api/security/status', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(responses.securityStatus), }); }); // ======================================== // Scheduler 端点 // ======================================== // Mock 调度任务 - GET /api/scheduler/tasks await page.route('**/api/scheduler/tasks', async (route) => { if (simulateDelay) await delay(delayMs); const method = route.request().method(); if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: responses.scheduledTasks }), }); } else if (method === 'POST') { const body = route.request().postDataJSON(); const newTask = { id: `task-${Date.now()}`, ...body, }; responses.scheduledTasks.push(newTask as any); await route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(newTask), }); } }); // ======================================== // Audit 端点 // ======================================== // Mock 审计日志 - GET /api/audit/logs await page.route('**/api/audit/logs', async (route) => { if (simulateDelay) await delay(delayMs); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ logs: [], total: 0 }), }); }); } /** * Mock Agent 消息响应(非流式) */ export async function mockAgentMessageResponse(page: Page, response: string): Promise { await page.route('**/api/agents/*/message', async (route) => { if (route.request().method() === 'POST') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(createAgentMessageResponse(response)), }); } }); } /** * Mock 错误响应 */ export async function mockErrorResponse( page: Page, path: string, status: number, message: string ): Promise { await page.route(`**/api/${path}**`, async (route) => { await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify({ error: true, message, }), }); }); } /** * Mock 网络超时 */ export async function mockTimeout(page: Page, path: string): Promise { await page.route(`**/api/${path}**`, async () => { // 永不响应,模拟超时 await new Promise(() => {}); }); } /** * WebSocket Mock 配置 */ export interface MockWebSocketConfig { /** 模拟响应内容 */ responseContent?: string; /** 是否模拟流式响应 */ streaming?: boolean; /** 流式响应的块延迟 (ms) */ chunkDelay?: number; /** 是否模拟错误 */ simulateError?: boolean; /** 错误消息 */ errorMessage?: string; } /** * 存储 WebSocket Mock 配置 */ let wsConfig: MockWebSocketConfig = { responseContent: 'This is a mock streaming response from the WebSocket server.', streaming: true, chunkDelay: 50, }; /** * 设置 WebSocket Mock 配置 */ export function setWebSocketConfig(config: Partial): void { wsConfig = { ...wsConfig, ...config }; } /** * Mock Agent WebSocket 流式响应 * 使用 Playwright 的 routeWebSocket API 拦截 WebSocket 连接 */ export async function mockAgentWebSocket( page: Page, config: Partial = {} ): Promise { const finalConfig = { ...wsConfig, ...config }; await page.routeWebSocket('**/api/agents/*/ws', async (ws: WebSocketRoute) => { // Handle incoming messages from the page ws.onMessage(async (message) => { try { const data = JSON.parse(message); // Handle chat message if (data.type === 'message' || data.content) { // Send connected event first ws.send(JSON.stringify({ type: 'connected', agent_id: 'default-agent', })); // Simulate error if configured if (finalConfig.simulateError) { ws.send(JSON.stringify({ type: 'error', message: finalConfig.errorMessage || 'Mock WebSocket error', })); ws.close({ code: 1011, reason: 'Error' }); return; } const responseText = finalConfig.responseContent || 'Mock response'; if (finalConfig.streaming) { // Send typing indicator ws.send(JSON.stringify({ type: 'typing', state: 'start', })); // Stream response in chunks const words = responseText.split(' '); let current = ''; for (let i = 0; i < words.length; i++) { current += (current ? ' ' : '') + words[i]; // Send text delta every few words if (current.length >= 10 || i === words.length - 1) { await new Promise(resolve => setTimeout(resolve, finalConfig.chunkDelay || 50)); ws.send(JSON.stringify({ type: 'text_delta', content: current, })); current = ''; } } // Send typing stop ws.send(JSON.stringify({ type: 'typing', state: 'stop', })); // Send phase done ws.send(JSON.stringify({ type: 'phase', phase: 'done', })); } else { // Non-streaming response ws.send(JSON.stringify({ type: 'response', content: responseText, input_tokens: 100, output_tokens: responseText.split(' ').length, })); } // Close connection after response ws.close({ code: 1000, reason: 'Stream complete' }); } } catch (err) { console.error('WebSocket mock error:', err); ws.send(JSON.stringify({ type: 'error', message: 'Failed to parse message', })); } }); // Handle connection close from page ws.onClose(() => { // Clean up }); }); } /** * 设置完整的 Gateway Mock (包括 WebSocket) */ export async function setupMockGatewayWithWebSocket( page: Page, config: MockGatewayConfig & { wsConfig?: Partial } = {} ): Promise { // Setup HTTP mocks await setupMockGateway(page, config); // Setup WebSocket mock await mockAgentWebSocket(page, config.wsConfig || {}); } // 辅助函数 function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function shouldError(simulate: boolean, rate: number): boolean { if (!simulate) return false; return Math.random() < rate; }