import test from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; process.env.NODE_ENV = 'test'; process.env.DB_PATH = ':memory:'; const jsonFetch = async ( baseUrl: string, path: string, options?: { method?: string; body?: unknown }, ) => { const res = await fetch(`${baseUrl}${path}`, { method: options?.method ?? 'GET', headers: options?.body ? { 'Content-Type': 'application/json' } : undefined, body: options?.body ? JSON.stringify(options.body) : undefined, }); const text = await res.text(); let json: any = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { status: res.status, json, text }; }; test('题库文本导入增量模式按内容覆盖', async () => { const { initDatabase, run, get } = await import('../api/database'); await initDatabase(); const { app } = await import('../api/server'); const server = app.listen(0); try { const addr = server.address(); assert.ok(addr && typeof addr === 'object'); const baseUrl = `http://127.0.0.1:${addr.port}`; const oldId = randomUUID(); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [oldId, '重复题目', 'single', JSON.stringify(['A', 'B']), 'A', 5, '通用'], ); const res = await jsonFetch(baseUrl, '/api/questions/import-text', { method: 'POST', body: { mode: 'incremental', questions: [ { content: '重复题目', type: 'multiple', category: '数学', options: ['选项A', '选项B', '选项C'], answer: ['选项A', '选项C'], analysis: '解析:这是一道示例题', score: 10, }, ], }, }); assert.equal(res.status, 200); assert.equal(res.json?.success, true); assert.equal(res.json?.data?.inserted, 0); assert.equal(res.json?.data?.updated, 1); const row = await get(`SELECT * FROM questions WHERE content = ?`, ['重复题目']); assert.equal(row.id, oldId); assert.equal(row.type, 'multiple'); assert.equal(row.category, '数学'); assert.equal(row.score, 10); assert.equal(row.analysis, '解析:这是一道示例题'); assert.equal(JSON.parse(row.options).length, 3); assert.deepEqual(JSON.parse(row.answer), ['选项A', '选项C']); } finally { await new Promise((resolve) => server.close(() => resolve())); } }); test('题库文本导入覆盖模式清空题库与答题记录', async () => { const { initDatabase, run, get } = await import('../api/database'); await initDatabase(); const { app } = await import('../api/server'); const server = app.listen(0); try { const addr = server.address(); assert.ok(addr && typeof addr === 'object'); const baseUrl = `http://127.0.0.1:${addr.port}`; const userId = randomUUID(); const subjectId = randomUUID(); const oldQuestionId = randomUUID(); const recordId = randomUUID(); const answerId = randomUUID(); await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [ userId, '测试用户', `138${Math.floor(Math.random() * 1e8).toString().padStart(8, '0')}`, '', ]); await run( `INSERT INTO exam_subjects (id, name, type_ratios, category_ratios, total_score, duration_minutes) VALUES (?, ?, ?, ?, ?, ?)`, [ subjectId, '测试科目', JSON.stringify({ single: 100 }), JSON.stringify({ 通用: 100 }), 100, 60, ], ); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [oldQuestionId, '旧题目', 'single', JSON.stringify(['A', 'B']), 'A', 5, '通用'], ); await run( `INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [recordId, userId, subjectId, null, 5, 1, 1, new Date().toISOString()], ); await run( `INSERT INTO quiz_answers (id, record_id, question_id, user_answer, score, is_correct, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [answerId, recordId, oldQuestionId, 'A', 5, 1, new Date().toISOString()], ); const res = await jsonFetch(baseUrl, '/api/questions/import-text', { method: 'POST', body: { mode: 'overwrite', questions: [ { content: '新题目', type: 'single', category: '通用', options: ['A', 'B'], answer: 'A', analysis: '解析:新题目的说明', score: 5, }, ], }, }); assert.equal(res.status, 200); assert.equal(res.json?.success, true); assert.equal(res.json?.data?.inserted, 1); assert.equal(res.json?.data?.updated, 0); const questionCount = await get(`SELECT COUNT(*) as total FROM questions`); const recordCount = await get(`SELECT COUNT(*) as total FROM quiz_records`); const answerCount = await get(`SELECT COUNT(*) as total FROM quiz_answers`); assert.equal(questionCount.total, 1); assert.equal(recordCount.total, 0); assert.equal(answerCount.total, 0); const row = await get(`SELECT * FROM questions WHERE content = ?`, ['新题目']); assert.equal(row.analysis, '解析:新题目的说明'); } finally { await new Promise((resolve) => server.close(() => resolve())); } }); test('答题记录详情应返回题目解析字段', async () => { const { initDatabase, run } = await import('../api/database'); await initDatabase(); const { app } = await import('../api/server'); const server = app.listen(0); try { const addr = server.address(); assert.ok(addr && typeof addr === 'object'); const baseUrl = `http://127.0.0.1:${addr.port}`; const userId = randomUUID(); const questionId = randomUUID(); const recordId = randomUUID(); const answerId = randomUUID(); const createdAt = new Date().toISOString(); await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [ userId, '测试用户', '13800138000', '', ]); await run( `INSERT INTO questions (id, content, type, options, answer, analysis, score, category) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [questionId, '带解析题目', 'single', JSON.stringify(['A', 'B']), 'A', '解析内容', 5, '通用'], ); await run( `INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [recordId, userId, null, null, 5, 1, 1, createdAt], ); await run( `INSERT INTO quiz_answers (id, record_id, question_id, user_answer, score, is_correct, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [answerId, recordId, questionId, 'A', 5, 1, createdAt], ); const res = await jsonFetch(baseUrl, `/api/quiz/records/detail/${recordId}`); assert.equal(res.status, 200); assert.equal(res.json?.success, true); assert.equal(res.json?.data?.record?.id, recordId); assert.equal(Array.isArray(res.json?.data?.answers), true); assert.equal(res.json?.data?.answers?.[0]?.questionAnalysis, '解析内容'); } finally { await new Promise((resolve) => server.close(() => resolve())); } }); test('前端文本解析支持 | 分隔格式', async () => { const { parseTextQuestions } = await import('../src/utils/questionTextImport'); const input = [ '题型|题目类别|分值|题目内容|选项|答案|解析', '单选|通用|5|我国首都是哪里?|北京|上海|广州|深圳|A|我国首都为北京', '多选|通用|5|以下哪些是水果?|苹果|白菜|香蕉|西红柿|A,C,D|水果包括苹果/香蕉/西红柿', '判断|通用|2|地球是圆的||正确|地球接近球体', ].join('\n'); const res = parseTextQuestions(input); assert.deepEqual(res.errors, []); assert.equal(res.questions.length, 3); const single = res.questions.find((q: any) => q.type === 'single'); assert.ok(single); assert.deepEqual(single.options, ['北京', '上海', '广州', '深圳']); assert.equal(single.answer, '北京'); assert.equal(single.analysis, '我国首都为北京'); const multiple = res.questions.find((q: any) => q.type === 'multiple'); assert.ok(multiple); assert.deepEqual(multiple.options, ['苹果', '白菜', '香蕉', '西红柿']); assert.deepEqual(multiple.answer, ['苹果', '香蕉', '西红柿']); assert.equal(multiple.analysis, '水果包括苹果/香蕉/西红柿'); const judgment = res.questions.find((q: any) => q.type === 'judgment'); assert.ok(judgment); assert.equal(judgment.answer, '正确'); assert.equal(judgment.analysis, '地球接近球体'); }); test('题目类别列表应返回题库数量统计', async () => { const { initDatabase, run } = await import('../api/database'); await initDatabase(); const { app } = await import('../api/server'); const server = app.listen(0); try { const addr = server.address(); assert.ok(addr && typeof addr === 'object'); const baseUrl = `http://127.0.0.1:${addr.port}`; await run(`DELETE FROM quiz_answers`); await run(`DELETE FROM quiz_records`); await run(`DELETE FROM questions`); await run(`DELETE FROM question_categories`); await run(`INSERT INTO question_categories (id, name) VALUES (?, ?)`, [randomUUID(), '空类别']); const q1 = randomUUID(); const q2 = randomUUID(); const q3 = randomUUID(); const q4 = randomUUID(); const q5 = randomUUID(); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [q1, '数学题1', 'single', JSON.stringify(['A', 'B']), 'A', 5, '数学'], ); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [q2, '数学题2', 'single', JSON.stringify(['A', 'B']), 'A', 5, '数学'], ); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [q3, '英语题1', 'single', JSON.stringify(['A', 'B']), 'A', 5, '英语'], ); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [q4, '通用题1', 'single', JSON.stringify(['A', 'B']), 'A', 5, '通用'], ); await run( `INSERT INTO questions (id, content, type, options, answer, score, category) VALUES (?, ?, ?, ?, ?, ?, ?)`, [q5, '空类别题(旧数据)', 'single', JSON.stringify(['A', 'B']), 'A', 5, ''], ); const res = await jsonFetch(baseUrl, '/api/question-categories'); assert.equal(res.status, 200); assert.equal(res.json?.success, true); assert.equal(Array.isArray(res.json?.data), true); const list = res.json?.data as any[]; const findByName = (name: string) => list.find((c) => c?.name === name); assert.equal(findByName('数学')?.questionCount, 2); assert.equal(findByName('英语')?.questionCount, 1); assert.equal(findByName('通用')?.questionCount, 2); assert.equal(findByName('空类别')?.questionCount, 0); } finally { await new Promise((resolve) => server.close(() => resolve())); } });