diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AttendanceActionContractTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AttendanceActionContractTest.java index f2a514e70..abfa95a40 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AttendanceActionContractTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AttendanceActionContractTest.java @@ -196,16 +196,18 @@ public class AttendanceActionContractTest lesson.setPlanCourseId(10L); lessons.put(1001L, lesson); - // 学员 E_A 属于 PLAN_A + // 学员 E_A 属于 PLAN_A (1:已报到在训) TrainEnrollment ea = new TrainEnrollment(); ea.setEnrollmentId(201L); ea.setPlanId("PLAN_A"); + ea.setEnrollmentStatus(1); enrollments.put(201L, ea); - // 学员 E_B 属于 PLAN_B (跨班) + // 学员 E_B 属于 PLAN_B (跨班, 1:已报到在训) TrainEnrollment eb = new TrainEnrollment(); eb.setEnrollmentId(202L); eb.setPlanId("PLAN_B"); + eb.setEnrollmentStatus(1); enrollments.put(202L, eb); } @@ -594,4 +596,227 @@ public class AttendanceActionContractTest assertEquals(AttendanceErrorCode.LESSON_NOT_FOUND, (int) e.getCode()); } } + + @Test + public void testSignInLifecycle_Status0_WaitingReport_Rejected() + { + // status=0 待报到学员:严禁直接签到绕过报到生命周期 + TrainEnrollment e0 = new TrainEnrollment(); + e0.setEnrollmentId(300L); + e0.setPlanId("PLAN_A"); + e0.setEnrollmentStatus(0); // 0: 待报到 + enrollments.put(300L, e0); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(300L); + dto.setLessonId(1001L); + + try + { + appService.signIn(dto, "admin"); + fail("待报到学员必须被拦截禁止签到"); + } + catch (ServiceException e) + { + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT, (int) e.getCode()); + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT_MESSAGE, e.getMessage()); + } + // 保证无任何写产生 + assertNull(checkinsByEnrollLesson.get("300_1001")); + } + + @Test + public void testSignInLifecycle_Status1_Reported_Allowed() + { + // status=1 已报到学员:允许签到 + TrainEnrollment e1 = new TrainEnrollment(); + e1.setEnrollmentId(301L); + e1.setPlanId("PLAN_A"); + e1.setEnrollmentStatus(1); // 1: 已报到 + enrollments.put(301L, e1); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(301L); + dto.setLessonId(1001L); + + TrainStudentCheckin c = appService.signIn(dto, "admin"); + assertNotNull(c); + assertNotNull(c.getSignInTime()); + assertEquals(Integer.valueOf(1), c.getAttendanceStatus()); + } + + @Test + public void testSignInLifecycle_Status2_Leave_Allowed_PreviewDefault() + { + // status=2 请假学员:Preview Default 允许签到 (LEAVE_SIGNIN_POLICY: PREVIEW_DEFAULT_ALLOWED) + TrainEnrollment e2 = new TrainEnrollment(); + e2.setEnrollmentId(302L); + e2.setPlanId("PLAN_A"); + e2.setEnrollmentStatus(2); // 2: 请假 + enrollments.put(302L, e2); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(302L); + dto.setLessonId(1001L); + + TrainStudentCheckin c = appService.signIn(dto, "admin"); + assertNotNull(c); + assertNotNull(c.getSignInTime()); + assertEquals(Integer.valueOf(1), c.getAttendanceStatus()); + } + + @Test + public void testSignInLifecycle_Status3_Abandoned_Rejected() + { + // status=3 放弃学员:严禁直接签到 + TrainEnrollment e3 = new TrainEnrollment(); + e3.setEnrollmentId(303L); + e3.setPlanId("PLAN_A"); + e3.setEnrollmentStatus(3); // 3: 放弃 + enrollments.put(303L, e3); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(303L); + dto.setLessonId(1001L); + + try + { + appService.signIn(dto, "admin"); + fail("已放弃学员必须被拦截禁止签到"); + } + catch (ServiceException e) + { + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED, (int) e.getCode()); + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED_MESSAGE, e.getMessage()); + } + assertNull(checkinsByEnrollLesson.get("303_1001")); + } + + @Test + public void testSignInLifecycle_Status4_Completed_Rejected() + { + // status=4 已完成学员:严禁直接签到 + TrainEnrollment e4 = new TrainEnrollment(); + e4.setEnrollmentId(304L); + e4.setPlanId("PLAN_A"); + e4.setEnrollmentStatus(4); // 4: 已完成 + enrollments.put(304L, e4); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(304L); + dto.setLessonId(1001L); + + try + { + appService.signIn(dto, "admin"); + fail("已结训学员必须被拦截禁止签到"); + } + catch (ServiceException e) + { + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED, (int) e.getCode()); + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED_MESSAGE, e.getMessage()); + } + assertNull(checkinsByEnrollLesson.get("304_1001")); + } + + @Test + public void testSignInLifecycle_UnknownOrNullStatus_Rejected() + { + // 未知或 null 参训状态:fail closed 严禁签到 + TrainEnrollment eNull = new TrainEnrollment(); + eNull.setEnrollmentId(305L); + eNull.setPlanId("PLAN_A"); + eNull.setEnrollmentStatus(null); + enrollments.put(305L, eNull); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(305L); + dto.setLessonId(1001L); + + try + { + appService.signIn(dto, "admin"); + fail("null 状态学员必须被 fail closed 拦截"); + } + catch (ServiceException e) + { + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID, (int) e.getCode()); + } + + // 状态 99 + eNull.setEnrollmentStatus(99); + try + { + appService.signIn(dto, "admin"); + fail("非法状态 99 学员必须被拦截"); + } + catch (ServiceException e) + { + assertEquals(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID, (int) e.getCode()); + } + } + + @Test + public void testSignInLifecycle_Status4_WithExistingCheckin_IdempotentRetainsFact() + { + // 场景:历史已经合法签到的学员,后来结训归档变为 status=4。重复调用 signIn 保持幂等返回已有事实,不产生新写 + TrainEnrollment e4 = new TrainEnrollment(); + e4.setEnrollmentId(306L); + e4.setPlanId("PLAN_A"); + e4.setEnrollmentStatus(4); // 4: 已完成 + enrollments.put(306L, e4); + + TrainStudentCheckin historical = new TrainStudentCheckin(); + historical.setCheckinId(6001L); + historical.setEnrollmentId(306L); + historical.setLessonId(1001L); + Date historicalTime = new Date(System.currentTimeMillis() - 86400000L); + historical.setSignInTime(historicalTime); + historical.setAttendanceStatus(1); + checkinsById.put(6001L, historical); + checkinsByEnrollLesson.put("306_1001", historical); + + AttendanceSignInDTO dto = new AttendanceSignInDTO(); + dto.setEnrollmentId(306L); + dto.setLessonId(1001L); + + TrainStudentCheckin returned = appService.signIn(dto, "admin"); + assertNotNull(returned); + assertEquals(Long.valueOf(6001L), returned.getCheckinId()); + assertEquals(historicalTime, returned.getSignInTime()); + } + + @Test + public void testRosterLifecycleEligibility_Status0And4_CannotSignIn_Status1And2_CanSignIn() + { + // 测试名册对于 status 0, 1, 2, 4 的 canSignIn 权限对齐: + // 310 (status=0 待报到), 311 (status=1 已报到), 312 (status=2 请假), 314 (status=4 已完成) + enrollments.clear(); + checkinsById.clear(); + checkinsByEnrollLesson.clear(); + + TrainEnrollment e0 = new TrainEnrollment(); e0.setEnrollmentId(310L); e0.setPlanId("PLAN_A"); e0.setEnrollmentStatus(0); enrollments.put(310L, e0); + TrainEnrollment e1 = new TrainEnrollment(); e1.setEnrollmentId(311L); e1.setPlanId("PLAN_A"); e1.setEnrollmentStatus(1); enrollments.put(311L, e1); + TrainEnrollment e2 = new TrainEnrollment(); e2.setEnrollmentId(312L); e2.setPlanId("PLAN_A"); e2.setEnrollmentStatus(2); enrollments.put(312L, e2); + TrainEnrollment e4 = new TrainEnrollment(); e4.setEnrollmentId(314L); e4.setPlanId("PLAN_A"); e4.setEnrollmentStatus(4); enrollments.put(314L, e4); + + List roster = appService.getLessonRoster(1001L); + assertEquals(4, roster.size()); + + com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r0 = roster.stream().filter(r -> r.getEnrollmentId().equals(310L)).findFirst().orElse(null); + assertNotNull(r0); + assertFalse("status=0 待报到学员名册 canSignIn 必须为 false", Boolean.TRUE.equals(r0.getCanSignIn())); + + com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r1 = roster.stream().filter(r -> r.getEnrollmentId().equals(311L)).findFirst().orElse(null); + assertNotNull(r1); + assertTrue("status=1 已报到学员名册 canSignIn 必须为 true", Boolean.TRUE.equals(r1.getCanSignIn())); + + com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r2 = roster.stream().filter(r -> r.getEnrollmentId().equals(312L)).findFirst().orElse(null); + assertNotNull(r2); + assertTrue("status=2 请假学员名册 canSignIn 必须为 true (Preview Default)", Boolean.TRUE.equals(r2.getCanSignIn())); + + com.ruoyi.train.domain.vo.AttendanceLessonRosterVO r4 = roster.stream().filter(r -> r.getEnrollmentId().equals(314L)).findFirst().orElse(null); + assertNotNull(r4); + assertFalse("status=4 已结训学员名册 canSignIn 必须为 false", Boolean.TRUE.equals(r4.getCanSignIn())); + } } diff --git a/build_ui/tests/unit/attendanceSigninEntry.spec.mjs b/build_ui/tests/unit/attendanceSigninEntry.spec.mjs index 58d240dc9..017ecaede 100644 --- a/build_ui/tests/unit/attendanceSigninEntry.spec.mjs +++ b/build_ui/tests/unit/attendanceSigninEntry.spec.mjs @@ -1,29 +1,15 @@ import assert from 'node:assert'; +import { + formatAttendanceTag, + formatEnrollmentStatusTag, + isEnrollmentEligibleForSignIn, + computeRosterActionEligibility, + resolveSignInFeedback +} from '../../src/views/train/checkin/attendancePolicy.js'; -console.log('=== Testing Attendance Sign-in Entry & Roster Semantics ==='); +console.log('=== Testing Real Production Attendance Policy Helper ==='); -// 1. 测试考勤状态语义映射:未签到 (NO_CHECKIN) 必须严格与缺勤 (0) 区分 -function formatAttendanceTag(status) { - if (status === null || status === undefined) { - return { text: '未签到', type: 'info' }; - } - switch (status) { - case 0: - return { text: '缺勤', type: 'danger' }; - case 1: - return { text: '正常', type: 'success' }; - case 2: - return { text: '迟到', type: 'warning' }; - case 3: - return { text: '早退', type: 'warning' }; - case 4: - return { text: '请假', type: 'primary' }; - default: - return { text: '-', type: '' }; - } -} - -// A. 验证未打卡学员渲染为“未签到”,绝不等于“缺勤” +// 1. 考勤状态语义映射:未签到 (NO_CHECKIN) 必须严格与缺勤 (0) 区分 { const tagNull = formatAttendanceTag(null); assert.strictEqual(tagNull.text, '未签到'); @@ -31,160 +17,142 @@ function formatAttendanceTag(status) { const tagUndefined = formatAttendanceTag(undefined); assert.strictEqual(tagUndefined.text, '未签到'); + assert.strictEqual(tagUndefined.type, 'info'); const tagAbsent = formatAttendanceTag(0); assert.strictEqual(tagAbsent.text, '缺勤'); assert.strictEqual(tagAbsent.type, 'danger'); - // 红线:未签到和缺勤完全不同 + // 严格区分未签到与缺勤事实 assert.notStrictEqual(tagNull.text, tagAbsent.text); assert.notStrictEqual(tagNull.type, tagAbsent.type); - console.log('✔ PASS: NO_CHECKIN is strictly distinct from ABSENT (0)'); + console.log('✔ PASS: Production formatAttendanceTag distinguishes NO_CHECKIN from ABSENT (0)'); } -// 2. 按钮可见性与防重复规则 -function getRowActionButtons(row) { - return { - signIn: !!row.canSignIn, - signOut: !!row.canSignOut, - adjust: !!row.canAdjust - }; -} - -// A. 无 checkin 记录学员:签到按钮可见,签退不可见 +// 2. 学员参训状态展示标签 { - const uncheckRow = { - enrollmentId: 101, + assert.strictEqual(formatEnrollmentStatusTag(0).text, '待报到'); + assert.strictEqual(formatEnrollmentStatusTag(1).text, '已报到'); + assert.strictEqual(formatEnrollmentStatusTag(2).text, '请假'); + assert.strictEqual(formatEnrollmentStatusTag(3).text, '放弃'); + assert.strictEqual(formatEnrollmentStatusTag(4).text, '已完成'); + console.log('✔ PASS: Production formatEnrollmentStatusTag maps all lifecycle statuses correctly'); +} + +// 3. 参训生命周期首次签到资格规则断言 +{ + assert.strictEqual(isEnrollmentEligibleForSignIn(0), false, 'status=0 待报到严禁签到'); + assert.strictEqual(isEnrollmentEligibleForSignIn(1), true, 'status=1 已报到在训允许签到'); + assert.strictEqual(isEnrollmentEligibleForSignIn(2), true, 'status=2 请假学员当前 Preview Default 允许签到'); + assert.strictEqual(isEnrollmentEligibleForSignIn(3), false, 'status=3 放弃学员严禁签到'); + assert.strictEqual(isEnrollmentEligibleForSignIn(4), false, 'status=4 已完成学员严禁签到'); + assert.strictEqual(isEnrollmentEligibleForSignIn(null), false, 'null 状态 fail closed'); + assert.strictEqual(isEnrollmentEligibleForSignIn(undefined), false, 'undefined 状态 fail closed'); + assert.strictEqual(isEnrollmentEligibleForSignIn(99), false, '未知状态 fail closed'); + console.log('✔ PASS: Production isEnrollmentEligibleForSignIn strictly enforces lifecycle guard'); +} + +// 4. 生产环境操作按钮可用性 (computeRosterActionEligibility) 覆盖 +{ + // A. status=0 待报到 + 无打卡记录 -> 签到按钮不存在 + const row0 = { + enrollmentId: 100, + enrollmentStatus: 0, checkinId: null, attendanceStatus: null, signInTime: null, signOutTime: null, - canSignIn: true, - canSignOut: false, - canAdjust: false + canSignIn: false }; - const actions = getRowActionButtons(uncheckRow); - assert.strictEqual(actions.signIn, true, '无签到记录学员签到按钮必须可见'); - assert.strictEqual(actions.signOut, false, '未签到学员不能显示签退'); - assert.strictEqual(actions.adjust, false, '无考勤记录学员不能显示调整'); - console.log('✔ PASS: Unchecked student has visible signIn button, no duplicate path'); -} + const act0 = computeRosterActionEligibility(row0); + assert.strictEqual(act0.canSignIn, false, 'status=0 待报到学员签到按钮必须不存在'); + assert.strictEqual(act0.canSignOut, false); + assert.strictEqual(act0.canAdjust, false); -// B. 已有签到记录:签到按钮不可重复执行 -{ - const signedInRow = { + // B. status=1 已报到 + NO_CHECKIN -> 签到按钮存在 + const row1 = { enrollmentId: 101, - checkinId: 8001, + enrollmentStatus: 1, + checkinId: null, + attendanceStatus: null, + signInTime: null, + signOutTime: null, + canSignIn: true + }; + const act1 = computeRosterActionEligibility(row1); + assert.strictEqual(act1.canSignIn, true, 'status=1 已报到学员未签到时签到按钮必须存在'); + assert.strictEqual(act1.canSignOut, false); + assert.strictEqual(act1.canAdjust, false); + + // C. status=2 请假 + NO_CHECKIN -> 签到按钮存在 (Preview Default) + const row2 = { + enrollmentId: 102, + enrollmentStatus: 2, + checkinId: null, + attendanceStatus: null, + signInTime: null, + signOutTime: null, + canSignIn: true + }; + const act2 = computeRosterActionEligibility(row2); + assert.strictEqual(act2.canSignIn, true, 'status=2 请假学员未签到时签到按钮必须存在 (Preview Default)'); + assert.strictEqual(act2.canSignOut, false); + assert.strictEqual(act2.canAdjust, false); + + // D. status=4 已完成 + NO_CHECKIN -> 签到按钮不存在 + const row4 = { + enrollmentId: 104, + enrollmentStatus: 4, + checkinId: null, + attendanceStatus: null, + signInTime: null, + signOutTime: null, + canSignIn: false + }; + const act4 = computeRosterActionEligibility(row4); + assert.strictEqual(act4.canSignIn, false, 'status=4 已完成学员签到按钮必须不存在'); + assert.strictEqual(act4.canSignOut, false); + assert.strictEqual(act4.canAdjust, false); + + // E. 已有合法签到记录 (无论 status 为何) -> 签到按钮禁止重复存在 + const rowSignedIn = { + enrollmentId: 101, + enrollmentStatus: 1, + checkinId: 8888, attendanceStatus: 1, - signInTime: '2026-09-21 08:30:00', + signInTime: '2026-09-22 08:30:00', signOutTime: null, canSignIn: false, canSignOut: true, canAdjust: true }; - const actions = getRowActionButtons(signedInRow); - assert.strictEqual(actions.signIn, false, '已有合法签到记录学员不得显示签到按钮'); - assert.strictEqual(actions.signOut, true, '已签到学员允许签退'); - assert.strictEqual(actions.adjust, true, '已有打卡记录允许人工调整'); - console.log('✔ PASS: Signed-in student cannot duplicate sign-in'); + const actSigned = computeRosterActionEligibility(rowSignedIn); + assert.strictEqual(actSigned.canSignIn, false, '已签到学员绝对不能再次显示签到按钮'); + assert.strictEqual(actSigned.canSignOut, true, '已签到未签退学员激活签退'); + assert.strictEqual(actSigned.canAdjust, true, '已有打卡记录激活调整'); + console.log('✔ PASS: Production computeRosterActionEligibility covers all status button visibility'); } -// 3. 模拟 signIn 闭环与名册状态刷新 +// 5. 后端生命周期报错反馈解析真实性 (resolveSignInFeedback) { - // 初始名册 - let roster = [ - { - enrollmentId: 201, - studentName: '张三', - checkinId: null, - attendanceStatus: null, - signInTime: null, - signOutTime: null, - canSignIn: true, - canSignOut: false, - canAdjust: false - } - ]; + const errWaiting = { code: 40085, msg: '学员尚未完成开班报到,禁止录入课次考勤' }; + const fbWaiting = resolveSignInFeedback(errWaiting, '张三'); + assert.strictEqual(fbWaiting.isSuccess, false); + assert.strictEqual(fbWaiting.message.includes('学员尚未完成开班报到'), true); + assert.strictEqual(fbWaiting.message.includes('成功'), false); - assert.strictEqual(roster[0].canSignIn, true); - assert.strictEqual(roster[0].attendanceStatus, null); + const errCompleted = { code: 40087, msg: '学员参训档案已结训完成,禁止录入课次考勤' }; + const fbCompleted = resolveSignInFeedback(errCompleted, '李四'); + assert.strictEqual(fbCompleted.isSuccess, false); + assert.strictEqual(fbCompleted.message.includes('学员参训档案已结训完成'), true); + assert.strictEqual(fbCompleted.message.includes('成功'), false); - // 模拟执行 signIn 成功 - function mockSignIn(enrollmentId, lessonId) { - return Promise.resolve({ - checkinId: 5001, - enrollmentId: enrollmentId, - lessonId: lessonId, - attendanceStatus: 1, - signInTime: '2026-09-21 09:00:00', - signOutTime: null - }); - } + const errAbandoned = { code: 40086, msg: '学员参训状态已放弃,禁止录入课次考勤' }; + const fbAbandoned = resolveSignInFeedback(errAbandoned, '王五'); + assert.strictEqual(fbAbandoned.isSuccess, false); + assert.strictEqual(fbAbandoned.message.includes('学员参训状态已放弃'), true); - // 模拟刷新 roster - function mockRefreshRoster(updatedRecord) { - roster = roster.map(item => { - if (item.enrollmentId === updatedRecord.enrollmentId) { - return { - ...item, - checkinId: updatedRecord.checkinId, - attendanceStatus: updatedRecord.attendanceStatus, - signInTime: updatedRecord.signInTime, - signOutTime: updatedRecord.signOutTime, - canSignIn: false, // 已签到禁止再签到 - canSignOut: true, - canAdjust: true - }; - } - return item; - }); - } - - mockSignIn(201, 1001).then(res => { - mockRefreshRoster(res); - assert.strictEqual(roster[0].checkinId, 5001); - assert.strictEqual(roster[0].attendanceStatus, 1); - assert.strictEqual(roster[0].canSignIn, false, '刷新后签到按钮必须消失'); - assert.strictEqual(roster[0].canSignOut, true, '刷新后签退按钮必须激活'); - assert.strictEqual(roster[0].canAdjust, true, '刷新后调整按钮必须激活'); - console.log('✔ PASS: signIn success refreshes roster to current attendance fact'); - }); + console.log('✔ PASS: Production resolveSignInFeedback faithfully reports errors without false success'); } -// 4. 后端错误真实展示,不得出现假成功 -{ - function handleSignInError(err) { - // 页面逻辑: - let displayedMsg = ''; - let isSuccess = false; - if (err && (err.msg || err.message)) { - displayedMsg = err.msg || err.message; - } else { - displayedMsg = '签到操作失败'; - } - return { displayedMsg, isSuccess }; - } - - const backendError = { code: 400, msg: '课次未关联有效的培训课程计划' }; - const feedback = handleSignInError(backendError); - - assert.strictEqual(feedback.isSuccess, false); - assert.strictEqual(feedback.displayedMsg, '课次未关联有效的培训课程计划'); - assert.strictEqual(feedback.displayedMsg.includes('成功'), false); - console.log('✔ PASS: Backend error is truthfully reported without false success'); -} - -// 5. 权限可达性检查:train_admin 具备 checkin 权限与 list 权限 -{ - const trainAdminPerms = ['train:attendance:list', 'train:attendance:checkin', 'train:attendance:edit', 'train:attendance:export']; - const canAccessList = trainAdminPerms.includes('train:attendance:list'); - const canPerformSignIn = trainAdminPerms.includes('train:attendance:checkin'); - const canPerformAdjust = trainAdminPerms.includes('train:attendance:edit'); - - assert.strictEqual(canAccessList, true, 'train_admin 必须有查看考勤名册权限'); - assert.strictEqual(canPerformSignIn, true, 'train_admin 必须有课次签到权限'); - assert.strictEqual(canPerformAdjust, true, 'train_admin 必须有课次调整权限'); - console.log('✔ PASS: train_admin has full permission reachability'); -} - -console.log('ALL FRONTEND ATTENDANCE SIGN-IN ENTRY TESTS PASSED!'); +console.log('ALL PRODUCTION ATTENDANCE POLICY TESTS PASSED!');