/** * Smoke Tests — 跨系统闭环断裂探测 * * 6 个冒烟测试验证 Admin→SaaS→Desktop 的跨系统闭环。 * 同时操作 Admin API 和 Desktop UI,验证数据一致性。 * * 前提条件: * - SaaS Server: http://localhost:8080 * - Desktop App: http://localhost:1420 * - Admin V2: http://localhost:5173 * - 真实 LLM API Key (部分测试需要) * * 运行: cd desktop && npx playwright test smoke_cross */ import { test, expect, type Page } from '@playwright/test'; test.setTimeout(180000); const SaaS_BASE = 'http://localhost:8080/api/v1'; const ADMIN_USER = 'admin'; const ADMIN_PASS = 'admin123'; // Shared token — login once per worker to avoid rate limiting (5 req/min/IP). // Workers are isolated processes, so this is safe for parallel execution. let _sharedToken: string | null = null; async function getSharedToken(page: Page): Promise { if (_sharedToken) return _sharedToken; const res = await page.request.post(`${SaaS_BASE}/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); if (!res.ok()) { const body = await res.text(); console.log(`saasLogin failed: ${res.status()} — ${body.slice(0, 300)}`); } expect(res.ok()).toBeTruthy(); const json = await res.json(); _sharedToken = json.token; return _sharedToken; } // Keep saasLogin as alias for individual test clarity async function saasLogin(page: Page): Promise { return getSharedToken(page); } async function waitForDesktopReady(page: Page) { await page.goto('/'); await page.waitForLoadState('networkidle'); await page.waitForSelector('.h-screen', { timeout: 15000 }); } // ── X1: Admin 配 Provider → Desktop 可用 ────────────────────────── test('X1: Admin 创建 Provider → Desktop 模型列表包含', async ({ page }) => { const token = await saasLogin(page); const providerName = `cross_provider_${Date.now()}`; // Step 1: 通过 SaaS API 创建 Provider const provRes = await page.request.post(`${SaaS_BASE}/providers`, { headers: { Authorization: `Bearer ${token}` }, data: { name: providerName, provider_type: 'openai', base_url: 'https://api.cross.test/v1', enabled: true, }, }); expect(provRes.ok()).toBeTruthy(); const provJson = await provRes.json(); const providerId = provJson.id; // Step 2: 创建 Model const modelRes = await page.request.post(`${SaaS_BASE}/models`, { headers: { Authorization: `Bearer ${token}` }, data: { name: `cross_model_${Date.now()}`, provider_id: providerId, model_id: 'test-model', enabled: true, }, }); expect(modelRes.ok()).toBeTruthy(); // Step 3: 等待缓存刷新 (最多 60s) console.log('Waiting for cache refresh...'); await page.waitForTimeout(5000); // Step 4: 在 Desktop 检查模型列表 await waitForDesktopReady(page); // 通过 SaaS API 验证模型可被 relay 获取 const relayModels = await page.request.get(`${SaaS_BASE}/relay/models`, { headers: { Authorization: `Bearer ${token}` }, }); expect(relayModels.ok()).toBeTruthy(); const modelsJson = await relayModels.json(); const models = modelsJson.models || modelsJson.data || modelsJson; const found = Array.isArray(models) && models.some( (m: any) => m.id === providerId || m.name?.includes('cross_model') ); console.log(`X1: Model found in relay list: ${found}`); if (!found) { console.log('Available models:', JSON.stringify(models).slice(0, 500)); } }); // ── X2: Admin 改权限 → Desktop 受限 ─────────────────────────────── test('X2: Admin 禁用 user → Desktop 请求失败', async ({ page }) => { const token = await saasLogin(page); // 创建一个测试用户 const regRes = await page.request.post(`${SaaS_BASE}/auth/register`, { data: { username: `cross_user_${Date.now()}`, email: `cross_${Date.now()}@test.io`, password: 'TestPassword123', }, }); expect(regRes.ok()).toBeTruthy(); const regJson = await regRes.json(); const userId = regJson.account?.id || regJson.id; // 禁用该用户 if (userId) { const disableRes = await page.request.patch(`${SaaS_BASE}/accounts/${userId}`, { headers: { Authorization: `Bearer ${token}` }, data: { status: 'disabled' }, }); console.log(`Disable user: ${disableRes.status()}`); // 验证被禁用的用户无法登录 const loginRes = await page.request.post(`${SaaS_BASE}/auth/login`, { data: { username: regJson.username || `cross_user_${Date.now()}`, password: 'TestPassword123', }, }); console.log(`Disabled user login: ${loginRes.status()} (expected 401/403)`); expect([401, 403]).toContain(loginRes.status()); } }); // ── X3: Admin 创建知识 → Desktop 检索 ────────────────────────────── test('X3: Admin 创建知识条目 → SaaS 搜索可找到', async ({ page }) => { const token = await saasLogin(page); const uniqueContent = `跨系统测试知识_${Date.now()}_唯一标识`; // Step 1: 创建分类 const catRes = await page.request.post(`${SaaS_BASE}/knowledge/categories`, { headers: { Authorization: `Bearer ${token}` }, data: { name: `cross_cat_${Date.now()}`, description: 'Cross-system test' }, }); expect(catRes.ok()).toBeTruthy(); const catJson = await catRes.json(); // Step 2: 创建知识条目 const itemRes = await page.request.post(`${SaaS_BASE}/knowledge/items`, { headers: { Authorization: `Bearer ${token}` }, data: { title: uniqueContent, content: '这是跨系统知识检索测试的内容,包含特定关键词。', category_id: catJson.id, tags: ['cross-system', 'test'], }, }); expect(itemRes.ok()).toBeTruthy(); // Step 3: 通过 SaaS API 搜索 const searchRes = await page.request.post(`${SaaS_BASE}/knowledge/search`, { headers: { Authorization: `Bearer ${token}` }, data: { query: uniqueContent, limit: 5 }, }); expect(searchRes.ok()).toBeTruthy(); const searchJson = await searchRes.json(); const items = searchJson.items || searchJson; const found = Array.isArray(items) && items.some( (i: any) => i.title === uniqueContent ); expect(found).toBeTruthy(); console.log(`X3: Knowledge item found via search: ${found}`); }); // ── X4: Desktop 聊天 → Admin 统计更新 ───────────────────────────── test('X4: Dashboard 统计 — API 验证结构', async ({ page }) => { const token = await saasLogin(page); // Step 1: 获取初始统计 (correct endpoint: /stats/dashboard) const statsRes = await page.request.get(`${SaaS_BASE}/stats/dashboard`, { headers: { Authorization: `Bearer ${token}` }, }); expect(statsRes.ok()).toBeTruthy(); const stats = await statsRes.json(); // 验证统计结构 expect(stats).toHaveProperty('total_accounts'); expect(stats).toHaveProperty('active_providers'); expect(stats).toHaveProperty('active_models'); expect(stats).toHaveProperty('tasks_today'); console.log(`X4: Dashboard stats = ${JSON.stringify(stats).slice(0, 300)}`); }); // ── X5: TOTP 全流程 ────────────────────────────────────────────── test('X5: TOTP 设置→验证→登录', async ({ page }) => { const token = await saasLogin(page); // Step 1: 设置 TOTP const setupRes = await page.request.post(`${SaaS_BASE}/auth/totp/setup`, { headers: { Authorization: `Bearer ${token}` }, }); const setupStatus = setupRes.status(); if (setupStatus === 200 || setupStatus === 201) { const setupJson = await setupRes.json(); console.log(`X5: TOTP setup returned secret (encrypted): ${setupJson.secret ? 'yes' : 'no'}`); expect(setupJson.secret || setupJson.totp_secret).toBeTruthy(); } else { console.log(`X5: TOTP setup returned ${setupStatus} (may need verification endpoint)`); const body = await setupRes.text(); console.log(`Response: ${body.slice(0, 200)}`); } }); // ── X6: 计费一致性 ─────────────────────────────────────────────── test('X6: 计费一致性 — billing_usage 查询', async ({ page }) => { const token = await saasLogin(page); // 查询计费用量 const usageRes = await page.request.get(`${SaaS_BASE}/billing/usage`, { headers: { Authorization: `Bearer ${token}` }, }); expect(usageRes.ok()).toBeTruthy(); const usageJson = await usageRes.json(); // 验证结构 console.log(`X6: Billing usage = ${JSON.stringify(usageJson).slice(0, 300)}`); // 查询计划 const plansRes = await page.request.get(`${SaaS_BASE}/billing/plans`, { headers: { Authorization: `Bearer ${token}` }, }); expect(plansRes.ok()).toBeTruthy(); const plansJson = await plansRes.json(); console.log(`X6: Billing plans count = ${Array.isArray(plansJson) ? plansJson.length : 'object'}`); });