import test from 'node:test';import assert from 'node:assert/strict'; import {mkdtemp,rm} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join} from 'node:path'; import {readFileSync} from 'node:fs'; import vm from 'node:vm'; import {runDirectSync} from '../run-nightly-direct-sync.mjs'; test('旧费用页完整且逐字段匹配的正金额记录才允许补进队列',async()=>{ const control=readFileSync(new URL('../cloud/legacy-direct-control.js',import.meta.url),'utf8'); const body=control.slice(control.indexOf('async function directReviewFeeProof('),control.indexOf('async function directControl(')); const rows=Array.from({length:5},(_,index)=>({ GeneralID:index+1,plid:index===4?31:30,yhid:20,xxjlid:90,orderId:80,kcid:70,fxzt:'1',wcsj:'2026-10-13 10:00', kywrq:'2026-10-13',kywsj:'2026-10-13 09:00',reviewFeeVerified:true, })); const warnings=[]; const run=vm.runInNewContext(body+';directReviewFeeProof',{ Psql:{query:async()=>rows.map(row=>({generalId:row.GeneralID,coachId:row.plid}))}, console:{warn:message=>warnings.push(message)}, directOld:async(action,{uid,cpage})=>{ if(action==='content_list')return {items:[],page:{itemCount:0,pageCount:0}}; assert.equal(action,'e_get_21list'); if(uid===31)return {items:[{...rows[4],money:1}],page:{pageCount:1,itemCount:2}}; return cpage===1?{items:[{...rows[0],money:1},{...rows[1],money:0},{...rows[2],money:1}],page:{pageCount:2,itemCount:5}}: {items:[{...rows[2],money:1},{...rows[3],money:1,wcsj:'2026-10-13 10:01'}],page:{pageCount:2,itemCount:5}}; }, }); const result=await run('c',rows); assert.deepEqual(Array.from(result,row=>row.reviewFeeVerified),[true,false,false,false,false]); assert.ok(rows.every(row=>row.reviewFeeVerified)); assert.equal(warnings.length,1); }); test('所有课次同步后分页核定工资,待核定原因进入回执,写入重试复用同一批次',async()=>{ const root=await mkdtemp(join(tmpdir(),'review-sync-runner-')),original=globalThis.fetch,order=[],receipts=new Map();let simulatedLostResponse=false,applied=0,final; try{ globalThis.fetch=async(url,options)=>{const p=JSON.parse(options.body).params;order.push(p.action==='source'?p.modelId:p.action);let data; if(p.action==='status')data={source:'old-api/WXAPP',bridgeUsed:false,reviewSyncVersion:'source-owned-v1'}; else if(p.action==='claim')data={runId:'review-sync-test-run',businessDate:'2026-11-02'}; else if(p.action==='supplement')data={accounts:{missing:0},nodes:{missing:0}}; else if(p.action==='source')data={items:[],page:{itemCount:0,pageCount:1}}; else if(p.action==='evidence')data={sourceChecksum:'d41d8cd98f00b204e9800998ecf8427e',targetChecksum:'d41d8cd98f00b204e9800998ecf8427e',reconciled:true}; else if(p.action==='review-income'){ if(!receipts.has(p.batchKey)){applied++;receipts.set(p.batchKey,p.afterTaskId?{processed:0,earned:0,excluded:0,amount:0,pending:1,errors:[{taskId:'z',reason:'来源课次时长仍不明确'}],hasMore:false,afterTaskId:'z'}:{processed:1,earned:1,excluded:0,amount:.5,pending:1,errors:[],hasMore:true,afterTaskId:'a'});} data=receipts.get(p.batchKey);if(!simulatedLostResponse){simulatedLostResponse=true;throw Error('response lost after commit');} } else if(p.action==='finance-members'||p.action==='read')data=[]; else if(p.action==='finish'){final=p.summary;data=p.summary;} else throw Error('Unexpected action '+p.action); return {ok:true,json:async()=>({success:true,data})};}; const result=await runDirectSync({XIAOSHU_SYNC_REPORT_DIR:root,XIAOSHU_SYNC_OPERATOR_TOKEN:'test'}); assert.equal(result.status,'completed-with-preserved-conflicts',result.reason);assert.equal(applied,2);assert.equal(receipts.size,2); assert.equal(final.reviewIncome.earned,1);assert.equal(final.reviewIncome.amount,.5);assert.equal(final.reviewIncome.pending,1);assert.equal(final.conflicts,1);assert.match(final.reviewIncome.errors[0].reason,/时长/); assert(order.lastIndexOf(59){ const control=readFileSync(new URL('../cloud/legacy-direct-control.js',import.meta.url),'utf8'); const body=control.slice(control.indexOf('async function directReviewFeeProof('),control.indexOf('async function directControl(')); const rows=[80,81,82,83,84].map((orderId,i)=>({GeneralID:i+1,plid:30,yhid:20,xxjlid:90+i,orderId,kcid:70,fxzt:'1',wcsj:'2026-10-13 10:00',kywrq:'2026-10-13',kywsj:'2026-10-13 09:00'})); const page=items=>({items,page:{itemCount:items.length,pageCount:1}}); const run=vm.runInNewContext(body+';directReviewFeeProof',{ Psql:{query:async()=>rows.map(row=>({generalId:row.GeneralID,coachId:30}))}, directOld:async(action,args)=>{ if(action==='e_get_21list')return page(rows.map((row,i)=>({...row,money:['0','1','0','',null][i]}))); const ids=args.ids.split(',').map(Number); if(args.modelId===54)return page(rows.map((row,i)=>({GeneralID:row.orderId,szyh:20,dslx:i===2?'2':'3',Title:'学员(体验课)'})).filter(row=>ids.includes(row.GeneralID))); if(args.modelId===56)return page(rows.map(row=>({GeneralID:row.xxjlid,dsid:row.orderId})).filter(row=>ids.includes(row.GeneralID))); throw Error('unexpected source'); }, }); const result=await run('c',rows); assert.equal(result[0].reviewTrialProof?.fee.money,'0'); assert.equal(result[0].reviewTrialProof?.appointment.GeneralID,80); assert.equal(result[0].reviewTrialProof?.study.GeneralID,90); assert.deepEqual(Array.from(result,row=>row.reviewFeeVerified),[false,true,false,false,false]); assert.deepEqual(Array.from(result,row=>Boolean(row.reviewTrialProof)),[true,false,false,false,false]); });