6599088e77
新增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>
541 lines
25 KiB
TypeScript
541 lines
25 KiB
TypeScript
/**
|
||
* ============================================================
|
||
* 性能和鲁棒性测试
|
||
*
|
||
* 性能测试:
|
||
* - 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(()=>{});
|
||
});
|
||
});
|