| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- 'use strict';
- const assert = require('node:assert/strict');
- const fs = require('fs');
- const path = require('path');
- const vm = require('vm');
- const mainPath = process.argv[2] || path.resolve(__dirname, '../src/timecard-main.actions.js');
- const repairPath = path.resolve(__dirname, '../src/timecard-repair.oyd.jsx');
- const evidence = process.argv[3] || '/Users/malk/Desktop/Tech/1. malk/宜搭/阿科德斯/TimeCard/project/.cache/openyida/overtime-evaluation-20261023/样本.json';
- /** @param {string} source @param {string} name @returns {string} */
- function extract(source, name) {
- const found = new RegExp('export function ' + name + '\\([^]*?\\n}').exec(source);
- assert.ok(found, '缺少 ' + name);
- return found[0].replace('export ', '');
- }
- /** @param {string} file @returns {object} */
- function methods(file) {
- const source = fs.readFileSync(file, 'utf8');
- const names = ['resolveHourValue', 'getDayTotal', 'isWeekend', 'isDayOff',
- 'getOvertimeDisplayTotals', 'resolveOvertimeRecord', 'buildOvertimeBaseline',
- 'getOvertimeDayTotal', 'validateMonthlyOvertime', 'processTimecardRecords'];
- if (source.includes('export function getOvertimeChangeAdjustment(')) names.push('getOvertimeChangeAdjustment');
- const fields = {draft:'draft',submitted:'submitted',approved:'approved',change:'change',projectCode:'key'};
- const context = {VALIDATION:{maxHoursPerDay:8,maxOvertimePerMonth:36},
- BILLABLE_SUB:{...fields,table:'billable'}, NON_BILLABLE_SUB:{...fields,table:'nonBillable'},
- OTHER_SUB:{...fields,table:'other',projectCode:undefined,activity:'key'},TIME_FIELD_IDS:{dayText:'day'}};
- const snapshot = JSON.parse(fs.readFileSync(evidence,'utf8'));
- context.OTHER_CATEGORIES = [...new Set(snapshot.people.flatMap(p=>p.originalOctober.flatMap(r=>
- r.lines.filter(x=>x.section==='other').map(x=>x.key))))].map(name=>({name}));
- vm.createContext(context);
- vm.runInContext(names.map(n=>extract(source,n)).join('\n'), context);
- return context;
- }
- /** @param {object} row @param {string} source @returns {object} */
- function adapt(row, source) {
- const data = {day:row.day,radioField_rtc81we6k:row.fillType};
- for (const section of ['billable','nonBillable','other']) data[section] = row.lines.filter(x=>x.section===section);
- return {formData:data,_timecardRecordSource:source};
- }
- /** @param {object} lib @param {Array<object>} originals @param {Array<object>} repairs @param {string} month @returns {object} */
- function setup(lib, originals, repairs, month='202610') {
- const rows = originals.map(x=>adapt(x,'original'));
- const state = {selectedYear:+month.slice(0,4),selectedMonth:+month.slice(4),isExternalStaff:false,
- billableData:{},nonBillableData:{},otherData:{},holidayMap:{}};
- for (const r of originals) for (const line of r.lines) {
- const key = line.section + 'Data';
- state[key][line.key] ||= {};
- state[key][line.key][+r.day.slice(-2)] = lib.resolveHourValue(+line.approved||0,+line.submitted||0,+line.draft||0,+line.change||0,true).value;
- }
- const env = {...lib,state,isBeforeEmploymentDate:()=>false,getTexts:()=>({overtimeExceeded:n=>String(n)}),
- utils:{toast:()=>{}},showTimecardError:()=>{}};
- state.overtimeDisplayDayTotals = env.getOvertimeDisplayTotals(state.billableData,state.nonBillableData,state.otherData);
- state.overtimeBaseDayTotals = env.buildOvertimeBaseline(rows.concat(repairs.map(x=>adapt(x,'repair'))),
- state.selectedYear,state.selectedMonth,state.overtimeDisplayDayTotals,false);
- return env;
- }
- /** @returns {Promise<void>} */
- async function main() {
- const snapshot = JSON.parse(fs.readFileSync(evidence,'utf8'));
- const outcomes = [];
- for (const file of [mainPath,repairPath]) {
- const lib = methods(file);
- const values = [];
- for (const p of snapshot.people) {
- const env = setup(lib,p.originalOctober,p.repairAll.filter(r=>r.day.startsWith('202610')));
- snapshot.holidays.filter(x=>x.day.startsWith('202610')).forEach(x=>{env.state.holidayMap[+x.day.slice(-2)]=x.type;});
- let total = 0;
- for(let d=1;d<=31;d++) total += env.isDayOff(2026,10,d) ? env.getOvertimeDayTotal(d) : Math.max(0,env.getOvertimeDayTotal(d)-8);
- values.push(total);
- assert.equal(env.validateMonthlyOvertime(),p.name!=='周璐');
- const integrated = {...lib,state:{selectedYear:2026,selectedMonth:10},
- getDayApprovalLock:()=>'',applyDefaultWorkdayHours:()=>({}),
- isBeforeEmploymentDate:()=>false,getTexts:()=>({overtimeExceeded:n=>String(n)}),
- utils:{toast:()=>{}},showTimecardError:()=>{}};
- integrated.setState = update=>Object.assign(integrated.state,update);
- const records = p.originalOctober.map(r=>adapt(r,'original'));
- const projects = [...new Set(p.originalOctober.flatMap(r=>r.lines.filter(x=>x.section!=='other').map(x=>x.key)))].map(code=>({code,name:code}));
- integrated.processTimecardRecords(records,2026,10,env.state.holidayMap,projects,false,false,false);
- assert.equal(integrated.validateMonthlyOvertime(),p.name!=='周璐','完整processTimecardRecords链路回放');
- if(p.name==='王赏') {
- env.state.billableData.extra={11:4};
- assert.equal(env.getOvertimeDayTotal(11),4,'未保存周日4h必须参与校验');
- }
- }
- assert.deepEqual(values,[40,0,0]);
- const env = setup(lib,[],[]);
- const row = {day:'20261022',fillType:'',lines:[{section:'billable',key:'P',draft:8,approved:0,submitted:0,change:168}]};
- const restored = setup(lib,[row],[]);
- assert.equal(restored.getOvertimeDayTotal(22),8);
- restored.state.billableData.P[22]=172;
- assert.equal(restored.getOvertimeDayTotal(22),12,'新增输入必须在还原基础上计算');
- const repair = structuredClone(row);
- repair.fillType='补填';repair.lines[0].approved=8;repair.lines[0].change=0;
- assert.equal(env.buildOvertimeBaseline([adapt(row,'original'),adapt(repair,'repair')],2026,10,{},false)[22],8);
- repair.lines[0].key='另一项目';
- assert.equal(env.buildOvertimeBaseline([adapt(row,'original'),adapt(repair,'repair')],2026,10,{},false)[22],8);
- repair.fillType='变更';repair.lines[0].approved=168;
- assert.equal(Object.keys(env.buildOvertimeBaseline([adapt(repair,'repair')],2026,10,{},false)).length,0);
- row.lines[0].change=-168;
- assert.equal(setup(lib,[row],[]).getOvertimeDayTotal(22),8,'负变更同样排除');
- assert.throws(()=>env.buildOvertimeBaseline([adapt(row,'original'),adapt(row,'original')],2026,10,{},false),/重复/);
- row.lines[0].submitted=8;repair.fillType='补填';repair.lines[0].approved=8;
- assert.throws(()=>env.buildOvertimeBaseline([adapt(row,'original'),adapt(repair,'repair')],2026,10,{},false),/均已提交/);
- for(const hours of [36,36.5]) {
- const boundary = setup(lib,[],[]);
- boundary.state.billableData.P={11:hours/2,18:hours/2};
- assert.equal(boundary.validateMonthlyOvertime(),hours===36);
- }
- outcomes.push({file:path.basename(file),hours:values});
- }
- const source = fs.readFileSync(repairPath,'utf8');
- const scope = {FORM_IDS:{timeSummary:'original',repairSummary:'repair'},TIME_FIELD_IDS:{submitter:'user',monthText:'month'},
- REPAIR_FILL_TYPE_FIELD:'radioField_rtc81we6k'};
- vm.createContext(scope);
- vm.runInContext(['loadTimecardData','prepareTimecardRecords'].map(n=>extract(source,n)).join('\n'),scope);
- const calls=[];
- const env={...scope,state:{isExternalStaff:true},getRepairMonthText:()=> '202609',
- queryAllTimecardRecords:async (...args)=>{calls.push(args[4]);return[];}};
- await env.loadTimecardData('external',2026,9);
- assert.deepEqual(calls,['repair'],'外部原生回退不得查询原表');
- calls.length=0;
- await env.loadTimecardData('external',2026,9,{staffType:'外部',originalRecords:[{formData:{}}],repairRecords:[]});
- assert.equal(calls.length,0);
- env.state.isExternalStaff=false;
- await env.loadTimecardData('internal',2026,9);
- assert.deepEqual(calls,['original','repair']);
- const change={formData:{radioField_rtc81we6k:'变更'}};
- assert.equal(env.prepareTimecardRecords([], [change]).length,0);
- console.log(JSON.stringify({status:'passed',outcomes,externalFallback:'repair-only',internalFallback:'two-tables'}));
- }
- main().catch(error=>{console.error(error);process.exitCode=1;});
|