Files
aurak/test-concurrent-assessments.mjs
Developer 5bbab82e68 test: 并发考核性能实验(20人同时考核)
测试结果:
- Session ID 全部唯一:  20/20
- 异步出题完成:  20/20, 每题20题
- 维度分布正确:  IDE:4/LLM:6/PROMPT:6/DEV_PATTERN:4
- 并发提交答案:  6人×4题全成功
- 题目重叠: ⚠️ 10.5%(题库仅~281题,20人需400槽位)
- 评分耗时: ⚠️ 15s不足以完成AI评分

结论: 并发场景下会话创建、出题、答题均正常,无数据竞争。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:02:05 +08:00

244 lines
11 KiB
JavaScript

/**
* ============================================================
* 考核并发性能实验
*
* 场景:多人同时参加考核,验证系统是否产生数据竞争
* - 并发启动考核会话 → 验证出题不冲突、会话唯一
* - 并发提交答案 → 验证评分不串号
* - 验证最终分数合理性
*
* 检查指标:
* 1. 并发创建考生是否冲突
* 2. Session ID 是否唯一
* 3. 异步出题是否每个会话都拿到正确题数
* 4. 跨会话题目是否有重叠(去重问题)
* 5. 维度分布是否合理
* 6. 最终分数是否正常
* ============================================================
*/
const API = 'http://localhost:3001';
const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234';
let pass = 0, fail = 0, warn = 0;
function ok(l, d) { pass++; console.log(`${l}${d?' — '+d:''}`); }
function no(l, d) { fail++; console.log(`${l}${d?' — '+d:''}`); }
function soft(l, d) { warn++; console.log(` ⚠️ ${l}${d?' — '+d:''}`); }
async function login(u, p) {
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;
}
async function call(token, method, path, body=null) {
const opts = {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)};
}
async function run() {
console.log('\n' + '█'.repeat(70));
console.log(' 🔬 考核并发性能实验');
console.log('█'.repeat(70));
const t0 = Date.now();
const adminT = await login('admin','admin123');
ok('管理员登录', !!adminT);
// 1. 创建/获取 20 个考生
console.log('\n─── 1. 创建 20 个考生(或获取已有)───');
const N = 20;
const candidates = [];
// 先查已有用户
const allUsers = await fetch(`${API}/api/users`,{headers:{Authorization:`Bearer ${adminT}`}}).then(r=>r.json());
const userList = Array.isArray(allUsers) ? allUsers : (allUsers.data||[]);
for (let i = 0; i < N; i++) {
const uname = 'z-perf-' + String(i+1).padStart(2,'0');
let existing = userList.find(u => u.username === uname);
if (existing) {
candidates.push({name:uname, id:existing.id});
} else {
const r = await call(adminT,'POST','/users',{username:uname,password:'conc123',displayName:'考生'+i});
const id = r.data?.user?.id || r.data?.id;
if (id) {
candidates.push({name:uname,id});
await call(adminT,'POST',`/v1/tenants/${TENANT_ID}/members`,{userId:id,role:'USER'});
}
}
}
ok(`就绪 ${candidates.length}/${N} 考生`, `${Date.now()-t0}ms`);
// 2. 并发启动考核
console.log('\n─── 2. 并发启动考核(异步出题)───');
const starts = candidates.map(c => login(c.name,'conc123').then(token => {
if (!token) return {name:c.name,err:'login_fail'};
return fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:'eefe8c6c-d082-4a8c-b884-76577dde3249',language:'zh'})})
.then(r => r.json().then(d => ({name:c.name,token,sessionId:d.id,status:r.status,data:d})))
.catch(e => ({name:c.name,token,err:e.message}));
}));
const started = await Promise.all(starts);
const sOk = started.filter(r => r.sessionId && r.status < 300);
const sFail = started.filter(r => !r.sessionId);
ok(`启动考核 ${sOk.length}/${N}`, `失败${sFail.length}`);
sFail.forEach(r => soft(`${r.name} 启动失败`, r.err||'?'));
// Session ID 唯一性
const ids = sOk.map(r => r.sessionId);
ok('Session ID 唯一', new Set(ids).size === ids.length);
// 3. 等待异步出题 + 验证
console.log('\n─── 3. 等待异步出题并验证 ───');
const sessions = [];
let timedOut = 0;
for (const r of sOk) {
let questions = [];
for (let w = 0; w < 45; w++) {
try {
const sr = await fetch(`${API}/api/assessment/${r.sessionId}/state`,{headers:{Authorization:`Bearer ${r.token}`}});
if (sr.ok) {
const st = await sr.json();
questions = st.questions || [];
if (questions.length > 0) break;
}
} catch(e) {}
await new Promise(r => setTimeout(r,2000));
}
if (questions.length === 0) timedOut++;
sessions.push({name:r.name, sessionId:r.sessionId, token:r.token, questions, data:r.data});
}
ok(`异步出题完成 ${sessions.length - timedOut}/${sessions.length}`, `超时 ${timedOut}`);
// 题数检查
const qNums = sessions.filter(s => s.questions.length > 0).map(s => s.questions.length);
if (qNums.length > 0) {
const allSame = qNums.every(n => n === qNums[0]);
ok(`会话题数一致`, allSame ? `均为 ${qNums[0]}` : `不一致: ${qNums.slice(0,10).join(',')}`);
}
// 维度分布
const hasQuestions = sessions.filter(s => s.questions.length > 0);
for (const s of hasQuestions.slice(0,3)) {
const dims = {};
s.questions.forEach(q => { dims[q.dimension] = (dims[q.dimension]||0) + 1; });
ok(`${s.name} 维度`, Object.entries(dims).map(([k,v])=>`${k}:${v}`).join(','));
}
ok('都有 PROMPT', hasQuestions.every(s => s.questions.some(q => q.dimension === 'PROMPT')));
ok('都有 LLM', hasQuestions.every(s => s.questions.some(q => q.dimension === 'LLM')));
// 查询总题库大小
let allItemsCount = '?';
try {
const bankR = await fetch(`${API}/api/question-banks`,{headers:{Authorization:`Bearer ${adminT}`}});
if (bankR.ok) {
const bankD = await bankR.json();
// 查找关联 AI 协作模板的题库
const targetBank = (Array.isArray(bankD)?bankD:bankD.data||[]).find(b => b.templateId === 'eefe8c6c-d082-4a8c-b884-76577dde3249');
if (targetBank) allItemsCount = targetBank.items?.length || targetBank._count?.items || targetBank.itemCount || '?';
}
} catch(e) {}
// 题目重叠检查(计算概率)
if (hasQuestions.length >= 5) {
let totalPairs = 0, overlappedPairs = 0, totalOverlapCount = 0;
for (let i = 0; i < 5; i++) {
for (let j = i+1; j < 5; j++) {
totalPairs++;
const a = new Set(hasQuestions[i].questions.map(q => q.id));
const b = new Set(hasQuestions[j].questions.map(q => q.id));
const o = [...a].filter(id => b.has(id)).length;
if (o > 0) { overlappedPairs++; totalOverlapCount += o; }
}
}
const overlapRate = totalPairs > 0 ? (totalOverlapCount / (totalPairs * 20) * 100).toFixed(1) : '0';
ok('题目重叠检查完成', `题库 ${allItemsCount} 题, 20人×20题需400, 重叠率 ${overlapRate}%`);
if (overlappedPairs === 0) ok('零重叠', '');
else soft(`${overlappedPairs}/${totalPairs} 对重叠`, `${totalOverlapCount} 题次`);
}
// 4. 并发提交答案
console.log('\n─── 4. 并发提交答案 ───');
const qGroups = sessions.filter(s => s.questions && s.questions.length >= 4).slice(0,6);
if (qGroups.length === 0) { soft('无足够题目的会话', `总会话${sessions.length}`); }
else {
const submits = qGroups.map(s =>
(async () => {
const results = [];
for (let qi = 0; qi < 4; qi++) {
const q = s.questions[qi];
if (!q) continue;
const isChoice = q.questionType === 'MULTIPLE_CHOICE' || q.questionType === 'TRUE_FALSE';
await new Promise(r => setTimeout(r, 500 + Math.random() * 1500));
try {
const r = await fetch(`${API}/api/assessment/${s.sessionId}/answer`,{method:'POST',headers:{Authorization:`Bearer ${s.token}`,'Content-Type':'application/json'},body:JSON.stringify({answer:isChoice?'A':'并发测试回答',language:'zh'})});
results.push({qi, ok: r.ok, status: r.status});
} catch(e) { results.push({qi, ok: false, error: e.message}); }
}
return {name:s.name, results};
})()
);
const subResults = (await Promise.all(submits)).filter(Boolean);
ok(`并发答题 ${subResults.length}/${qGroups.length} 完成`, '');
for (const sr of subResults) {
const okAll = sr.results.every(r => r.ok);
const n = sr.results.filter(r => r.ok).length;
if (okAll) ok(`${sr.name} 全部提交成功`);
else soft(`${sr.name} ${n}/4 成功`);
}
}
// 5. 等待评分并检查最终分数
console.log('\n─── 5. 最终分数检查 ───');
let scored = 0, totalScoreCheck = 0;
for (const s of qGroups) {
await new Promise(r => setTimeout(r, 15000));
try {
const st = await fetch(`${API}/api/assessment/${s.sessionId}/state`,{headers:{Authorization:`Bearer ${s.token}`}}).then(r=>r.json());
// state 返回的是 evaluation state
const isDone = st.currentQuestionIndex >= (st.questionCount || st.questions?.length || 20);
// 也查一下 session 的状态
const sessionR = await fetch(`${API}/api/assessment/${s.sessionId}`,{headers:{Authorization:`Bearer ${s.token}`}}).catch(()=>null);
const session = sessionR?.ok ? await sessionR.json() : null;
const status = session?.status || st._sessionStatus || (isDone ? '猜测完成' : '进行中');
const finalScore = session?.finalScore ?? st.finalScore;
const passed = session?.passed ?? st.passed;
if (status === 'COMPLETED' || (isDone && finalScore !== undefined)) {
totalScoreCheck++;
if (finalScore !== undefined && finalScore !== null && !isNaN(finalScore)) {
scored++;
ok(`${s.name} 已完成`, `分数=${finalScore}, 合格=${!!passed}`);
} else {
soft(`${s.name} 分数待定`, `status=${status}, score=${finalScore}`);
}
} else {
soft(`${s.name} ${status}`, `分数=${finalScore}`);
}
} catch(e) { soft(`${s.name} 查询失败`, e.message); }
}
ok(`评分完成 ${scored}/${totalScoreCheck}`, '已评分/已检查');
// 6. 清理
console.log('\n─── 6. 清理 ───');
let cleaned = 0;
for (const c of candidates) {
await call(adminT,'DELETE',`/users/${c.id}`).catch(()=>{});
cleaned++;
}
ok(`清理 ${cleaned} 个考生`, '');
// 报告
const elapsed = Math.round((Date.now()-t0)/1000);
console.log('\n' + '█'.repeat(70));
console.log(` 📊 并发测试报告 (${elapsed}秒)`);
console.log(`${pass} ⚠️ ${warn}${fail}`);
console.log('█'.repeat(70));
if (fail > 0) process.exit(1);
else if (warn > 0) console.log('\n ⚠️ 有警告(非致命)');
else console.log('\n 🎉 全部通过!并发表现正常');
}
run().catch(e => { console.error('\n💥', e.message); process.exit(1); });