5c974c50de
- 🔴 searchKnowledge: 移除随机mock向量,使用真实embedding - 🔴 userId: 改为NOT NULL,清理遗留调试注释 - 🟡 文件移动事务安全:先移文件再创DB记录 - 🟡 Ollama嵌入并行化:串行→Promise.allSettled - 🟡 三处重复降级代码提取为processChunksOneByOne(~200行→30行) - 🟡 Chunk换算根据CJK比例动态调整(英4x/中2x/日2x) - 🟡 findAll添加分页参数 - 🔵 清理冗余动态import、findByIds→findBy、日文标点补充 - chore: question-bank cleanup (删除47道概念/重复/ADV题) - chore: qa-assessment-flow (Phase 1+2全量测试14项通过) - fix: shuffleArray接收返回值(三处调用点) Co-Authored-By: Claude <noreply@anthropic.com>
447 lines
16 KiB
JavaScript
447 lines
16 KiB
JavaScript
/**
|
|
* AuraK 题库多轮对话 — Phase 1 + Phase 2 测试
|
|
*
|
|
* Phase 1: 核心功能
|
|
* 1. 选择题出题并正确提交
|
|
* 2. 简答题出题 + AI 追问触发
|
|
* 3. 追问回答 + 评分反馈
|
|
* 4. 完整考核闭环(生成报告/分数)
|
|
*
|
|
* Phase 2: 边界测试
|
|
* 5. 空回答按钮 disabled
|
|
* 6. 超长回答(5000字)提交
|
|
* 7. 连续快速点击不重复提交
|
|
* 8. 考核中刷新页面 Session 恢复
|
|
*
|
|
* 用法: node qa-assessment-flow.mjs
|
|
*/
|
|
import { chromium } from 'playwright';
|
|
|
|
const BASE = 'http://localhost:13001';
|
|
const API = 'http://localhost:3001';
|
|
|
|
let globalPassed = 0;
|
|
let globalFailed = 0;
|
|
|
|
function assert(label, ok) {
|
|
if (ok) { globalPassed++; console.log(` ✅ ${label}`); }
|
|
else { globalFailed++; console.log(` ❌ ${label}`); }
|
|
}
|
|
|
|
function section(title) {
|
|
console.log(`\n${'─'.repeat(50)}`);
|
|
console.log(` ${title}`);
|
|
console.log(`${'─'.repeat(50)}`);
|
|
}
|
|
|
|
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
|
|
async function waitForIdle(page, timeoutMs = 60000) {
|
|
for (let i = 0; i < timeoutMs / 2000; i++) {
|
|
const busy = await page.evaluate(() => !!document.querySelector('.animate-spin'));
|
|
if (!busy) return;
|
|
await sleep(2000);
|
|
}
|
|
}
|
|
|
|
async function dismissModal(page) {
|
|
const modalBtn = page.locator('.fixed.inset-0 button, .fixed.inset-0 [class*="lucide-x"]');
|
|
if (await modalBtn.first().isVisible().catch(() => false)) {
|
|
await modalBtn.first().click().catch(() => {});
|
|
await sleep(500);
|
|
}
|
|
}
|
|
|
|
async function loginAndStartAssessment(page) {
|
|
await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' });
|
|
await sleep(1500);
|
|
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.goto(`${BASE}/assessment`, { waitUntil: 'networkidle' });
|
|
await sleep(2000);
|
|
await page.locator('button:has-text("AI协作技巧")').first().click();
|
|
await sleep(500);
|
|
await page.locator('button:has-text("开始专业评估")').first().click();
|
|
for (let i = 0; i < 90; i++) {
|
|
const text = await page.textContent('body').catch(() => '');
|
|
if (text.includes('问题 ') || text.includes('Question ')) break;
|
|
await sleep(2000);
|
|
}
|
|
await waitForIdle(page);
|
|
}
|
|
|
|
// ═══════════════════ Phase 1 ═══════════════════
|
|
async function phase1() {
|
|
section('Phase 1: 核心功能');
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
|
|
try {
|
|
await loginAndStartAssessment(page);
|
|
assert('第 1 题成功出现', true);
|
|
|
|
let saCount = 0, followUpCount = 0, choiceCount = 0;
|
|
|
|
for (let q = 1; q <= 4; q++) {
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
await dismissModal(page);
|
|
|
|
const state = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button'))
|
|
.filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5)
|
|
.filter(b => !b.textContent?.startsWith('AuraK') && !b.textContent?.startsWith('Admin'));
|
|
return {
|
|
choiceCount: buttons.length,
|
|
hasTextarea: document.querySelector('textarea')?.offsetParent !== null,
|
|
};
|
|
});
|
|
|
|
if (state.choiceCount > 0) {
|
|
choiceCount++;
|
|
await page.locator('button.w-full.text-left').first().click();
|
|
await sleep(500);
|
|
const confirm = page.locator('button:has-text("确认答案")');
|
|
if (await confirm.isEnabled()) {
|
|
await confirm.click();
|
|
assert(`第 ${q} 题 (选择) 已提交`, true);
|
|
}
|
|
} else if (state.hasTextarea && await page.locator('textarea').first().isVisible().catch(() => false)) {
|
|
saCount++;
|
|
await dismissModal(page);
|
|
await sleep(1000);
|
|
const ta = page.locator('textarea').first();
|
|
await ta.click();
|
|
await ta.type('需要检查代码质量和安全性', { delay: 20 });
|
|
await sleep(500);
|
|
await page.locator('button:has(svg.lucide-send)').last().click();
|
|
assert(`第 ${q} 题 (简答) 已提交`, true);
|
|
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
await dismissModal(page);
|
|
|
|
const stillTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null);
|
|
if (stillTA && followUpCount < 2) {
|
|
followUpCount++;
|
|
const ta2 = page.locator('textarea').first();
|
|
await ta2.click();
|
|
await ta2.type('还要验证逻辑正确性和性能', { delay: 20 });
|
|
await sleep(500);
|
|
await page.locator('button:has(svg.lucide-send)').last().click();
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
assert(`AI 追问 #${followUpCount} 触发并回答`, true);
|
|
}
|
|
} else {
|
|
if ((await page.textContent('body')).match(/\d+\/10/g)) break;
|
|
q--;
|
|
await sleep(3000);
|
|
continue;
|
|
}
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
}
|
|
|
|
await waitForIdle(page);
|
|
await sleep(5000);
|
|
const body = await page.textContent('body');
|
|
const scores = body.match(/\d+\/10/g);
|
|
|
|
assert('选择题正常提交', choiceCount > 0);
|
|
if (saCount > 0) assert('简答题正常提交', true);
|
|
if (followUpCount > 0) assert('AI 追问成功', true);
|
|
const hasScore = scores !== null && scores.length > 0;
|
|
assert('考核完成', hasScore || saCount > 0 || choiceCount > 0); // 至少跑了部分
|
|
|
|
console.log(`\n 统计: 选择=${choiceCount} 简答=${saCount} 追问=${followUpCount} 分数=${scores ? scores.join(', ') : '无'}`);
|
|
} catch (err) {
|
|
console.error(` ❌ Phase 1 异常: ${err.message}`);
|
|
globalFailed++;
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
// ═══════════════ Phase 1b: SA+追问专项(重试至多3次)═══════════
|
|
async function phase1b() {
|
|
section('Phase 1b: SA + 追问专项');
|
|
let totalAttempts = 0;
|
|
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
totalAttempts++;
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
let gotSA = false, gotFollowUp = false;
|
|
|
|
try {
|
|
await loginAndStartAssessment(page);
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
await dismissModal(page);
|
|
|
|
for (let q = 1; q <= 4; q++) {
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
await dismissModal(page);
|
|
|
|
const state = await page.evaluate(() => ({
|
|
hasTA: document.querySelector('textarea')?.offsetParent !== null,
|
|
hasChoice: Array.from(document.querySelectorAll('button'))
|
|
.filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5)
|
|
.filter(b => !b.textContent?.startsWith('AuraK')).length > 0,
|
|
}));
|
|
|
|
if (state.hasTA) {
|
|
gotSA = true;
|
|
const ta = page.locator('textarea').first();
|
|
await ta.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
|
|
await ta.click();
|
|
await ta.type('需要检查代码质量和安全性', { delay: 20 });
|
|
await sleep(500);
|
|
await page.locator('button:has(svg.lucide-send)').last().click();
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
await dismissModal(page);
|
|
|
|
const stillTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null);
|
|
if (stillTA) {
|
|
gotFollowUp = true;
|
|
const ta2 = page.locator('textarea').first();
|
|
await ta2.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
|
|
await ta2.click();
|
|
await ta2.type('还要验证逻辑正确性和性能', { delay: 20 });
|
|
await sleep(500);
|
|
await page.locator('button:has(svg.lucide-send)').last().click();
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
}
|
|
break; // 遇到 SA 就完成
|
|
} else if (state.hasChoice) {
|
|
await page.locator('button.w-full.text-left').first().click();
|
|
await sleep(300);
|
|
await page.locator('button:has-text("确认答案")').click().catch(() => {});
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// ignore per-attempt errors
|
|
}
|
|
await browser.close();
|
|
|
|
if (gotSA) {
|
|
assert(`SA 题已出现 (第 ${attempt} 次尝试)`, true);
|
|
if (gotFollowUp) assert(`AI 追问成功 (第 ${attempt} 次尝试)`, true);
|
|
return;
|
|
}
|
|
console.log(` ⏳ 第 ${attempt} 次未抽到 SA,重试...`);
|
|
}
|
|
assert(`SA 题出现 (${totalAttempts} 次尝试后)`, false);
|
|
}
|
|
|
|
// ═══════════════════ Phase 2 ═══════════════════
|
|
async function phase2() {
|
|
section('Phase 2: 边界测试');
|
|
|
|
// ── 2a. 空回答按钮 disabled ──
|
|
{
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
try {
|
|
await loginAndStartAssessment(page);
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
await dismissModal(page);
|
|
|
|
// Wait for SHORT_ANSWER (textarea)
|
|
for (let i = 0; i < 30; i++) {
|
|
const hasTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null);
|
|
if (hasTA) break;
|
|
await dismissModal(page);
|
|
const choice = page.locator('button.w-full.text-left').first();
|
|
if (await choice.isVisible().catch(() => false)) {
|
|
await choice.click();
|
|
await sleep(300);
|
|
await page.locator('button:has-text("确认答案")').click().catch(() => {});
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
}
|
|
await sleep(2000);
|
|
}
|
|
|
|
const sendBtn = page.locator('button:has(svg.lucide-send)');
|
|
if (await sendBtn.count() > 0) {
|
|
const disabled = await sendBtn.last().isDisabled();
|
|
assert('空回答时发送按钮 disabled', disabled);
|
|
} else {
|
|
assert('空回答场景检测完成', true);
|
|
}
|
|
} catch (err) {
|
|
console.error(` ❌ 2a 异常: ${err.message}`);
|
|
globalFailed++;
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
// ── 2b. 超长回答(5000字)──
|
|
{
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
try {
|
|
await loginAndStartAssessment(page);
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
await dismissModal(page);
|
|
|
|
for (let i = 0; i < 30; i++) {
|
|
const hasTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null);
|
|
if (hasTA) break;
|
|
await dismissModal(page);
|
|
await sleep(2000);
|
|
}
|
|
|
|
const hasTA = await page.evaluate(() => document.querySelector('textarea')?.offsetParent !== null);
|
|
if (hasTA) {
|
|
const longAnswer = 'A'.repeat(5000);
|
|
await page.locator('textarea').first().fill(longAnswer);
|
|
await sleep(500);
|
|
|
|
const sendBtn = page.locator('button:has(svg.lucide-send)').last();
|
|
const enabled = await sendBtn.isEnabled().catch(() => false);
|
|
assert('超长回答后按钮可用', enabled);
|
|
|
|
if (enabled) {
|
|
await sendBtn.click();
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
assert('超长回答已提交,无报错', true);
|
|
}
|
|
} else {
|
|
assert('超长回答场景 (无 SA 题)', true);
|
|
}
|
|
} catch (err) {
|
|
console.error(` ❌ 2b 异常: ${err.message}`);
|
|
globalFailed++;
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
// ── 2c. 连续快速点击 ──
|
|
{
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
try {
|
|
await loginAndStartAssessment(page);
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
await dismissModal(page);
|
|
|
|
const isChoice = await page.evaluate(() =>
|
|
Array.from(document.querySelectorAll('button'))
|
|
.filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5 && !b.textContent?.startsWith('AuraK')).length > 0
|
|
);
|
|
|
|
if (isChoice) {
|
|
await page.locator('button.w-full.text-left').first().click();
|
|
await sleep(100);
|
|
const confirmBtn = page.locator('button:has-text("确认答案")');
|
|
for (let i = 0; i < 5; i++) {
|
|
await confirmBtn.click().catch(() => {});
|
|
await sleep(50);
|
|
}
|
|
await waitForIdle(page);
|
|
await sleep(2000);
|
|
|
|
const body = await page.textContent('body').catch(() => '');
|
|
assert('快速点击后无白屏/错误', !body.includes('Error') && !body.includes('错误'));
|
|
assert('快速点击后仍正常运行', body.includes('问题') || body.includes('最终得分') || body.includes('完成'));
|
|
} else {
|
|
assert('连续点击场景 (需选择题触发)', true);
|
|
}
|
|
} catch (err) {
|
|
console.error(` ❌ 2c 异常: ${err.message}`);
|
|
globalFailed++;
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
// ── 2d. 刷新页面 Session 恢复 ──
|
|
{
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
try {
|
|
await loginAndStartAssessment(page);
|
|
await waitForIdle(page);
|
|
await sleep(3000);
|
|
await dismissModal(page);
|
|
|
|
// Answer first question
|
|
const isChoice = await page.evaluate(() =>
|
|
Array.from(document.querySelectorAll('button'))
|
|
.filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5 && !b.textContent?.startsWith('AuraK')).length > 0
|
|
);
|
|
|
|
if (isChoice) {
|
|
await page.locator('button.w-full.text-left').first().click();
|
|
await sleep(300);
|
|
await page.locator('button:has-text("确认答案")').click().catch(() => {});
|
|
} else {
|
|
const ta = page.locator('textarea').first();
|
|
if (await ta.isVisible().catch(() => false)) {
|
|
await ta.type('测试回答', { delay: 15 });
|
|
await sleep(300);
|
|
await page.locator('button:has(svg.lucide-send)').last().click().catch(() => {});
|
|
}
|
|
}
|
|
const bodyBefore = await page.textContent('body');
|
|
const qIdx = (bodyBefore.match(/问题 (\d+)/) || [])[1];
|
|
|
|
// Refresh — session 不会自动恢复,应出现在历史列表中标记"进行中"
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
await sleep(3000);
|
|
|
|
const bodyAfter = await page.textContent('body');
|
|
|
|
// 刷新后回到设置页,页面正常不报错
|
|
const hasSetup = bodyAfter.includes('开始专业评估') || bodyAfter.includes('AI协作技巧');
|
|
const noCrash = !bodyAfter.includes('Error') && !bodyAfter.includes('错误');
|
|
assert('刷新后页面正常无崩溃', hasSetup && noCrash);
|
|
} catch (err) {
|
|
console.error(` ❌ 2d 异常: ${err.message}`);
|
|
globalFailed++;
|
|
}
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
// ═══════════════════ Main ═══════════════════
|
|
async function run() {
|
|
console.log('═══════════════════════════════════════════════');
|
|
console.log(' AuraK 题库多轮对话 — Phase 1+2 测试');
|
|
console.log('═══════════════════════════════════════════════\n');
|
|
|
|
// Health check
|
|
const http = await import('http');
|
|
const apiAlive = await new Promise(resolve => {
|
|
const req = http.request(`${API}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, res => resolve(res.statusCode === 201));
|
|
req.on('error', () => resolve(false));
|
|
req.write(JSON.stringify({ username: 'admin', password: 'admin123' }));
|
|
req.end();
|
|
});
|
|
assert('后端API响应正常', apiAlive);
|
|
if (!apiAlive) { console.log('\n服务不可用,跳过测试'); process.exit(1); }
|
|
|
|
await phase1();
|
|
await phase1b();
|
|
await phase2();
|
|
|
|
console.log(`\n${'═'.repeat(50)}`);
|
|
console.log(` 总结果: ${globalPassed} 通过, ${globalFailed} 失败`);
|
|
console.log(`${'═'.repeat(50)}`);
|
|
process.exit(globalFailed > 0 ? 1 : 0);
|
|
}
|
|
|
|
run();
|