Files
zclaw_openfang/desktop/tests/e2e/fixtures/mock-gateway.ts
iven ce562e8bfc feat: complete Phase 1-3 architecture optimization
Phase 1 - Security:
- Add AES-GCM encryption for localStorage fallback
- Enforce WSS protocol for non-localhost WebSocket connections
- Add URL sanitization to prevent XSS in markdown links

Phase 2 - Domain Reorganization:
- Create Intelligence Domain with Valtio store and caching
- Add unified intelligence-client for Rust backend integration
- Migrate from legacy agent-memory, heartbeat, reflection modules

Phase 3 - Core Optimization:
- Add virtual scrolling for ChatArea with react-window
- Implement LRU cache with TTL for intelligence operations
- Add message virtualization utilities

Additional:
- Add OpenFang compatibility test suite
- Update E2E test fixtures
- Add audit logging infrastructure
- Update project documentation and plans

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 22:11:50 +08:00

789 lines
21 KiB
TypeScript

/**
* Gateway Mock 工具
* 模拟后端 Gateway 服务,用于独立测试前端功能
* 基于实际 API 端点: http://127.0.0.1:50051
*/
import { Page } 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<typeof mockResponses>;
}
/**
* 设置完整的 Gateway Mock
* 基于实际 API 端点: http://127.0.0.1:50051/api/*
*/
export async function setupMockGateway(
page: Page,
config: MockGatewayConfig = {}
): Promise<void> {
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(),
}),
});
}
});
// 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: [] }),
});
});
// ========================================
// 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<void> {
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<void> {
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<void> {
await page.route(`**/api/${path}**`, async () => {
// 永不响应,模拟超时
await new Promise(() => {});
});
}
// 辅助函数
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function shouldError(simulate: boolean, rate: number): boolean {
if (!simulate) return false;
return Math.random() < rate;
}