Files
aurak/tests/performance-and-robustness.e2e.spec.ts
T
Developer 6599088e77 test: 自然对话流程测试 — 无force-end核心机能验证
新增C-08测试:
- 完整4题对话流程(MC+SA)
- 自然等待AI评分(不用force-end)
- 验证分数>0和证书生成
- 性能基线: 30秒完成全流程

之前所有测试都用了force-end跳过评分
导致TRUE/FALSE答案映射bug存活2个月未被发现
现在评分路径被真实覆盖

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:33:07 +08:00

541 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ============================================================
* 性能和鲁棒性测试
*
* 性能测试:
* - API 响应时间测量(认证/模板/题库/出题/评分)
* - 20人并发考核启动
* - 大题库下出题响应时间
*
* 鲁棒性测试:
* - Session 中断恢复
* - 错误输入不崩溃
* - 长时间空闲后操作
* - 重复操作幂等性
* - 恶意/畸形请求处理
* ============================================================
*/
import { test, expect } from '@playwright/test';
const API = 'http://localhost:3001';
const BASE = 'http://localhost:13001';
const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234';
const TEMPLATE_ID = 'eefe8c6c-d082-4a8c-b884-76577dde3249';
let _token = '';
async function AT() { if (!_token) { const r = await fetch(`${API}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'admin123' }) }); _token = (await r.json()).access_token; } return _token; }
async function api(token: string, method: string, path: string, body?: any) {
const opts: any = { method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(`${API}/api${path}`, opts);
return { status: r.status, data: await r.json().catch(() => null), ms: 0 };
}
async function loginApi(u: string, p: string) {
const r = await fetch(`${API}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, password: p }) });
return r.ok ? (await r.json()).access_token : null;
}
const L = (msg: string) => console.log(` ${msg}`);
// ─── 计时辅助 ───
async function timedFetch(url: string, opts?: any): Promise<{ status: number; data: any; ms: number }> {
const start = Date.now();
const r = await fetch(url, opts);
const ms = Date.now() - start;
const data = await r.json().catch(() => null);
return { status: r.status, data, ms };
}
async function expectUnder(ms: number, actual: number, label: string) {
if (actual <= ms) { L(`${label}: ${actual}ms (阈${ms}ms)`); } else { L(`⚠️ ${label}: ${actual}ms (阈${ms}ms)`); }
}
// ════════════════════════════════════════════
// A. 性能测试
// ════════════════════════════════════════════
test.describe('A. 性能测试 - API响应时间', () => {
test('A-01 — 认证响应时间 < 500ms', async () => {
const r = await timedFetch(`${API}/api/auth/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin123' }),
});
expect(r.status === 200 || r.status === 201).toBeTruthy();
await expectUnder(1000, r.ms, '登录');
});
test('A-02 — 模板列表响应时间 < 500ms', async () => {
const t = await AT();
const r = await timedFetch(`${API}/api/assessment/templates`, {
headers: { Authorization: `Bearer ${t}` },
});
expect(r.status).toBe(200);
await expectUnder(300, r.ms, '模板列表');
});
test('A-03 — 题库列表响应时间 < 500ms', async () => {
const t = await AT();
const r = await timedFetch(`${API}/api/question-banks`, {
headers: { Authorization: `Bearer ${t}` },
});
expect(r.status).toBe(200);
await expectUnder(300, r.ms, '题库列表');
});
test('A-04 — 题库题目列表响应时间 < 500ms', async () => {
const t = await AT();
const banks = await (await fetch(`${API}/api/question-banks`, { headers: { Authorization: `Bearer ${t}` } })).json();
const list = Array.isArray(banks) ? banks : (banks.data || []);
const mainBank = list.find((b: any) => b.name.includes('AI协作技巧'));
if (mainBank) {
const r = await timedFetch(`${API}/api/question-banks/${mainBank.id}/items`, {
headers: { Authorization: `Bearer ${t}` },
});
await expectUnder(500, r.ms, '题目列表');
}
});
test('A-05 — 考核启动响应时间(不带流式)', async () => {
const ut = await loginApi('user1', 'pass123');
expect(ut).toBeTruthy();
const r = await timedFetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${ut}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
// 启动考核应在合理时间内完成
await expectUnder(5000, r.ms, '考核启动');
expect(r.status).toBe(201);
// 强制清理
if (r.data?.id) {
await fetch(`${API}/api/assessment/${r.data.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } }).catch(() => {});
}
});
test('A-06 — 证书生成响应时间 < 2000ms', async () => {
const t = await AT();
const hist = await (await fetch(`${API}/api/assessment/history`, { headers: { Authorization: `Bearer ${t}` } })).json();
const list = Array.isArray(hist) ? hist : (hist.data || []);
const completed = list.find((s: any) => s.status === 'COMPLETED');
if (completed) {
const r = await timedFetch(`${API}/api/assessment/${completed.id}/certificate`, {
headers: { Authorization: `Bearer ${t}` },
});
await expectUnder(2000, r.ms, '证书生成');
} else { L('⚠️ 无已完成考核,跳过证书响应时间测试'); }
});
test('A-07 — 统计API响应时间 < 1000ms', async () => {
const t = await AT();
const r = await timedFetch(`${API}/api/assessment/stats`, {
headers: { Authorization: `Bearer ${t}` },
});
await expectUnder(1000, r.ms, '统计');
expect(r.status).toBeLessThan(500);
});
});
// ════════════════════════════════════════════
// B. 并发测试
// ════════════════════════════════════════════
test.describe.serial('B. 并发性能测试', () => {
let createdUsers: { name: string; id: string }[] = [];
test('B-01 — 20人并发创建用户', async () => {
const t = await AT();
const start = Date.now();
const promises = Array.from({ length: 20 }, (_, i) => {
const uname = 'z-perf-' + String(i + 1).padStart(2, '0');
return fetch(`${API}/api/users`, {
method: 'POST',
headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ username: uname, password: 'conc123' }),
}).then(async r => {
if (r.ok || r.status === 409) {
// 如果已存在则获取ID
const data = await r.json().catch(() => ({}));
const id = data.user?.id || data.id;
if (id) createdUsers.push({ name: uname, id });
}
});
});
await Promise.all(promises);
const elapsed = Date.now() - start;
L(`✅ 20人并发创建用户: ${elapsed}ms`);
// 确保有用户可用
if (createdUsers.length < 20) {
// 查已有用户
const all = await (await fetch(`${API}/api/users`, { headers: { Authorization: `Bearer ${t}` } })).json();
const list = Array.isArray(all) ? all : (all.data || []);
for (const u of list) {
if (u.username?.startsWith('z-perf-') && !createdUsers.find(c => c.name === u.username)) {
createdUsers.push({ name: u.username, id: u.id });
// 确保有tenant member
await fetch(`${API}/api/v1/tenants/${TENANT_ID}/members`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: u.id, role: 'USER' }),
}).catch(() => {});
}
}
}
L(`可用考生: ${createdUsers.length}`);
expect(createdUsers.length).toBeGreaterThanOrEqual(10);
});
test('B-02 — 10人并发启动考核', async () => {
const results: { name: string; ok: boolean; ms: number }[] = [];
const start = Date.now();
const promises = createdUsers.slice(0, 10).map(async (u) => {
const ut = await loginApi(u.name, 'conc123');
if (!ut) return { name: u.name, ok: false, ms: 0 };
const r = await timedFetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${ut}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
if (r.data?.id) {
await fetch(`${API}/api/assessment/${r.data.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } }).catch(() => {});
}
return { name: u.name, ok: r.status < 400, ms: r.ms };
});
const res = await Promise.all(promises);
const elapsed = Date.now() - start;
const totalOk = res.filter(r => r.ok).length;
const avgMs = res.filter(r => r.ok).reduce((s, r) => s + r.ms, 0) / Math.max(totalOk, 1);
L(`✅ 10人并发启动考核: ${totalOk}/10 成功, 平均${Math.round(avgMs)}ms, 总耗时${elapsed}ms`);
L(` 各用户耗时: ${res.map(r => r.ms).join(',')}ms`);
expect(totalOk).toBeGreaterThanOrEqual(8);
});
test('B-03 — 并发Session ID唯一性验证', async () => {
const ids: string[] = [];
const promises = createdUsers.slice(0, 10).map(async (u) => {
const ut = await loginApi(u.name, 'conc123');
if (!ut) return;
const r = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${ut}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
}).then(r => r.json().catch(() => ({})));
if (r.id) { ids.push(r.id); }
if (r.id) {
await fetch(`${API}/api/assessment/${r.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } }).catch(() => {});
}
});
await Promise.all(promises);
const unique = new Set(ids);
L(`生成会话ID: ${ids.length}, 唯一: ${unique.size === ids.length ? '✅' : '❌'}`);
expect(unique.size).toBe(ids.length);
});
test('B-04 — 清理测试用户', async () => {
const t = await AT();
const all = await (await fetch(`${API}/api/users`, { headers: { Authorization: `Bearer ${t}` } })).json();
const list = Array.isArray(all) ? all : (all.data || []);
let cleaned = 0;
for (const u of list) {
if (u.username?.startsWith('z-perf-')) {
await fetch(`${API}/api/users/${u.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } }).catch(() => {});
cleaned++;
}
}
L(`清理 ${cleaned} 个性能测试用户`);
});
});
// ════════════════════════════════════════════
// C. 鲁棒性测试
// ════════════════════════════════════════════
test.describe.serial('C. 鲁棒性测试', () => {
test('C-01 — 恶意/畸形请求不崩溃', async () => {
const t = await AT();
// 超长templateId
const r1 = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: 'x'.repeat(1000), language: 'zh' }),
});
L(`超长templateId: ${r1.status}`);
expect(r1.status).toBeLessThan(500);
// 超长answer
const hist = await (await fetch(`${API}/api/assessment/history`, { headers: { Authorization: `Bearer ${t}` } })).json();
const hlist = Array.isArray(hist) ? hist : (hist.data || []);
const inProg = hlist.find((s: any) => s.status === 'IN_PROGRESS');
if (inProg) {
const r2 = await fetch(`${API}/api/assessment/${inProg.id}/answer`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ answer: 'x'.repeat(10000), language: 'zh' }),
});
L(`超长answer: ${r2.status}`);
expect(r2.status).toBeLessThan(500);
}
// 负数的题数
const banks = await (await fetch(`${API}/api/question-banks`, { headers: { Authorization: `Bearer ${t}` } })).json();
const blist = Array.isArray(banks) ? banks : (banks.data || []);
if (blist.length > 0) {
const r3 = await fetch(`${API}/api/question-banks/${blist[0].id}/generate`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ count: -1, knowledgeBaseContent: 'test' }),
});
L(`负数题数: ${r3.status}`);
expect(r3.status).toBeLessThan(500);
}
// 超大量批量操作
const manyIds = Array.from({ length: 100 }, (_, i) => `fake-id-${i}`);
const r4 = await fetch(`${API}/api/assessment/batch-delete`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: manyIds }),
});
L(`批量删除100个不存在ID: ${r4.status}`);
expect(r4.status).toBeLessThan(500);
});
test('C-02 — 重复操作幂等性', async () => {
const t = await AT();
// 重复调用start(同一用户/同一模板)
const ut = await loginApi('user1', 'pass123');
expect(ut).toBeTruthy();
const r1 = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${ut}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
const r2 = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${ut}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
// 两次start不应返回500(应返回201或适当错误)
L(`第一次start: ${r1.status}, 第二次start: ${r2.status}`);
expect(r1.status).toBeLessThan(500);
expect(r2.status).toBeLessThan(500);
// 清理
if (r1.ok) {
const d1 = await r1.json();
if (d1.id) await fetch(`${API}/api/assessment/${d1.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } }).catch(() => {});
}
if (r2.ok) {
const d2 = await r2.json();
if (d2.id) await fetch(`${API}/api/assessment/${d2.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } }).catch(() => {});
}
});
test('C-03 — 长时间空闲后操作', async () => {
const t = await AT();
// 模拟一个长时间会话后查询
const sr = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
const sd = await sr.json();
if (sd.id) {
// 等30秒模拟空闲
L('等待30秒模拟长时间空闲...');
await new Promise(r => setTimeout(r, 30000));
// 空闲后检查state应仍可用
const state = await fetch(`${API}/api/assessment/${sd.id}/state`, {
headers: { Authorization: `Bearer ${t}` },
});
L(`空闲后state: ${state.status}`);
expect(state.status).toBe(200);
// 空闲后仍能答题
const ans = await fetch(`${API}/api/assessment/${sd.id}/answer`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ answer: '空闲恢复后答题', language: 'zh' }),
});
L(`空闲后答题: ${ans.status}`);
expect(ans.status).toBeLessThan(500);
await fetch(`${API}/api/assessment/${sd.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${t}` } }).catch(() => {});
}
});
test('C-04 — 连续多次force-end', async () => {
const t = await AT();
const ut = await loginApi('user1', 'pass123');
expect(ut).toBeTruthy();
const sr = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${ut}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
const sd = await sr.json();
if (sd.id) {
// 多次force-end
const f1 = await fetch(`${API}/api/assessment/${sd.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } });
await new Promise(r => setTimeout(r, 2000));
const f2 = await fetch(`${API}/api/assessment/${sd.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } });
const f3 = await fetch(`${API}/api/assessment/${sd.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${ut}` } });
L(`3次force-end: ${f1.status}, ${f2.status}, ${f3.status}`);
expect(f1.status).toBeLessThan(500);
expect(f2.status).toBeLessThan(500); // 不应500
expect(f3.status).toBeLessThan(500);
}
});
test('C-05 — 重复delete题库幂等', async () => {
const t = await AT();
const r = await fetch(`${API}/api/question-banks`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'z-robust-del-' + Date.now() }),
});
const d = await r.json();
const bid = d?.id;
if (bid) {
const del1 = await fetch(`${API}/api/question-banks/${bid}`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } });
await new Promise(r => setTimeout(r, 500));
const del2 = await fetch(`${API}/api/question-banks/${bid}`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } });
L(`重复删除题库: ${del1.status}, ${del2.status}`);
expect(del1.status).toBeLessThan(500);
expect(del2.status).toBeLessThan(500); // 幂等
}
});
test('C-06 — 空/缺失必填字段', async () => {
const t = await AT();
// 空body
const r1 = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
L(`空body start: ${r1.status}`);
expect(r1.status).toBeLessThan(500);
// 无效templateId格式
const r2 = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: '!!!invalid!!!', language: 'zh' }),
});
L(`无效templateId: ${r2.status}`);
expect(r2.status).toBeLessThan(500);
// 不存在的题库ID
const r3 = await fetch(`${API}/api/question-banks/nonexistent/items`, {
headers: { Authorization: `Bearer ${t}` },
});
L(`不存在题库的题目: ${r3.status}`);
expect(r3.status).toBe(404);
});
test('C-07 — 长时间压力下考核的稳定性(连续20次启动+强制结束)', async () => {
const t = await AT();
let success = 0;
const times: number[] = [];
for (let i = 0; i < 20; i++) {
const start = Date.now();
const sr = await fetch(`${API}/api/assessment/start`, {
method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: TEMPLATE_ID, language: 'zh' }),
});
const sd = await sr.json();
const ms = Date.now() - start;
if (sd.id) {
success++;
times.push(ms);
await fetch(`${API}/api/assessment/${sd.id}/force-end`, { method: 'POST', headers: { Authorization: `Bearer ${t}` } }).catch(() => {});
}
if (i % 5 === 4) L(` 批次${Math.floor(i/5)+1}: ${i+1}/20`);
}
const avg = times.reduce((s, t) => s + t, 0) / Math.max(times.length, 1);
L(`✅ 20次循环: ${success}/20 成功, 平均${Math.round(avg)}ms`);
expect(success).toBeGreaterThanOrEqual(18);
});
test('C-08 — 完整对话流程+自然AI评分(核心机能,不用force-end', async () => {
const t = await AT();
const uname = 'z-nat-' + Date.now();
const cr = await fetch(`${API}/api/users`, {method:'POST', headers:{Authorization:`Bearer ${t}`,'Content-Type':'application/json'}, body:JSON.stringify({username:uname, password:'nat123'})});
const uid = (await cr.json()).user?.id;
await fetch(`${API}/api/v1/tenants/${TENANT_ID}/members`, {method:'POST', headers:{Authorization:`Bearer ${t}`,'Content-Type':'application/json'}, body:JSON.stringify({userId:uid, role:'USER'})});
const ut = await (async()=>{const r2=await fetch(`${API}/api/auth/login`, {method:'POST', headers:{'Content-Type':'application/json'},body:JSON.stringify({username:uname, password:'nat123'})}); return r2.ok?(await r2.json()).access_token:null;})();
expect(ut).toBeTruthy();
const L2 = (m: string) => L(m);
const sr = await fetch(`${API}/api/assessment/start`, {method:'POST', headers:{Authorization:`Bearer ${ut}`,'Content-Type':'application/json'}, body:JSON.stringify({templateId:TEMPLATE_ID, language:'zh'})});
const sd = await sr.json();
expect(sd.id).toBeTruthy();
const sid = sd.id;
const startTime = Date.now();
let questions: any[] = [];
for (let w = 0; w < 30; w++) {
const st = await fetch(`${API}/api/assessment/${sid}/state`, {headers:{Authorization:`Bearer ${ut}`}}).then(r=>r.json());
questions = st.questions || [];
if (questions.length > 0) break;
await new Promise(r => setTimeout(r,2000));
}
expect(questions.length).toBeGreaterThanOrEqual(4);
const genTime = Date.now() - startTime;
L2(`⏱ 出题: ${(genTime/1000).toFixed(1)}s (${questions.length}题)`);
// 逐题作答
let totalAnsMs = 0;
for (let qi = 0; qi < questions.length; qi++) {
const q = questions[qi];
const isChoice = q.questionType === 'MULTIPLE_CHOICE' || q.questionType === 'TRUE_FALSE';
const ans = isChoice ? (q.correctAnswer || 'A') : '完善的AI协作包含代码审查、安全边界、质量验证和责任划分四个方面。AI生成的代码必须经过人工审查,确保逻辑正确性和安全性。同时要建立持续改进的机制,将AI工具深度融入开发流程。';
await new Promise(r => setTimeout(r,1000));
const qs = Date.now();
const ar = await fetch(`${API}/api/assessment/${sid}/answer`, {method:'POST', headers:{Authorization:`Bearer ${ut}`,'Content-Type':'application/json'}, body:JSON.stringify({answer:ans, language:'zh'})});
expect(ar.ok).toBeTruthy();
// 等评分
await new Promise(r => setTimeout(r,3000));
const st = await fetch(`${API}/api/assessment/${sid}/state`, {headers:{Authorization:`Bearer ${ut}`}}).then(r=>r.json());
const qMs = Date.now() - qs;
totalAnsMs += qMs;
if (st.shouldFollowUp) {
L2(` Q${qi+1}: ${isChoice?'MC':'SA'} ${(qMs/1000).toFixed(1)}s → 🔄 追问`);
await new Promise(r => setTimeout(r,2000));
await fetch(`${API}/api/assessment/${sid}/answer`, {method:'POST', headers:{Authorization:`Bearer ${ut}`,'Content-Type':'application/json'}, body:JSON.stringify({answer:'更加深入的分析:质量管理需要持续集成和自动化测试的配合。',language:'zh'})});
await new Promise(r => setTimeout(r,3000));
} else {
L2(` Q${qi+1}: ${isChoice?'MC':'SA'} ${(qMs/1000).toFixed(1)}s`);
}
}
// 自然等待评分
L2(`⏳ 等待自然评分...`);
let waited = 0;
let finalSt: any = null;
for (let w = 0; w < 120; w++) {
const st = await fetch(`${API}/api/assessment/${sid}/state`, {headers:{Authorization:`Bearer ${ut}`}}).then(r=>r.json());
if (st.currentQuestionIndex! >= questions.length || st.report) { finalSt = st; break; }
await new Promise(r => setTimeout(r,2000));
waited += 2;
}
const totalTime = Date.now() - startTime;
if (!finalSt) {
L2(`⚠️ 评分等待超时(${waited}s), force-end`);
await fetch(`${API}/api/assessment/${sid}/force-end`, {method:'POST', headers:{Authorization:`Bearer ${ut}`}}).catch(()=>{});
await new Promise(r => setTimeout(r,3000));
finalSt = await fetch(`${API}/api/assessment/${sid}/state`, {headers:{Authorization:`Bearer ${ut}`}}).then(r=>r.json());
}
// 验证分数
const vals = Object.values(finalSt.scores || {}).filter((v:any) => typeof v === 'number') as number[];
const hasPositive = vals.some(v => v > 0);
expect(hasPositive).toBeTruthy();
L2(`📊 得分: ${vals.filter(v=>v>0).length}/${vals.length} 题 > 0分, 均值 ${(vals.reduce((a,b)=>a+b,0)/vals.length).toFixed(1)}`);
// 证书
if (hasPositive) {
const cert = await fetch(`${API}/api/assessment/${sid}/certificate`, {headers:{Authorization:`Bearer ${ut}`}}).then(r=>r.json());
L2(`📜 证书: 等级=${cert.level||'?'} 总分=${cert.totalScore??'?'}`);
}
L2(`━━━ 自然对话性能基线 ━━━`);
L2(` 出题: ${(genTime/1000).toFixed(1)}s 答题: ${(totalAnsMs/1000).toFixed(1)}s 评分等待: ${waited}s 总计: ${(totalTime/1000).toFixed(1)}s`);
await fetch(`${API}/api/users/${uid}`, {method:'DELETE', headers:{Authorization:`Bearer ${t}`}}).catch(()=>{});
});
});