完整实现 Swarm 多智能体协作系统
- 新增 CLIPluginAdapter 统一接口 (backend/app/core/agent_adapter.py) - 新增 LLM 服务层,支持 Anthropic/OpenAI/DeepSeek/Ollama (backend/app/services/llm_service.py) - 新增 Agent 执行引擎,支持文件锁自动管理 (backend/app/services/agent_executor.py) - 新增 NativeLLMAgent 原生 LLM 适配器 (backend/app/adapters/native_llm_agent.py) - 新增进程管理器 (backend/app/services/process_manager.py) - 新增 Agent 控制 API (backend/app/routers/agents_control.py) - 新增 WebSocket 实时通信 (backend/app/routers/websocket.py) - 更新前端 AgentsPage,支持启动/停止 Agent - 测试通过:Agent 启动、批量操作、栅栏同步 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
92
frontend/tests/api.spec.ts
Normal file
92
frontend/tests/api.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// API 接口测试
|
||||
test.describe('Swarm API 接口测试', () => {
|
||||
const apiBase = 'http://localhost:8000/api';
|
||||
|
||||
test('健康检查接口', async ({ request }) => {
|
||||
const response = await request.get(`${apiBase}/health`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('status');
|
||||
expect(body).toHaveProperty('version');
|
||||
expect(body.status).toBe('healthy');
|
||||
});
|
||||
|
||||
test('获取 Agent 列表', async ({ request }) => {
|
||||
const response = await request.get(`${apiBase}/agents`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('agents');
|
||||
expect(Array.isArray(body.agents)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('获取文件锁列表', async ({ request }) => {
|
||||
const response = await request.get(`${apiBase}/locks`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('locks');
|
||||
expect(Array.isArray(body.locks)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('获取心跳列表', async ({ request }) => {
|
||||
const response = await request.get(`${apiBase}/heartbeats`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('heartbeats');
|
||||
});
|
||||
|
||||
test('注册 Agent', async ({ request }) => {
|
||||
const testAgent = {
|
||||
agent_id: `test-agent-${Date.now()}`,
|
||||
name: 'Test Agent',
|
||||
role: 'developer',
|
||||
model: 'test-model',
|
||||
description: 'Playwright 测试用 Agent',
|
||||
};
|
||||
|
||||
const response = await request.post(`${apiBase}/agents/register`, {
|
||||
data: testAgent,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const body = await response.json();
|
||||
expect(body.agent_id).toBe(testAgent.agent_id);
|
||||
expect(body.name).toBe(testAgent.name);
|
||||
});
|
||||
|
||||
test('解析任务文件', async ({ request }) => {
|
||||
const response = await request.post(`${apiBase}/parse-task`, {
|
||||
data: { task: '修复 src/auth/login.py 中的 bug' },
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('files');
|
||||
expect(Array.isArray(body.files)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('获取任务主要角色', async ({ request }) => {
|
||||
const response = await request.post(`${apiBase}/roles/primary`, {
|
||||
data: { task: '实现登录功能并编写测试用例' },
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('primary_role');
|
||||
expect(body).toHaveProperty('role_scores');
|
||||
});
|
||||
|
||||
test('列出工作流文件', async ({ request }) => {
|
||||
const response = await request.get(`${apiBase}/workflows/files`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toHaveProperty('files');
|
||||
expect(Array.isArray(body.files)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
267
frontend/tests/e2e-workflow.spec.ts
Normal file
267
frontend/tests/e2e-workflow.spec.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Swarm Command Center - 端到端工作流测试
|
||||
* 测试完整用户场景和交互流程
|
||||
*/
|
||||
|
||||
// 辅助函数:等待页面加载完成
|
||||
async function waitForPageLoad(page: Page) {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('text=Swarm Center', { state: 'visible' });
|
||||
}
|
||||
|
||||
// 辅助函数:导航到指定页面
|
||||
async function navigateTo(page: Page, menuText: string) {
|
||||
await page.click(`text=${menuText}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
test.describe('端到端工作流测试', () => {
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(baseUrl);
|
||||
await waitForPageLoad(page);
|
||||
});
|
||||
|
||||
test('完整用户旅程 - 从仪表盘到各功能模块', async ({ page }) => {
|
||||
// 1. 验证仪表盘首页
|
||||
await expect(page.locator('h1:has-text("仪表盘")')).toBeVisible();
|
||||
await expect(page.locator('text=系统概览与实时监控')).toBeVisible();
|
||||
|
||||
// 2. 点击统计卡片导航到 Agent 管理
|
||||
await page.click('text=Agent 总数');
|
||||
await expect(page).toHaveURL(/\/agents/);
|
||||
await expect(page.locator('h1:has-text("Agent 管理")')).toBeVisible();
|
||||
|
||||
// 3. 返回仪表盘
|
||||
await navigateTo(page, '仪表盘');
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
|
||||
// 4. 点击会议统计卡片
|
||||
await page.click('text=今日会议');
|
||||
await expect(page).toHaveURL(/\/meetings/);
|
||||
await expect(page.locator('h1:has-text("会议管理")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Agent 管理 - 完整 CRUD 流程', async ({ page }) => {
|
||||
await navigateTo(page, 'Agent 管理');
|
||||
|
||||
// 1. 检查页面元素
|
||||
await expect(page.locator('button:has-text("注册 Agent")')).toBeVisible();
|
||||
await expect(page.locator('input[placeholder="搜索 Agent..."]')).toBeVisible();
|
||||
|
||||
// 2. 打开注册对话框
|
||||
await page.click('button:has-text("注册 Agent")');
|
||||
await expect(page.locator('text=注册新 Agent')).toBeVisible();
|
||||
|
||||
// 3. 填写表单
|
||||
const testAgentName = `TestAgent-${Date.now()}`;
|
||||
await page.fill('input[name="name"]', testAgentName);
|
||||
await page.fill('input[name="model"]', 'gpt-4');
|
||||
await page.selectOption('select[name="role"]', 'developer');
|
||||
await page.fill('textarea[name="description"]', 'Playwright 端到端测试用 Agent');
|
||||
|
||||
// 4. 验证表单填写成功
|
||||
await expect(page.locator('input[name="name"]')).toHaveValue(testAgentName);
|
||||
await expect(page.locator('select[name="role"]')).toHaveValue('developer');
|
||||
|
||||
// 5. 关闭对话框(取消提交,避免影响实际数据)
|
||||
await page.click('button:has-text("取消")');
|
||||
await expect(page.locator('text=注册新 Agent')).not.toBeVisible();
|
||||
|
||||
// 6. 测试搜索功能
|
||||
await page.fill('input[placeholder="搜索 Agent..."]', 'nonexistent');
|
||||
await expect(page.locator('text=没有找到匹配的 Agent')).toBeVisible();
|
||||
});
|
||||
|
||||
test('会议管理 - 创建会议流程', async ({ page }) => {
|
||||
await navigateTo(page, '会议管理');
|
||||
|
||||
// 1. 检查页面元素
|
||||
await expect(page.locator('button:has-text("创建会议")')).toBeVisible();
|
||||
|
||||
// 2. 打开创建会议对话框
|
||||
await page.click('button:has-text("创建会议")');
|
||||
await expect(page.locator('text=创建新会议')).toBeVisible();
|
||||
|
||||
// 3. 填写会议表单
|
||||
const testMeetingTitle = `TestMeeting-${Date.now()}`;
|
||||
await page.fill('input[name="title"]', testMeetingTitle);
|
||||
await page.fill('textarea[name="agenda"]', '测试议程内容\n1. 测试项1\n2. 测试项2');
|
||||
await page.selectOption('select[name="type"]', 'design_review');
|
||||
|
||||
// 4. 添加参与者
|
||||
await page.click('button:has-text("添加参与者")');
|
||||
// 注:实际实现可能需要选择具体的 Agent
|
||||
|
||||
// 5. 验证表单填写
|
||||
await expect(page.locator('input[name="title"]')).toHaveValue(testMeetingTitle);
|
||||
|
||||
// 6. 关闭对话框
|
||||
await page.click('button:has-text("取消")');
|
||||
await expect(page.locator('text=创建新会议')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('资源监控 - 切换标签页和数据展示', async ({ page }) => {
|
||||
await navigateTo(page, '资源监控');
|
||||
|
||||
// 1. 验证标签页存在
|
||||
const tabs = ['文件锁', '心跳状态', 'Agent 状态'];
|
||||
for (const tab of tabs) {
|
||||
await expect(page.locator(`button:has-text("${tab}")`)).toBeVisible();
|
||||
}
|
||||
|
||||
// 2. 切换标签页
|
||||
await page.click('button:has-text("心跳状态")');
|
||||
await page.waitForTimeout(300); // 等待切换动画
|
||||
|
||||
await page.click('button:has-text("Agent 状态")');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.click('button:has-text("文件锁")');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// 3. 验证刷新按钮
|
||||
await expect(page.locator('button:has-text("刷新")')).toBeVisible();
|
||||
await page.click('button:has-text("刷新")');
|
||||
await expect(page.locator('text=加载中...')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('工作流页面 - 工作流操作', async ({ page }) => {
|
||||
await navigateTo(page, '工作流');
|
||||
|
||||
// 1. 验证页面元素
|
||||
await expect(page.locator('h1:has-text("工作流")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("新建工作流")')).toBeVisible();
|
||||
|
||||
// 2. 打开新建工作流对话框
|
||||
await page.click('button:has-text("新建工作流")');
|
||||
await expect(page.locator('text=新建工作流')).toBeVisible();
|
||||
|
||||
// 3. 填写工作流名称
|
||||
const workflowName = `TestWorkflow-${Date.now()}`;
|
||||
await page.fill('input[name="name"]', workflowName);
|
||||
|
||||
// 4. 关闭对话框
|
||||
await page.click('button:has-text("取消")');
|
||||
});
|
||||
|
||||
test('系统配置 - 保存设置', async ({ page }) => {
|
||||
await navigateTo(page, '配置');
|
||||
|
||||
// 1. 验证页面元素
|
||||
await expect(page.locator('h1:has-text("系统配置")')).toBeVisible();
|
||||
await expect(page.locator('text=API 基础地址')).toBeVisible();
|
||||
|
||||
// 2. 修改配置
|
||||
await page.fill('input[name="apiBaseUrl"]', 'http://localhost:9000');
|
||||
await page.fill('input[name="heartbeatInterval"]', '15');
|
||||
|
||||
// 3. 保存配置
|
||||
await page.click('button:has-text("保存配置")');
|
||||
|
||||
// 4. 验证成功提示
|
||||
await expect(page.locator('text=配置已保存')).toBeVisible();
|
||||
|
||||
// 5. 恢复原始配置
|
||||
await page.fill('input[name="apiBaseUrl"]', 'http://localhost:8000');
|
||||
await page.fill('input[name="heartbeatInterval"]', '30');
|
||||
await page.click('button:has-text("保存配置")');
|
||||
});
|
||||
|
||||
test('响应式布局 - 侧边栏折叠展开', async ({ page }) => {
|
||||
// 1. 验证侧边栏默认展开
|
||||
await expect(page.locator('text=仪表盘')).toBeVisible();
|
||||
await expect(page.locator('text=Agent 管理')).toBeVisible();
|
||||
|
||||
// 2. 测试移动端视图(如果支持)
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 3. 恢复桌面视图
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 4. 验证侧边栏仍然可见
|
||||
await expect(page.locator('text=仪表盘')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('错误处理和边界情况', () => {
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
|
||||
test('后端连接失败时的错误显示', async ({ page }) => {
|
||||
// 模拟后端不可用的情况
|
||||
await page.route('http://localhost:8000/api/**', (route) => {
|
||||
route.abort('failed');
|
||||
});
|
||||
|
||||
await page.goto(baseUrl);
|
||||
await waitForPageLoad(page);
|
||||
|
||||
// 验证错误提示显示
|
||||
await expect(page.locator('text=连接后端失败')).toBeVisible();
|
||||
});
|
||||
|
||||
test('404 页面处理', async ({ page }) => {
|
||||
await page.goto(`${baseUrl}/nonexistent-page`);
|
||||
await waitForPageLoad(page);
|
||||
|
||||
// 应该重定向到首页
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
await expect(page.locator('h1:has-text("仪表盘")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('快速导航切换', async ({ page }) => {
|
||||
await page.goto(baseUrl);
|
||||
await waitForPageLoad(page);
|
||||
|
||||
// 快速切换多个页面
|
||||
const pages = ['Agent 管理', '会议管理', '资源监控', '工作流', '配置'];
|
||||
for (const pageName of pages) {
|
||||
await navigateTo(page, pageName);
|
||||
}
|
||||
|
||||
// 最后回到仪表盘
|
||||
await navigateTo(page, '仪表盘');
|
||||
await expect(page.locator('h1:has-text("仪表盘")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('性能和加载测试', () => {
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
|
||||
test('页面加载性能', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
await page.goto(baseUrl);
|
||||
await waitForPageLoad(page);
|
||||
const loadTime = Date.now() - startTime;
|
||||
|
||||
// 验证页面加载时间在合理范围内(5秒内)
|
||||
expect(loadTime).toBeLessThan(5000);
|
||||
|
||||
// 验证关键元素渲染
|
||||
await expect(page.locator('h1:has-text("仪表盘")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('数据刷新功能', async ({ page }) => {
|
||||
await page.goto(baseUrl);
|
||||
await waitForPageLoad(page);
|
||||
|
||||
// 等待自动刷新间隔(10秒)
|
||||
// 这里我们只是验证初始加载成功
|
||||
await expect(page.locator('text=自动刷新')).toBeVisible();
|
||||
await expect(page.locator('text=10 秒')).toBeVisible();
|
||||
});
|
||||
|
||||
test('大量数据渲染性能', async ({ page }) => {
|
||||
// 这个测试需要后端返回大量数据
|
||||
// 目前只是验证页面结构可以支持列表渲染
|
||||
await navigateTo(page, 'Agent 管理');
|
||||
|
||||
// 验证列表容器存在
|
||||
await expect(page.locator('[data-testid="agent-list"]').or(page.locator('text=暂无 Agent 数据'))).toBeVisible();
|
||||
});
|
||||
});
|
||||
77
frontend/tests/navigation.spec.ts
Normal file
77
frontend/tests/navigation.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// 前端导航测试
|
||||
test.describe('Swarm Command Center - 前端导航测试', () => {
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(baseUrl);
|
||||
});
|
||||
|
||||
test('页面标题正确显示', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(/Swarm/);
|
||||
});
|
||||
|
||||
test('侧边栏导航显示正确', async ({ page }) => {
|
||||
// 检查 Logo 和标题
|
||||
await expect(page.locator('text=Swarm Center')).toBeVisible();
|
||||
await expect(page.locator('text=多智能体协作系统')).toBeVisible();
|
||||
|
||||
// 检查导航链接
|
||||
await expect(page.locator('text=仪表盘')).toBeVisible();
|
||||
await expect(page.locator('text=Agent 管理')).toBeVisible();
|
||||
await expect(page.locator('text=会议管理')).toBeVisible();
|
||||
await expect(page.locator('text=资源监控')).toBeVisible();
|
||||
await expect(page.locator('text=工作流')).toBeVisible();
|
||||
await expect(page.locator('text=配置')).toBeVisible();
|
||||
});
|
||||
|
||||
test('仪表盘页面加载', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("仪表盘")')).toBeVisible();
|
||||
await expect(page.locator('text=系统概览与实时监控')).toBeVisible();
|
||||
|
||||
// 检查统计卡片
|
||||
await expect(page.locator('text=Agent 总数')).toBeVisible();
|
||||
await expect(page.locator('text=今日会议')).toBeVisible();
|
||||
await expect(page.locator('text=文件锁')).toBeVisible();
|
||||
await expect(page.locator('text=在线 Agent')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Agent 管理页面导航', async ({ page }) => {
|
||||
await page.click('text=Agent 管理');
|
||||
await expect(page).toHaveURL(/\/agents/);
|
||||
await expect(page.locator('h1:has-text("Agent 管理")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("注册 Agent")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('会议管理页面导航', async ({ page }) => {
|
||||
await page.click('text=会议管理');
|
||||
await expect(page).toHaveURL(/\/meetings/);
|
||||
await expect(page.locator('h1:has-text("会议管理")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("创建会议")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('资源监控页面导航', async ({ page }) => {
|
||||
await page.click('text=资源监控');
|
||||
await expect(page).toHaveURL(/\/resources/);
|
||||
await expect(page.locator('h1:has-text("资源监控")')).toBeVisible();
|
||||
|
||||
// 检查标签页
|
||||
await expect(page.locator('button:has-text("文件锁")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("心跳状态")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Agent 状态")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('工作流页面导航', async ({ page }) => {
|
||||
await page.click('text=工作流');
|
||||
await expect(page).toHaveURL(/\/workflow/);
|
||||
await expect(page.locator('h1:has-text("工作流")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('配置页面导航', async ({ page }) => {
|
||||
await page.click('text=配置');
|
||||
await expect(page).toHaveURL(/\/settings/);
|
||||
await expect(page.locator('h1:has-text("系统配置")')).toBeVisible();
|
||||
await expect(page.locator('text=API 基础地址')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user