Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Layer 1 break detection results (21/30 pass, 63%): - SaaS API: 5/5 pass (S3 skip no LLM key) - Admin V2: 5/6 pass (A6 flaky auth guard) - Desktop Chat: 3/6 pass (D1 no chat response in browser; D2/D3 skip non-Tauri) - Desktop Feature: 6/6 pass - Cross-System: 2/6 pass (4 blocked by login rate limit 429) Bugs found: - P0-01: Account lockout not enforced (locked_until set but not checked) - P1-01: Refresh token still valid after logout - P1-02: Desktop browser chat no response (stores not exposed) - P1-03: Provider API requires display_name (undocumented) Fixes applied: - desktop/src/index.css: @import -> @plugin for Tailwind v4 compatibility - Admin tests: correct credentials admin/admin123 from .env - Cross tests: correct dashboard endpoint /stats/dashboard
245 lines
8.6 KiB
TypeScript
245 lines
8.6 KiB
TypeScript
/**
|
||
* 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';
|
||
|
||
async function saasLogin(page: Page): Promise<string> {
|
||
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();
|
||
return json.token;
|
||
}
|
||
|
||
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'}`);
|
||
});
|