/** * 烟雾测试 — 快速发现人才测评系统当前故障 * * 覆盖 Phase 1 核心流程 + Phase 2 评分 + Phase 3 权限 * 不依赖被测系统之外的资源,纯 API + 少量 Playwright */ import { chromium } from 'playwright'; const API = 'http://localhost:3001'; const BASE = 'http://localhost:13001'; 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(60)); console.log(' 🔥 烟雾测试 — 人才测评系统健康状况检查'); console.log('█'.repeat(60)); const t0 = Date.now(); // ────────── 1. 环境 ────────── console.log('\n─── 1. 环境可达性 ───'); const adminT = await login('admin','admin123'); ok('admin 登录', !!adminT); const taT = await login('ta_admin','pass123'); ok('ta_admin 登录', !!taT); const u1T = await login('user1','pass123'); ok('user1 登录', !!u1T); // ────────── 2. 模板检查 ────────── console.log('\n─── 2. 模板与出题 ───'); // 2a. 模板列表 const tpls = await call(adminT,'GET','/assessment/templates'); ok('模板列表可获取', tpls.status === 200, `status=${tpls.status}`); const tplArr = Array.isArray(tpls.data) ? tpls.data : []; ok('至少有一个模板', tplArr.length > 0, `共${tplArr.length}个`); const techTpl = tplArr.find(t => t.name.includes('AI协作技巧')); const nonTechTpl = tplArr.find(t => t.name.includes('非技术人员')); ok('技术人员模板存在', !!techTpl); ok('非技术人员模板存在', !!nonTechTpl); // 2b. 检查模板 attemptLimit(不要为1导致admin被锁) if (techTpl) ok('技术人员模板 attemptLimit 正常', techTpl.attemptLimit === 0 || techTpl.attemptLimit > 1, `attemptLimit=${techTpl.attemptLimit}`); // 2c. 题库检查 const bank = await call(adminT,'GET','/question-banks/by-template/eefe8c6c-d082-4a8c-b884-76577dde3249'); ok('题库可获取', bank.status < 300, `status=${bank.status}`); let techBankItems = 0; if (bank.data?.id) { const items = await call(adminT,'GET',`/question-banks/${bank.data.id}/items`); techBankItems = Array.isArray(items.data) ? items.data.length : (items.data?.data||[]).length; ok('题库有题目', techBankItems > 0, `${techBankItems} 题`); } // 2d. 启动考核 const sr = await fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${u1T}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:'eefe8c6c-d082-4a8c-b884-76577dde3249',language:'zh'})}); const sd = await sr.json(); ok('启动考核正常', sr.ok && !!sd.id, `status=${sr.status} id=${sd.id?.substring(0,8)}`); // 2e. 异步出题等待 let questions = []; if (sd.id) { for (let w = 0; w < 30; w++) { const st = await fetch(`${API}/api/assessment/${sd.id}/state`,{headers:{Authorization:`Bearer ${u1T}`}}).then(r=>r.json()); questions = st.questions || []; if (questions.length > 0) break; await new Promise(r => setTimeout(r, 2000)); } ok('出题成功', questions.length > 0, `${questions.length} 题`); // 2f. 维度分布检查 if (questions.length > 0) { const dims = {}; questions.forEach(q => { dims[q.dimension] = (dims[q.dimension]||0)+1; }); ok('包含 PROMPT', (dims['PROMPT'] || 0) > 0); ok('包含 LLM', (dims['LLM'] || 0) > 0); // 2g. 答题 let answerOk = true; for (let qi = 0; qi < Math.min(questions.length, 4); qi++) { const q = questions[qi]; const isChoice = q.questionType === 'MULTIPLE_CHOICE' || q.questionType === 'TRUE_FALSE'; if (!q) continue; await new Promise(r => setTimeout(r, 1500)); const ansR = await fetch(`${API}/api/assessment/${sd.id}/answer`,{method:'POST',headers:{Authorization:`Bearer ${u1T}`,'Content-Type':'application/json'},body:JSON.stringify({answer:isChoice?'A':'烟雾测试回答',language:'zh'})}); if (!ansR.ok) answerOk = false; } ok('答题提交正常', answerOk); // 2h. 等待评分完成 await new Promise(r => setTimeout(r, 15000)); const state = await fetch(`${API}/api/assessment/${sd.id}/state`,{headers:{Authorization:`Bearer ${u1T}`}}).then(r=>r.json()); if (state.currentQuestionIndex >= state.questionCount || state.questionCount===undefined) { ok('评分状态正常', true); } else { ok('评分进行中', true); } // 2i. 强制结束后查看证书 await fetch(`${API}/api/assessment/${sd.id}/force-end`,{method:'POST',headers:{Authorization:`Bearer ${u1T}`}}); await new Promise(r => setTimeout(r, 3000)); const cert = await fetch(`${API}/api/assessment/${sd.id}/certificate`,{headers:{Authorization:`Bearer ${u1T}`}}).then(r=>r.json()); ok('证书可获取', !!cert.id || !!cert.level, `level=${cert.level||'?'} score=${cert.totalScore||'?'}`); ok('证书含等级', !!cert.level); ok('证书含总分', cert.totalScore !== undefined && cert.totalScore !== null); } } // ────────── 3. 权限隔离 ────────── console.log('\n─── 3. 权限隔离 ───'); // 3a. USER 不能管理模板 const userCreateTpl = await call(u1T,'POST','/assessment/templates',{name:'x',questionCount:5}); ok('USER 创建模板被拒', userCreateTpl.status >= 400, `status=${userCreateTpl.status}`); // 3b. TA 可创建模板 if (nonTechTpl) { // 不用实际创建,验证 TA 能查看模板即可 const taTpls = await call(taT,'GET','/assessment/templates'); ok('TA 可查看模板', taTpls.status === 200, `status=${taTpls.status}`); } // 3c. 题库权限 const userBank = await call(u1T,'GET','/question-banks'); ok('USER 可查看题库', userBank.status < 400 || userBank.status === 404, `status=${userBank.status}`); // 3d. 用户不能查看他人的答题回顾 const adminSessions = await call(adminT,'GET','/assessment/history'); const adminSessList = Array.isArray(adminSessions.data) ? adminSessions.data : []; if (adminSessList.length > 0) { const otherSessionId = adminSessList[0].id; const forbiddenReview = await fetch(`${API}/api/assessment/${otherSessionId}/review`,{headers:{Authorization:`Bearer ${u1T}`}}).then(r=>r.json()); ok('USER 不能查看他人回顾', !forbiddenReview.id || forbiddenReview.statusCode >= 400, `msg=${(forbiddenReview.message||'').substring(0,30)}`); } // ────────── 4. 前端 UI 快速检查 ────────── console.log('\n─── 4. 前端 UI 检查 ───'); const browser = await chromium.launch({headless:true}); const page = await browser.newPage({viewport:{width:1440,height:900}}); // Login await page.goto(BASE+'/login',{waitUntil:'networkidle'}); await page.waitForTimeout(1000); await page.locator('input[type="text"]').first().fill('admin'); await page.locator('input[type="password"]').first().fill('admin123'); await page.locator('button[type="submit"]').click(); await page.waitForURL('**/'); await page.waitForTimeout(1000); // 检查考核页 await page.goto(BASE+'/assessment',{waitUntil:'networkidle'}); await page.waitForTimeout(3000); const pageBody = await page.textContent('body'); ok('考核页渲染', pageBody.includes('AI协作') || pageBody.includes('模板'), `内容前100: ${pageBody.substring(0,100).replace(/\s+/g,' ')}`); // 检查两个模板按钮 const techBtn = await page.locator('button:has-text("AI协作技巧-对话测评")').isVisible().catch(()=>false); const nonTechBtn = await page.locator('button:has-text("AI协作-非技术人员测评")').isVisible().catch(()=>false); ok('技术人员模板按钮可见', techBtn); ok('非技术人员模板按钮可见', nonTechBtn); // 点击开启考核 if (techBtn) { await page.locator('button:has-text("AI协作技巧-对话测评")').first().click(); await page.waitForTimeout(500); const startBtn = await page.locator('button:has-text("开始专业评估")').isVisible().catch(()=>false); ok('开始评估按钮可见', startBtn); if (startBtn) { await page.locator('button:has-text("开始专业评估")').first().click(); await page.waitForTimeout(10000); const hasError = await page.evaluate(() => { const body = document.body.textContent || ''; return body.includes('Error') || body.includes('错误') || body.includes('Failed') || body.includes('找不到'); }); ok('点击开始无报错', !hasError); const hasQuestion = await page.evaluate(() => { const body = document.body.textContent || ''; return body.includes('问题 1') || body.includes('Question 1'); }); ok('题目已加载', hasQuestion); } } await browser.close(); // ────────── 5. 结果 ────────── const elapsed = Math.round((Date.now()-t0)/1000); console.log('\n' + '█'.repeat(60)); console.log(` 📊 烟雾测试报告 (${elapsed}秒)`); console.log(` ✅ ${pass} ❌ ${fail}`); console.log('█'.repeat(60)); if (fail > 0) { console.log(`\n ❌ ${fail} 个测试失败,需要修复`); process.exit(1); } else { console.log('\n 🎉 系统运行正常,核心流程全部通过!'); } } run().catch(e => { console.error('\n💥', e.message); process.exit(1); });