'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); /** @param {string} source 源码 @param {string} name 方法名 @returns {string} 可执行方法。 */ function extract(source, name) { const match = new RegExp('export (?:async )?function ' + name + '\\([\\s\\S]*?\\n}').exec(source); assert.ok(match, '缺少方法 ' + name); return match[0].replace('export ', ''); } /** @param {string} file 源文件 @returns {object} 隔离运行态。 */ function library(file) { const source = fs.readFileSync(file, 'utf8'); const names = ['getRepairMonthText', 'getDayApprovalLock', 'resolveHourValue', 'getOvertimeChangeAdjustment', 'getOvertimeDisplayTotals', 'resolveOvertimeRecord', 'buildOvertimeBaseline', 'applyDefaultWorkdayHours', 'processTimecardRecords', 'getAdjustmentSummary', 'collectApprovalData', 'collectOtherApprovalData', 'buildApprovalGroupKey', 'isRepairDayLocked', 'isRepairDayAllowed', 'loadTimecardData', 'prepareTimecardRecords', 'updateCellValue', 'buildTimecardRepairRequest']; const scope = {}; vm.createContext(scope); vm.runInContext(source.slice(0, source.indexOf('export function didMount')) + names.map(name => extract(source, name)).join('\n'), scope); return scope; } /** @param {object} lib 方法与字段 @param {string} mode 场景 @returns {object} 页面实例。 */ function instance(lib, mode = 'NORMAL') { const env = { ...lib, state: { selectedYear: 2026, selectedMonth: 8, userId: 'user', isExternalStaff: true, repairMode: mode, userInfo: {staffType: '外部'}, reentryAllowedDays: [] }, isBeforeEmploymentDate: () => false, getTexts: () => ({ repairContextIncomplete: 'incomplete', staffTypeCheckFailed: 'staff mismatch' }) }; env.setState = update => Object.assign(env.state, update); return env; } /** @param {object} lib 字段映射 @param {number} day 日期 @param {number} hours 工时 @param {boolean} repair 补填 @param {boolean} approved 已审批 @returns {object} 测试实例。 */ function record(lib, day, hours, repair = false, approved = true) { const main = lib.TIME_FIELD_IDS; const row = lib.BILLABLE_SUB; const data = {}; data[main.dayText] = '202608' + String(day).padStart(2, '0'); data[main.dailyDraftTotal] = hours; data[main.dailySubmittedTotal] = approved ? hours : 0; data[main.dailyApprovedTotal] = approved ? hours : 0; data[repair ? lib.REPAIR_APPROVAL_STATUS_FIELD : main.approvalStatus] = approved ? '已审批' : '暂存'; data[row.table] = [{ [row.projectCode]: 'P', [row.draft]: hours, [row.submitted]: approved ? hours : 0, [row.approved]: approved ? hours : 0 }]; if (repair) data[lib.REPAIR_FILL_TYPE_FIELD] = '补填'; return { formInstId: (repair ? 'repair-' : 'original-') + day, formData: data }; } /** @param {object} env 页面 @param {Array} original 正式记录 @param {Array} repair 补填记录 @returns {void} 完整加载处理链。 */ function process(env, original, repair) { env.processTimecardRecords(env.prepareTimecardRecords(original, repair), 2026, 8, {}, [{code: 'P', name: 'Project', managerId: 'manager'}], false, false); } /** @returns {Promise} 验证真实问题形态与重录隔离。 */ async function main() { for (const file of ['timecard-repair.oyd.jsx', 'timecard-repair-lifecycle.oyd.jsx']) { const lib = library(path.resolve(__dirname, '../src', file)); const days = [3,4,5,6,7,8,10,11,12,13,14,17,18,19,20,21,22,24,25,26,27,28,29]; const original = days.map(day => record(lib, day, day === 8 ? 4 : [6,7,11,12,13,18].includes(day) ? 10 : 8)); const drafts = days.filter(day => new Date(2026,7,day).getDay() % 6 !== 0).map(day => record(lib, day, 0, true, false)); const env = instance(lib); process(env, original, drafts); const summary = env.getAdjustmentSummary(); assert.equal(summary.baseline, 192); assert.equal(summary.delta, 8); assert.equal(summary.total, 200); for (const day of days) { assert.equal(env.state.cellStatusMap['billable_0_' + day], 'approved'); assert.equal(env.state.cellStatusMap['nonBillable_0_' + day], 'approved'); assert.equal(env.state.cellStatusMap['other_0_' + day], 'approved'); const before = env.state.billableData[0][day]; env.updateCellValue('billable', 0, day, 99); assert.equal(env.state.billableData[0][day], before); } let groups = env.collectApprovalData(); let rows = Object.values(groups).flatMap(group => group.billableRows); assert.deepEqual(Array.from(rows, row => row.dateStr), ['20260831']); const request = await env.buildTimecardRepairRequest(); assert.equal(request.repairMode, 'NORMAL'); process(env, original, drafts.concat([record(lib,31,8,true)])); assert.equal(env.isRepairDayLocked(31), true); assert.equal(Object.keys(env.collectApprovalData()).length, 0, '31日已审批补填不可再次提交'); const reentry = instance(lib, 'REENTRY'); reentry.state.reentryAllowedDays = ['20260803']; process(reentry, original, []); assert.equal(reentry.getAdjustmentSummary().baseline, 0, '重录不回带已冲销原审批基线'); assert.equal(reentry.state.billableData[0][3], 8); assert.equal(Object.keys(reentry.state.billableData[0]).length, 1, '范围外日期不得默认8h'); assert.equal(reentry.isRepairDayLocked(4), true); rows = Object.values(reentry.collectApprovalData()).flatMap(group => group.billableRows); assert.deepEqual(Array.from(rows, row => row.dateStr), ['20260803']); await assert.rejects(reentry.loadTimecardData('user',2026,8), /incomplete/, '重录不能原生回退绕过资格'); await assert.rejects(env.loadTimecardData('user',2026,8, {staffType:'外部',repairMode:'REENTRY',summaryReadMode:'repair-only',originalRecords:[],repairRecords:[]}), /incomplete/); const change = record(lib,4,-8,true); change.formData[lib.REPAIR_FILL_TYPE_FIELD] = '变更'; assert.equal(env.prepareTimecardRecords([], [change]).length,0); } console.log('PASS 双补填入口:192h基线、零草稿覆盖保护、只提交31日8h、已审批补填锁定、重录范围及模式隔离'); } main().catch(error => { console.error(error); process.exitCode = 1; });