import assert from 'node:assert'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const planPagePath = path.resolve(__dirname, '../../src/views/train/plan/index.vue'); const enrollPanelPath = path.resolve(__dirname, '../../src/views/train/plan/components/EnrollmentPanel.vue'); const requestPath = path.resolve(__dirname, '../../src/utils/request.js'); const enrollApiPath = path.resolve(__dirname, '../../src/api/train/enrollment.js'); const planSource = fs.readFileSync(planPagePath, 'utf8'); const enrollSource = fs.readFileSync(enrollPanelPath, 'utf8'); const requestSource = fs.readFileSync(requestPath, 'utf8'); const enrollApiSource = fs.readFileSync(enrollApiPath, 'utf8'); console.log('=== Running Phase 0 Product Defects Remediation Regression Suite ===\n'); // ========================================== // 1. DEFECT 1 — REPORT-OVERDUE-01 (BLOCKER) // ========================================== console.log('--- Checking DEFECT 1: REPORT-OVERDUE-01 ---'); { // 1.1 request-level silentErrorCodes contract assert.strictEqual( requestSource.includes('const silentCodes = (res.config && res.config.silentErrorCodes) || []'), true, 'request.js 必须通过 res.config.silentErrorCodes 按需抑制全局 Notification' ); assert.strictEqual( requestSource.includes('!silentCodes.includes(code)'), true, '当业务错误码不在 silentErrorCodes 中时必须正常触发 Notification.error' ); assert.strictEqual( enrollApiSource.includes('silentErrorCodes: [40051]'), true, 'confirmReport API 请求必须显式声明 silentErrorCodes: [40051]' ); assert.strictEqual( requestSource.includes('err.code = code'), true, 'request.js 必须在 rejected Error 上附加业务错误码 code' ); // 1.2 模拟 request.js 拦截器行为:仅当请求配置 silentErrorCodes 包含对应 code 时抑制 let notifications = []; const mockNotification = { error: (opts) => { notifications.push(opts.title); } }; const simulateResponseInterceptor = (res) => { const code = res.data.code || 200; const msg = res.data.msg || '系统未知错误'; if (code !== 200) { const silentCodes = (res.config && res.config.silentErrorCodes) || []; if (!silentCodes.includes(code)) { mockNotification.error({ title: msg }); } const err = new Error(msg); err.code = code; err.data = res.data; return Promise.reject(err); } return Promise.resolve(res.data); }; // 测试用例 A: confirmReport 携带 silentErrorCodes: [40051],40051 被抑制 notifications = []; try { await simulateResponseInterceptor({ data: { code: 40051, msg: '报到时限提醒' }, config: { url: '/train/enrollments/1/report', silentErrorCodes: [40051] } }); } catch (e) { assert.strictEqual(e.code, 40051); } assert.strictEqual(notifications.length, 0, 'confirmReport 请求收到 40051 不得触发全局 Notification.error'); // 测试用例 B: 其他普通请求收到 40051,未配置 silentErrorCodes,必须正常弹出 Notification.error notifications = []; try { await simulateResponseInterceptor({ data: { code: 40051, msg: '其他模块时限提醒' }, config: { url: '/train/other/action' } }); } catch (e) { assert.strictEqual(e.code, 40051); } assert.strictEqual(notifications.length, 1, '未配置 silentErrorCodes 的普通请求收到 40051 必须正常弹出全局 Notification.error'); assert.strictEqual(notifications[0], '其他模块时限提醒'); // 1.3 EnrollmentPanel.vue: doConfirmReport & 循环防重控制 assert.strictEqual( enrollSource.includes('!forceOverride && isOverdueWarning'), true, '二次确认弹窗必须严格限定在首次提交 (!forceOverride) 且发生逾期警告时触发' ); assert.strictEqual( enrollSource.includes('this.$modal.msgError((err && err.message) || \'报到失败\')'), true, 'override 请求失败或非逾期错误必须结束流程并展示实际错误,不得再次弹窗' ); // 1.4 模拟报到确认全生命周期: // 场景 A: initial 40051 -> confirm dialog exactly once -> cancel -> no mutation // 场景 B: initial 40051 -> confirm dialog exactly once -> confirm -> override success // 场景 C: initial 40051 -> confirm -> override request fails with 40051 again -> NO second dialog, error message displayed let dialogShownCount = 0; let modalErrors = []; const simulateDoConfirmReport = async (row, forceOverride = false, backendBehavior = 'normal_overdue') => { const payload = { reportWay: 4, verifyNote: '管理员人工核验到班' }; if (forceOverride) { payload.forceOverride = true; } try { if (!forceOverride) { const err = new Error('已超过本期培训报到截止时间,触发时限提醒'); err.code = 40051; throw err; } if (backendBehavior === 'override_fails_again') { const err = new Error('强制逾期报到仍被拒绝:业务已封存'); err.code = 40051; throw err; } return { code: 200, msg: '报到成功' }; } catch (err) { const isOverdueWarning = err && ( err.code === 40051 || (err.message && (err.message.includes('40051') || err.message.includes('已超过本期培训报到截止时间') || err.message.includes('报到截止时间'))) ); if (!forceOverride && isOverdueWarning) { dialogShownCount++; // 模拟用户确认 return simulateDoConfirmReport(row, true, backendBehavior); } else { modalErrors.push(err.message || '报到失败'); return { failed: true, error: err }; } } }; // 回归测试 1: 首次 40051 弹窗刚好 1 次,用户确认后二次提交成功 dialogShownCount = 0; modalErrors = []; const resSuccess = await simulateDoConfirmReport({ enrollmentId: 101 }, false, 'normal_overdue'); assert.strictEqual(dialogShownCount, 1, '逾期报到确认弹窗必须刚好出现 1 次'); assert.strictEqual(resSuccess.code, 200, '二次确认携带 override 后成功'); assert.strictEqual(modalErrors.length, 0, '成功流程不得展示错误弹窗'); // 回归测试 2: 二次提交 (forceOverride=true) 再次收到 40051,严禁再次弹窗,必须结束流程并展示实际错误 dialogShownCount = 0; modalErrors = []; const resFail = await simulateDoConfirmReport({ enrollmentId: 101 }, false, 'override_fails_again'); assert.strictEqual(dialogShownCount, 1, 'override 再次失败时不得出现第 2 次确认弹窗'); assert.strictEqual(modalErrors.length, 1, 'override 失败必须展示实际错误'); assert.strictEqual(modalErrors[0], '强制逾期报到仍被拒绝:业务已封存'); console.log('✔ PASS: REPORT-OVERDUE-01 verified (request-level suppression + no confirmation loop + single prompt)'); } // ========================================== // 2. DEFECT 2 — ENROLL-PICKER-01 (MAJOR) // ========================================== console.log('\n--- Checking DEFECT 2: ENROLL-PICKER-01 ---'); { // 2.1 修复 sGender=0 的 falsy 缺陷 assert.strictEqual( enrollSource.includes('scope.row.sGender !== undefined && scope.row.sGender !== null ? scope.row.sGender : scope.row.gender'), true, '性别列必须显式判断 scope.row.sGender !== undefined && !== null,禁止将 0 当作 falsy 过滤' ); const formatGender = (val) => { if (val === '0' || val === 0 || val === '男') return '男'; if (val === '1' || val === 1 || val === '女') return '女'; return '-'; }; // 性别映射全覆盖测试 assert.strictEqual(formatGender(0), '男', 'sGender=0 必须正确渲染为 男'); assert.strictEqual(formatGender('0'), '男', 'sGender="0" 必须正确渲染为 男'); assert.strictEqual(formatGender(1), '女', 'sGender=1 必须正确渲染为 女'); assert.strictEqual(formatGender('1'), '女', 'sGender="1" 必须正确渲染为 女'); assert.strictEqual(formatGender(null), '-', 'null 必须安全退化为 -'); assert.strictEqual(formatGender(undefined), '-', 'undefined 必须安全退化为 -'); assert.strictEqual(formatGender(''), '-', '空字符串必须安全退化为 -'); // 2.2 验证表格列字段映射 assert.strictEqual( enrollSource.includes('prop="sName"'), true, '学员姓名列 prop 必须为 sName' ); assert.strictEqual( enrollSource.includes('scope.row.sName || scope.row.studentName || \'-\''), true, '学员姓名展示必须优先取 sName' ); assert.strictEqual( enrollSource.includes('prop="sGender"'), true, '学员性别列 prop 必须为 sGender' ); assert.strictEqual( enrollSource.includes('scope.row.idCardMasked || \'-\''), true, '身份证号列必须绑定 idCardMasked' ); // 2.3 收敛 StudentQuery 检索字段:严格单字段 sName assert.strictEqual( enrollSource.includes("studentSearchQuery: {\n sName: ''\n }"), true, 'studentSearchQuery 仅保留单一契约字段 sName,删除冗余的 studentName' ); assert.strictEqual( enrollSource.includes("sName: keyword || undefined"), true, 'searchStudentPool 请求参数仅发送 sName: keyword' ); assert.strictEqual( enrollSource.includes("studentName: keyword"), false, '严禁在请求中同时冗余发送 studentName' ); // 2.4 selectedStudentText 缺失姓名时占位符修复:严禁退回 sId / studentId const getSelectedStudentText = (selectedStudent) => { if (!selectedStudent) return ''; const name = (selectedStudent.sName || selectedStudent.studentName || '').trim(); const org = selectedStudent.orgName ? ` - ${selectedStudent.orgName}` : ''; return name ? `${name}${org}` : '-'; }; // 测试用例 A: 完整学员 const normalStudent = { sId: 'S1001', sName: '张伟', orgName: '大连供电段' }; assert.strictEqual(getSelectedStudentText(normalStudent), '张伟 - 大连供电段'); // 测试用例 B: 姓名缺失但存在 sId / studentId 的异常情况 const anonymousStudent = { sId: 'S9999', studentId: 9999, sName: '', orgName: '沈阳车辆段' }; const textAnonymous = getSelectedStudentText(anonymousStudent); assert.strictEqual(textAnonymous, '-', '姓名缺失时必须展示明确占位符 -,严禁退化展示内部数字/编号 ID'); assert.strictEqual(textAnonymous.includes('S9999'), false, '展示严禁包含 internal ID'); assert.strictEqual(textAnonymous.includes('undefined'), false, '展示严禁包含 undefined'); console.log('✔ PASS: ENROLL-PICKER-01 verified (sGender falsy fixed + singular sName search + human-readable display)'); } // ========================================== // 3. AUDIT: PLAN-EDIT-01 & PLAN-DATE-RULE-01 // ========================================== console.log('\n--- Auditing PLAN-EDIT-01 & PLAN-DATE-RULE-01 (Scope Rollback) ---'); { assert.strictEqual( planSource.includes("this.form.startTime >= this.form.endTime"), true, '原始计划表单已包含 startTime >= endTime 校验' ); assert.strictEqual( planSource.includes("this.form.reportDeadline > this.form.startTime"), true, '原始计划表单已包含 reportDeadline > startTime 校验' ); assert.strictEqual( planSource.includes("this.$modal.msgError('报到截止时间不得晚于开班时间')"), true, '原始计划表单已包含明确错误提示阻止非法提交' ); console.log('✔ PASS: Verified PLAN-EDIT-01 and PLAN-DATE-RULE-01 are cleanly retained at baseline with original checks intact.'); } console.log('\n================================================================'); console.log('ALL PHASE 0 DEFECT REMEDIATION REGRESSION TESTS PASSED (0 FAIL)'); console.log('================================================================');