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>
244 lines
8.1 KiB
TypeScript
244 lines
8.1 KiB
TypeScript
/**
|
|
* OpenFang API 端点兼容性测试
|
|
*
|
|
* 验证 ZCLAW 前端与 OpenFang 后端的 REST API 兼容性。
|
|
*/
|
|
|
|
import { test, expect, Page } from '@playwright/test';
|
|
import { openFangResponses } from '../fixtures/openfang-responses';
|
|
|
|
const BASE_URL = 'http://localhost:1420';
|
|
|
|
async function setupMockAPI(page: Page) {
|
|
await page.route('**/api/health', async route => {
|
|
await route.fulfill({ json: openFangResponses.health });
|
|
});
|
|
|
|
await page.route('**/api/status', async route => {
|
|
await route.fulfill({ json: openFangResponses.status });
|
|
});
|
|
|
|
await page.route('**/api/agents', async route => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ json: openFangResponses.agents });
|
|
} else if (route.request().method() === 'POST') {
|
|
await route.fulfill({ json: { clone: { id: 'new-agent-001', name: 'New Agent' } } });
|
|
}
|
|
});
|
|
|
|
await page.route('**/api/agents/*', async route => {
|
|
await route.fulfill({ json: openFangResponses.agent });
|
|
});
|
|
|
|
await page.route('**/api/models', async route => {
|
|
await route.fulfill({ json: openFangResponses.models });
|
|
});
|
|
|
|
await page.route('**/api/hands', async route => {
|
|
await route.fulfill({ json: openFangResponses.hands });
|
|
});
|
|
|
|
await page.route('**/api/hands/*', async route => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ json: openFangResponses.hand });
|
|
} else if (route.request().url().includes('/activate')) {
|
|
await route.fulfill({ json: openFangResponses.handActivation });
|
|
}
|
|
});
|
|
|
|
await page.route('**/api/workflows', async route => {
|
|
await route.fulfill({ json: openFangResponses.workflows });
|
|
});
|
|
|
|
await page.route('**/api/workflows/*', async route => {
|
|
await route.fulfill({ json: openFangResponses.workflow });
|
|
});
|
|
|
|
await page.route('**/api/sessions', async route => {
|
|
await route.fulfill({ json: openFangResponses.sessions });
|
|
});
|
|
|
|
await page.route('**/api/config', async route => {
|
|
await route.fulfill({ json: openFangResponses.config });
|
|
});
|
|
|
|
await page.route('**/api/channels', async route => {
|
|
await route.fulfill({ json: openFangResponses.channels });
|
|
});
|
|
|
|
await page.route('**/api/skills', async route => {
|
|
await route.fulfill({ json: openFangResponses.skills });
|
|
});
|
|
}
|
|
|
|
test.describe('OpenFang API 端点兼容性测试', () => {
|
|
|
|
test.describe('API-01: Health 端点', () => {
|
|
test('应返回正确的健康状态', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/health');
|
|
return res.json();
|
|
});
|
|
expect(response.status).toBe('ok');
|
|
expect(response.version).toBeDefined();
|
|
});
|
|
});
|
|
|
|
test.describe('API-02: Agents 端点', () => {
|
|
test('应返回 Agent 列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/agents');
|
|
return res.json();
|
|
});
|
|
expect(Array.isArray(response)).toBe(true);
|
|
expect(response[0]).toHaveProperty('id');
|
|
expect(response[0]).toHaveProperty('name');
|
|
expect(response[0]).toHaveProperty('state');
|
|
});
|
|
});
|
|
|
|
test.describe('API-03: Create Agent 端点', () => {
|
|
test('应创建新 Agent', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/agents', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: 'Test Agent', model: 'qwen3.5-plus' }),
|
|
});
|
|
return res.json();
|
|
});
|
|
expect(response.clone).toHaveProperty('id');
|
|
expect(response.clone).toHaveProperty('name');
|
|
});
|
|
});
|
|
|
|
test.describe('API-04: Hands 端点', () => {
|
|
test('应返回 Hands 列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/hands');
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('hands');
|
|
expect(Array.isArray(response.hands)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('API-05: Hand Activation 端点', () => {
|
|
test('应激活 Hand 并返回 instance_id', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/hands/Browser/activate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
});
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('instance_id');
|
|
expect(response).toHaveProperty('status');
|
|
});
|
|
});
|
|
|
|
test.describe('API-06: Workflows 端点', () => {
|
|
test('应返回工作流列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/workflows');
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('workflows');
|
|
expect(Array.isArray(response.workflows)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('API-07: Sessions 端点', () => {
|
|
test('应返回会话列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/sessions');
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('sessions');
|
|
expect(Array.isArray(response.sessions)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('API-08: Models 端点', () => {
|
|
test('应返回模型列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/models');
|
|
return res.json();
|
|
});
|
|
expect(Array.isArray(response)).toBe(true);
|
|
expect(response[0]).toHaveProperty('id');
|
|
expect(response[0]).toHaveProperty('name');
|
|
expect(response[0]).toHaveProperty('provider');
|
|
});
|
|
});
|
|
|
|
test.describe('API-09: Config 端点', () => {
|
|
test('应返回配置信息', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/config');
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('data_dir');
|
|
expect(response).toHaveProperty('default_model');
|
|
});
|
|
});
|
|
|
|
test.describe('API-10: Channels 端点', () => {
|
|
test('应返回通道列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/channels');
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('channels');
|
|
expect(Array.isArray(response.channels)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('API-11: Skills 端点', () => {
|
|
test('应返回技能列表', async ({ page }) => {
|
|
await setupMockAPI(page);
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/skills');
|
|
return res.json();
|
|
});
|
|
expect(response).toHaveProperty('skills');
|
|
expect(Array.isArray(response.skills)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('API-12: Error Handling', () => {
|
|
test('应正确处理 404 错误', async ({ page }) => {
|
|
await page.route('**/api/nonexistent', async route => {
|
|
await route.fulfill({ status: 404, json: { error: 'Not found' } });
|
|
});
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/nonexistent');
|
|
return { status: res.status, body: await res.json() };
|
|
});
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
test('应正确处理 500 错误', async ({ page }) => {
|
|
await page.route('**/api/error', async route => {
|
|
await route.fulfill({ status: 500, json: { error: 'Internal server error' } });
|
|
});
|
|
const response = await page.evaluate(async () => {
|
|
const res = await fetch('/api/error');
|
|
return { status: res.status, body: await res.json() };
|
|
});
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
});
|