import assert from 'node:assert/strict' import test from 'node:test' import { spawnSync } from 'node:child_process' import { readFile } from 'node:fs/promises' import { options, plan, validateFixture, validatePage, assertItem, verify, execute } from './appointment-work-test-env.mjs' import { guardFleetWrites } from './fleet-test-env-acceptance.mjs' const WORK='/api/platform/v1/last-leg/appointment-work', API='/api/platform/v1/last-leg' const now=Date.parse('2026-10-15T08:00:00Z') function sample() { return {schemaVersion:1,environment:'TEST',runId:'aw-test',producerRunId:'producer-test',producedAt:'2026-10-15T07:00:00Z',producerReport:'reports/producer/report.json',producerSha256:'a'.repeat(64), cases:[{name:'private',containerBookingId:'1',containerIds:['1'],type:'DELIVERY',flow:'TRANSFER',requirementKey:'DISPATCH:20',batchKey:'DISPATCH:20',batchNo:'D-20',expected:{executionStatus:'PENDING',planStatus:'UNPLANNED',dateBasis:'NONE',calendarDate:null}}], batches:[{case:'private',kind:'CUSTOMER',dispatchNo:'D-20',packageCount:4,pallets:[{planPalletId:'10',packageCount:4}]}],sharedTrips:[],retired:[],workflow:{case:'private',ownerId:'90',followupDate:'2026-10-16',noBooking:true}} } function workItem(c=sample().cases[0]) { return {workItemKey:'WORK-1',version:1,type:c.type,flow:c.flow,requirementKey:c.requirementKey,batchKey:c.batchKey,batchNo:c.batchNo,businessNo:'D-20', containers:c.containerIds.map(id=>({containerBookingId:id,containerNo:`CNT-${id}`,bookingNo:null})),...c.expected,actual:c.expected.actual||null, allowedActions:[{code:'SAVE_FOLLOWUP'},{code:'SAVE_REQUIREMENT'}],blockReasons:[],reviewRequired:false,owner:null,followupDate:null} } function response(data,http=200,code='200') {return {http,body:{code,data},requestId:'REQ',traceId:'TRACE'}} function harness(fixture=sample()) { const requests=[], detail={item:workItem(fixture.cases[0]),history:[],references:[],relatedItems:[],requirement:{bookingRequired:true},review:null,returnHandoff:null},replays=new Map() const clone=v=>structuredClone(v) async function request(actor,method,path,body) { requests.push({actor,method,path,body:clone(body)}) if(actor==='anonymous')return response(null,401,'A0401') if(actor==='merchant'||actor==='reader'&&method==='POST')return response(null,403,'A0403') if(path===`${WORK}/summary`)return response({all:1,followToday:0,overdue:0,undated:1,review:0}) if(path.startsWith(`${WORK}/items?`))return response({records:[clone(detail.item)],page:1,size:100,total:1,pages:1}) if(path===`${WORK}/items/WORK-1`&&method==='GET')return response(clone(detail)) if(path===`${API}/dispatches/D-20`)return response({id:'20',dispatchNo:'D-20',packageCount:4,items:[{planPalletId:'10',packageCount:4}]}) if(path===`${WORK}/items/WORK-1/commands`) { if(replays.has(body.idempotencyKey)){const old=replays.get(body.idempotencyKey);return JSON.stringify(old.body)===JSON.stringify(body)?response(clone(old.result)):response(null,409,'A0409')} if(body.expectedVersion!==detail.item.version)return response(null,409,'A0409') if(body.command==='SAVE_FOLLOWUP'){detail.item.owner={id:body.payload.ownerId,name:'验收操作员'};detail.item.followupDate=body.payload.followupDate} else if(body.command==='SAVE_REQUIREMENT'){detail.item.planStatus='NOT_REQUIRED';detail.requirement.bookingRequired=false} else throw Error('Unexpected command') detail.item.version++;detail.history.push({action:body.command,summary:'fixture'}) const result={item:clone(detail.item),affectedWorkItemKeys:[]};replays.set(body.idempotencyKey,{body:clone(body),result:clone(result)});return response(result) } throw Error(`Unexpected request ${method} ${path}`) } return {request,requests,detail} } test('default and explicit plan perform no network and no fixture/file reads',async()=>{ const original=globalThis.fetch;globalThis.fetch=()=>{throw Error('NETWORK_FORBIDDEN')} try {assert.equal((await execute([],[],{})).execute,false);assert.deepEqual(await execute(['--plan'],[],{}),plan())}finally{globalThis.fetch=original} const child=spawnSync(process.execPath,['scripts/appointment-work-test-env.mjs','--plan'],{encoding:'utf8'}) assert.equal(child.status,0,child.stderr);assert.equal(JSON.parse(child.stdout).execute,false) }) test('execution rejects ambiguous environments, duplicate args and wrong private profile',()=>{ const args=['--execute','--environment=TEST','--run-id=aw-test','--mode=workflow','--fixture=reports/new/fixture.json'] const env={CHONGCHONG_ENV:'test',CHONGCHONG_ADMIN_API_BASE_URL:'https://example.invalid/api',CHONGCHONG_ADMIN_USERNAME:'admin',CHONGCHONG_ADMIN_PASSWORD:'secret',CHONGCHONG_MERCHANT_USERNAME:'customer',CHONGCHONG_MERCHANT_PASSWORD:'secret',APPOINTMENT_WORK_READER_USERNAME:'reader',APPOINTMENT_WORK_READER_PASSWORD:'secret'} const flags=['--env-file=C:/Users/test/.codex/env/chongchong-global/test-acceptance.env'] assert.equal(options(args,flags,env).mode,'workflow') assert.throws(()=>options([...args,'--mode=read'],flags,env)) assert.throws(()=>options(args,flags,{...env,CHONGCHONG_ENV:'prod'})) assert.throws(()=>options(args,['--env-file=other.env'],env)) assert.throws(()=>options(args,flags,{...env,CHONGCHONG_ADMIN_API_BASE_URL:'https://example.invalid/overseas-api'})) assert.throws(()=>options(args,flags,{...env,APPOINTMENT_WORK_READER_PASSWORD:''})) }) test('fixture requires current producer evidence and explicit nonempty expectations',()=>{ assert.equal(validateFixture(sample(),{runId:'aw-test',mode:'workflow'},now).cases.length,1) for(const patch of [{cases:[]},{runId:'old'},{producedAt:'2026-10-13T00:00:00Z'},{producerSha256:''},{batches:[{case:'private',kind:'CUSTOMER',dispatchNo:'D-20',packageCount:4,pallets:[]}]}])assert.throws(()=>validateFixture({...sample(),...patch},{runId:'aw-test',mode:'workflow'},now)) const incomplete=sample();incomplete.cases[0].expected.executionStatus='COMPLETED';assert.throws(()=>validateFixture(incomplete,{runId:'aw-test',mode:'read'},now)) }) test('pagination and actual evidence cannot silently truncate or invent completion',()=>{ assert.throws(()=>validatePage({records:[],page:1,size:100,total:1,pages:1},1)) const c=sample().cases[0], item=workItem(c);assertItem(item,c) assert.throws(()=>assertItem({...item,actual:{localOccurredAt:'2026-10-15T11:00:00',occurredAt:null,timezone:null}},c)) assert.throws(()=>assertItem({...item,containers:[{containerBookingId:'foreign'}]},c)) }) test('workflow runs denial/replay/stale/no-booking checks without original-business writes',async()=>{ const h=harness(),result=await verify(h,sample(),{mode:'workflow',runId:'aw-test'}) assert.ok(result.checks.includes('no booking does not mean executed')) assert.ok(result.checks.includes('stale version rejected')) assert.equal(h.detail.item.executionStatus,'PENDING');assert.equal(h.detail.item.actual,null) assert.equal(h.detail.history.length,2) assert.ok(h.requests.filter(r=>r.method!=='GET').every(r=>r.method==='POST'&&r.path===`${WORK}/items/WORK-1/commands`)) assert.ok(!h.requests.some(r=>r.path.includes('/DISPATCH%3A20/dispatch-candidates'))) }) test('read mode is GET-only and rejects private dispatch identity or quantity drift',async()=>{ const h=harness();await verify(h,sample(),{mode:'read',runId:'aw-test'});assert.ok(h.requests.every(r=>r.method==='GET')) for(const wrong of [{id:'21',dispatchNo:'D-20',packageCount:4,items:[{planPalletId:'10',packageCount:4}]},{id:'20',dispatchNo:'D-20',packageCount:4,items:[{planPalletId:'10',packageCount:3}]}]){ const h=harness();await assert.rejects(()=>verify({request:async(...args)=>args[2]===`${API}/dispatches/D-20`?response(wrong):h.request(...args)},sample(),{mode:'read',runId:'aw-test'})) } }) test('denied writes must leave history and versions unchanged',async()=>{ const h=harness();await assert.rejects(()=>verify({request:async(...args)=>{const r=await h.request(...args);if(args[0]==='reader'&&args[1]==='POST')h.detail.item.version++;return r}},sample(),{mode:'workflow',runId:'aw-test'}),/Denied write/) }) test('FBA batch source allocation and candidate quantity are independently checked',async()=>{ const f=sample(),c=f.cases[0];Object.assign(c,{name:'fba',batchKey:'FBA_APPOINTMENT:20',requirementKey:'FBA_APPOINTMENT:20',batchNo:'AP-20'}) const expected={customerId:'C1',sourceKey:'ORDER:1',destinationKey:'FBA:ONT8',packageCount:4} f.batches=[{case:'fba',kind:'FBA',appointmentNo:'AP-20',sources:[expected],minimumReadyCandidates:1}] const h=harness(f) let count=4 const request=async(...args)=>{ if(args[2]===`${API}/appointment-planning/batches/FBA_APPOINTMENT%3A20`)return response({appointment:{batchKey:c.batchKey,appointmentNo:'AP-20',version:1,items:[expected]}}) if(args[2].endsWith('/dispatch-candidates'))return response({batchKey:c.batchKey,version:1,blockReasons:[],candidates:[{planPalletId:'10',customerId:'C1',packageCount:4,blockReasons:[],allocations:[{...expected,packageCount:count,batchKey:c.batchKey,planPalletId:'10'}]}]}) return h.request(...args) } await verify({request},f,{mode:'read',runId:'aw-test'});count=3;await assert.rejects(()=>verify({request},f,{mode:'read',runId:'aw-test'}),/quantity conservation/) }) test('shared trip checks original dispatch identities, quantities and current containers',async()=>{ const f=sample(),c=f.cases[0];Object.assign(c,{name:'trip',type:'TRANSPORT',containerIds:['1','2'],batchKey:null,batchNo:null,requirementKey:null});f.batches=[] f.sharedTrips=[{case:'trip',tripNo:'TR-1',tripId:'100',containerIds:['1','2'],dispatches:[{dispatchNo:'D-20',customerId:'C1',packageCount:4}]}] const h=harness(f);h.detail.item.businessNo='TR-1';h.detail.references=[{kind:'FLEET_TRIP',key:'100',businessNo:'TR-1'}] const request=async(...args)=>args[2]===`${API}/fleet/trips/TR-1`?response({trip:{tripNo:'TR-1'},dispatches:f.sharedTrips[0].dispatches}):h.request(...args) await verify({request},f,{mode:'read',runId:'aw-test'}) h.detail.references[0].key='999';await assert.rejects(()=>verify({request},f,{mode:'read',runId:'aw-test'}),/canonical trip identity/) }) test('completed local facts keep their explicit date and do not disappear behind the pending filter',async()=>{ const f=sample();f.batches=[];Object.assign(f.cases[0].expected,{executionStatus:'COMPLETED',dateBasis:'ACTUAL',calendarDate:'2026-10-15',actual:{occurredAt:null,localOccurredAt:'2026-10-15T23:30:00',timezone:null}}) const h=harness(f),request=async(...args)=>args[2].startsWith(`${WORK}/calendar?`)?response({days:[{date:'2026-10-15',total:1,typeCounts:{DELIVERY:1}}]}):h.request(...args) await verify({request},f,{mode:'read',runId:'aw-test'}) assert.ok(h.requests.some(r=>r.path.includes('/items?')&&r.path.includes('date=2026-10-15'))) assert.equal(h.requests.filter(r=>r.method==='POST').length,0) }) test('retired cancellation requires visible original history and exact replacement identities',async()=>{ const f=sample();f.retired=[{workItemKey:'OLD',containerBookingId:'1',replacementCases:['private']}] const h=harness();let replacements=[structuredClone(h.detail.item)] const request=async(...args)=>args[2]===`${WORK}/items/OLD`?response({history:[{action:'REPLACED'}],item:{allowedActions:[{code:'VIEW_HISTORY'}]},relatedItems:replacements}):h.request(...args) await verify({request},f,{mode:'read',runId:'aw-test'});replacements=[] await assert.rejects(()=>verify({request},f,{mode:'read',runId:'aw-test'}),/original requirement restored/) }) test('existing write guard persists before send and halts after unknown result',async()=>{ const events=[],context={options:{},request:async()=>{events.push('SENT');return {http:503,body:null}}} guardFleetWrites(context,async entry=>events.push(entry.status)) await assert.rejects(()=>context.request('POST',`${WORK}/items/WORK-1/commands`,{}),/Unknown write outcome/) await assert.rejects(()=>context.request('POST',`${WORK}/items/WORK-1/commands`,{})) assert.deepEqual(events,['SENDING','SENT','UNKNOWN_RESULT']) let sends=0;const diskFailure={options:{},request:async()=>{sends++}} guardFleetWrites(diskFailure,async()=>{throw Error('DISK_FAILED')}) await assert.rejects(()=>diskFailure.request('POST','/api/auth/login',{}),/DISK_FAILED/);assert.equal(sends,0) }) test('entry keeps report exclusive and reuses formal transport without SQL/internal sync/deploy',async()=>{ const code=await readFile(new URL('./appointment-work-test-env.mjs',import.meta.url),'utf8') assert.match(code,/new AcceptanceRun/);assert.match(code,/guardFleetWrites\(transport,checkpoint\)/) assert.match(code,/await mkdir\(directory\)/);assert.match(code,/'wx',0o600/);assert.match(code,/await file\.sync\(\)/) assert.match(code,/digest\(producer\)===fixture\.producerSha256/) assert.doesNotMatch(code,/mysql|JENKINS_|\/internal\/|fetch\(/) })