This commit is contained in:
2026-10-21 14:30:41 +08:00
parent 0af977bdef
commit 40a03978cd
53 changed files with 1004 additions and 1291 deletions
+31 -74
View File
@@ -13,12 +13,12 @@ function departedRecoveryFixture() {
const measured=[{warehousePalletId:'P1',palletSpaces:2},{warehousePalletId:'P2',palletSpaces:1}]
const departure={action:'DEPART',recordVersion:'27',snapshot:{trip:snapshotTrip,confirmedLoad:{manifestHash:'a'.repeat(64),cargo,pallets:measured}}}
const refs=[{customerId:'C1',deliveryNo:'DL-OWN',outboundNo:'OUT-C1',packageCount:2},{customerId:'C1',deliveryNo:'DL-EXT',outboundNo:'OUT-C1',packageCount:1}]
const detail={dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',counts:{packageCount:3,pickedPackageCount:3,shippedPackageCount:3},documents:{deliveries:refs}}
const detail={dispatchInfo:{departedAt:snapshotTrip.departedAt},dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',counts:{packageCount:3,pickedPackageCount:3,shippedPackageCount:3},documents:{deliveries:refs}}
const items={dispatchNo:'OWN',recordVersion:'31',items:cargo.map(row=>({dispatchItemId:row.dispatchItemId,customerId:row.customerId,warehousePalletId:row.warehousePalletId,scheduledPackages:row.packages,pickedPackages:row.packages}))}
const orders=refs.map(ref=>({...ref,dispatchNo:'OWN',status:'DELIVERING',version:'1',documentSummary:{categories:[{fileCount:0}]}}))
const proof={protocol:'NATIVE_DISPATCH_V1',marker:'RUN',tripNo:'TR-OWN',dispatchNo:'OWN',warehouseId:'W1',customerId:'C1',status:'CHECKING',stage:'TRIP_OPERATING_COSTS',checkpoints:[],
dispatchNos:['OWN'],picker:{membershipId:'OLD-PDA'},departureRequest:{idempotencyKey:'old-key'},departureSnapshot:{detail:structuredClone(detail),items:structuredClone(items)},departureDeliveries:structuredClone(orders),
departure:{action:'DEPART',dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',result:{departedAt:snapshotTrip.departedAt,shippedPackages:3,deliveries:refs}}}
departure:{action:'LOADING_COMPLETE',dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',result:{loadingCompletionHash:'HASH',loadingCompletedAt:snapshotTrip.departedAt,confirmedLoad:{manifestHash:'a'.repeat(64)}}}}
const trip={marker:'RUN',tripNo:'TR-OWN',dispatchNo:'OWN',warehouseId:'W1',confirmedLoad:{manifestHash:'a'.repeat(64),pallets:measured},costAcceptance:{marker:'RUN',status:'FAILED',costs:[],departure,
requests:[{method:'GET',status:'RESPONDED',http:200,code:'200',requestId:'r',traceId:'t'}]},externalReceipt:{status:'VERIFIED',bookingNo:'BK',warehousePalletId:'P2'},externalDispatch:{dispatchNo:'OWN',deliveryNo:'DL-EXT'}}
const report={success:false,steps:[{name:'独立预期覆盖全部实物且没有重叠托盘',passed:false},{name:'模块执行',passed:false}],createdResources:{fleetAcceptance:proof,fleetTrips:[trip],fleetCostActors:{status:'DISABLED'},fleetFeeCoverageCases:[{mode:'PARTIAL',dispatchNo:'OWN',proof:{deliveryNos:['DL-OWN','DL-EXT']}}]}}
@@ -66,7 +66,7 @@ function returnSourceRecoveryFixture() {
trip.returnRedispatch={marker:'RUN',status:'FAILED',failure:report.steps[0].evidence,originalDeliveries:originals,
requests:originals.map(order=>({method:'GET',path:`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}?customerId=C1`,status:'RESPONDED',http:200,code:'200',requestId:'r',traceId:'t',result:{order:structuredClone(order)}}))}
report.createdResources.fleetFeeCoverageCases[0].proof.deliveryNos=['DL-A','DL-B','DL-EXT']
const detail={dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',counts:{packageCount:5,pickedPackageCount:5,shippedPackageCount:5},documents:{deliveries:originals.map(o=>({deliveryNo:o.deliveryNo,outboundNo:o.outboundNo,customerId:o.customerId,packageCount:o.packageCount}))}}
const detail={dispatchInfo:{departedAt:original.snapshot.trip.departedAt},dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',counts:{packageCount:5,pickedPackageCount:5,shippedPackageCount:5},documents:{deliveries:originals.map(o=>({deliveryNo:o.deliveryNo,outboundNo:o.outboundNo,customerId:o.customerId,packageCount:o.packageCount}))}}
const items={dispatchNo:'OWN',recordVersion:'31',items:original.snapshot.confirmedLoad.cargo.map(c=>({dispatchItemId:c.dispatchItemId,customerId:c.customerId,warehousePalletId:c.warehousePalletId,scheduledPackages:c.packages,pickedPackages:c.packages}))}
run.departureSnapshot={detail:structuredClone(detail),items:structuredClone(items)}
responses.set('/api/platform/v1/last-leg/dispatches/OWN',detail);responses.set('/api/platform/v1/last-leg/dispatches/OWN/items',items)
@@ -459,7 +459,7 @@ test('return redispatch is explicit, fresh-only and declares source-event proof
})
test('handover stage exports only a verified same-run departure fixture before the optional UI checkpoint', async () => {
const order = [], departure = { action:'DEPART', dispatchNo:'PC-NEW', status:'ACTIVE', stage:'DELIVERING', result:{departedAt:'2026-10-19T00:00:00Z',shippedPackages:1} }
const order = [], departure = { action:'LOADING_COMPLETE', dispatchNo:'PC-NEW', status:'ACTIVE', stage:'DELIVERING', result:{loadingCompletionHash:'HASH',loadingCompletedAt:'2026-10-19T00:00:00Z',confirmedLoad:{manifestHash:'HASH'}} }
const context = { marker: 'CURRENT', context: { saveFleetHandoverFixture(value) { order.push('save'); assert.equal(value.marker, 'CURRENT') } },
createdResources: { fleetAcceptance: { tripNo:'TR-NEW', dispatchNo:'PC-NEW', departure } } }
const trip = { tripNo: 'TR-NEW', dispatchNo:'PC-NEW', warehouseId: '92', costAcceptance: { status: 'VERIFIED', marker: 'CURRENT', expected: { finalCny: '100.03', usd: '13.07' } } }, details = [{ dispatchNo: 'PC-NEW' }]
@@ -706,10 +706,9 @@ test('checkpoint guard forbids repeating commands whose write outcome may be unk
assert.throws(() => reviewedFullCheckpoint(mutated), /unknown outcomes/)
assert.throws(() => reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED'), /exact successful command/)
Object.assign(mutated.createdResources.fleetFeeCoverageCases[0].proof, { deliveryNos: ['DEL-1'], confirmationRequest: { recordVersion:0,groups:[{groupKey:'G'}],idempotencyKey: 'original-key' } })
mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.push({ path:'/api/platform/v1/last-leg/dispatches/D/transfer-delivery',label: '正式保存本次原费用服务覆盖并转派送', method: 'POST',status:'RESPONDED',requestId:'r',traceId:'t',http: 200, code: '200',
body: structuredClone(mutated.createdResources.fleetFeeCoverageCases[0].proof.confirmationRequest), result: { action:'ARRANGE_DELIVERY',dispatchNo:'D',status:'ACTIVE',stage:'AFTER_LOADING',result:{outbounds:[{outboundNo:'OUT-1'}],deliveries:[{deliveryNo:'DEL-1'}]} } })
mutated.createdResources.fleetTrips[1].loadingCommand={body:{recordVersion:0,idempotencyKey:'original-key',measurements:[{warehousePalletId:'P'}]},result:{action:'LOADING_COMPLETE',dispatchNo:'D',status:'ACTIVE',stage:'DELIVERING',result:{loadingCompletionHash:'HASH',loadingCompletedAt:'2026-10-02T20:00:00Z',confirmedLoad:{manifestHash:'HASH'}}},afterReplay:{main:{documents:{outbounds:[{outboundNo:'OUT-1'}],deliveries:[{deliveryNo:'DEL-1'}]}}}}
assert.equal(reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED').persisted, true)
mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.at(-1).body.idempotencyKey = 'different-key'
delete mutated.createdResources.fleetTrips[1].loadingCommand.result.result.loadingCompletionHash
assert.throws(() => reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED'), /exact successful command/)
})
@@ -760,7 +759,7 @@ function terminalFixture(fault = '') {
const refs=[{customerId:'92',deliveryNo:'DEL-OWN',outboundNo:'OUT',packageCount:5},{customerId:'92',deliveryNo:'DEL-EXT',outboundNo:'OUT',packageCount:1}]
const proof={protocol:'NATIVE_DISPATCH_V1',marker:'M',tripNo:'T',dispatchNo:'OWN',dispatchNos:['OWN'],customerId:'92',stage:'RECEIPTS_AND_RESOURCE_RELEASE',closure,
receipts:refs.map(row=>({customerId:'92',deliveryNo:row.deliveryNo,signedAt:'2026-10-02T22:00:00'})),
departure:{action:'DEPART',dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',result:{departedAt:'2026-10-02T20:00:00Z',shippedPackages:6}},finance:{customerId:'92',status:'CONSUMER_PERSISTED',source:{sourceFeeId:'F1',sourceVersion:1}}}
departureSnapshot:{detail:{counts:{shippedPackageCount:6},dispatchInfo:{departedAt:'2026-10-02T20:00:00Z'}}},departure:{action:'LOADING_COMPLETE',dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',result:{loadingCompletionHash:'HASH',loadingCompletedAt:'2026-10-02T20:00:00Z',confirmedLoad:{manifestHash:'HASH'}}},finance:{customerId:'92',status:'CONSUMER_PERSISTED',source:{sourceFeeId:'F1',sourceVersion:1}}}
const resources={fleetAcceptance:proof,fleetTrips:[trip],fleetFeeCoverageCases:[{mode:'PARTIAL',dispatchNo:'OWN',proof:{deliveryNos:['DEL-OWN','DEL-EXT']}}]}
const calls=[],context={createdResources:resources,assert(){},async request(method,path){
calls.push({method,path});assert.equal(method,'GET');let data
@@ -837,74 +836,32 @@ test('native delivery references deduplicate the shared external destination but
assert.throws(() => nativeFleetDeliveries({ dispatchNo: 'MAIN', documents: { deliveries: [] } }))
})
function nativeDepartureFixture(fault = '') {
const refs = [{ customerId: '11', deliveryNo: 'DEL-A', outboundNo: 'OUT-A', packageCount: 2 },
{ customerId: '22', deliveryNo: 'DEL-B', outboundNo: 'OUT-B', packageCount: 1 }]
const before = { dispatchNo: 'MAIN', status: 'ACTIVE', stage: 'AFTER_LOADING', recordVersion: '9007199254740993', pickingReconciled: true,
counts: { packageCount: 3, pickedPackageCount: 3, shippedPackageCount: 0 }, documents: { deliveries: refs } }
const task = { ...structuredClone(before), warehouseId: '12', actions: { DEPART: { allowed: true } } }
const items = { dispatchNo: 'MAIN', recordVersion: before.recordVersion, items: [
{ dispatchItemId: '1', scheduledPackages: 2, pickedPackages: 2 }, { dispatchItemId: '2', scheduledPackages: 1, pickedPackages: 1 }] }
if (fault === 'stale-task') task.recordVersion = '9007199254740992'
if (fault === 'wrong-main') task.dispatchNo = 'OTHER'
if (fault === 'not-reconciled') task.pickingReconciled = false
if (fault === 'not-loaded') before.stage = 'LOADING'
if (fault === 'not-arranged') task.actions.DEPART.allowed = false
const command = { dispatchNo: 'MAIN', status: 'ACTIVE', stage: 'DELIVERING', action: 'DEPART', recordVersion: '9007199254740994', replayed: false,
result: { departedAt: '2026-10-19T01:00:00Z', shippedPackages: 3, deliveries: structuredClone(refs) } }
const after = { ...structuredClone(before), stage: 'DELIVERING', recordVersion: command.recordVersion,
counts: { packageCount: 3, pickedPackageCount: 3, shippedPackageCount: 3 } }
let departed = false
const calls = [], saved = [], run = { tripNo: 'TRIP', dispatchNo: 'MAIN' }, trip = { tripNo: 'TRIP', dispatchNo: 'MAIN', warehouseId: '12' }
const context = { async checkpoint() { saved.push(structuredClone(run)) }, async request(method, path, body, token) {
calls.push({ method, path, body: structuredClone(body), token }); let value
if (method === 'POST') {
assert.deepEqual(saved[0].departureRequest, body, 'original current-version command is durable before sending')
assert.equal(body.recordVersion, '9007199254740993')
if (path.startsWith('/api/pda/')) {
assert.equal(token, 'pda'); departed = true; value = structuredClone(command)
if (fault === 'changed-owner') value.result.deliveries[0].customerId = '99'
if (fault === 'changed-quantities') { value.result.deliveries[0].packageCount = 1; value.result.deliveries[1].packageCount = 2 }
} else {
assert.equal(token, undefined); assert.deepEqual(body, calls.find(row => row.method === 'POST').body)
value = { ...structuredClone(command), replayed: fault !== 'replay-miss' }
if (fault === 'replay-version') value.recordVersion = '9007199254740995'
}
} else if (path.startsWith('/api/pda/')) { assert.equal(token, 'pda'); value = task }
else if (path.endsWith('/items')) value = { ...items, recordVersion: departed ? after.recordVersion : before.recordVersion }
else if (path.includes('/delivery-orders/')) {
const ref = refs.find(row => path.includes('/' + row.deliveryNo + '?'))
assert.ok(path.endsWith('customerId=' + ref.customerId))
value = { order: { ...ref, dispatchNo: 'MAIN', status: 'DELIVERING', version: '3' } }
if (fault === 'foreign-delivery') value.order.customerId = '99'
} else value = departed ? after : before
return { http: 200, body: { code: '200', data: structuredClone(value) }, requestId: 'r', traceId: 't' }
} }
return { context, trip, run, calls, saved }
function nativeDepartureFixture(fault='') {
const refs=[{customerId:'11',deliveryNo:'DEL-A',outboundNo:'OUT-A',packageCount:2},{customerId:'22',deliveryNo:'DEL-B',outboundNo:'OUT-B',packageCount:1}]
const result={action:'LOADING_COMPLETE',dispatchNo:'MAIN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'9007199254740994',result:{loadingCompletionHash:'HASH',loadingCompletedAt:'2026-10-19T01:00:00Z',confirmedLoad:{manifestHash:'HASH'}}}
const detail={dispatchNo:'MAIN',status:'ACTIVE',stage:fault==='not-loaded'?'LOADING':'DELIVERING',recordVersion:fault==='stale-version'?'1':result.recordVersion,counts:{packageCount:3,pickedPackageCount:3,shippedPackageCount:fault==='quantity'?2:3},documents:{deliveries:refs}}
const run={tripNo:'TRIP',dispatchNo:'MAIN'},trip={tripNo:'TRIP',dispatchNo:'MAIN',warehouseId:'12',loadingCommand:{body:{idempotencyKey:'original'},result:structuredClone(result),replay:{...result,replayed:true}}},calls=[]
if(fault==='missing-command')delete trip.loadingCommand
const context={async checkpoint(){},async request(method,path,body,token){
calls.push({method,path,body,token});assert.equal(method,'GET');let data
if(path.includes('/commands/')){assert.equal(token,'pda');data=structuredClone(result);if(fault==='changed-command')data.result.loadingCompletionHash='OTHER'}
else if(path.endsWith('/items'))data={dispatchNo:'MAIN',recordVersion:detail.recordVersion,items:[]}
else if(path.includes('/delivery-orders/')){const ref=refs.find(r=>path.includes(r.deliveryNo));data={order:{...ref,dispatchNo:'MAIN',status:'DELIVERING',version:'3'}};if(fault==='foreign-delivery')data.order.customerId='OTHER'}
else data=detail
return {http:200,body:{code:'200',data:structuredClone(data)},requestId:'r',traceId:'t'}
}}
return {context,trip,run,calls}
}
test('one native current-version PDA DEPART replays exactly through platform and retains every customer delivery version', async () => {
const f = nativeDepartureFixture()
const result = await departNativeFleet(f.context, f.trip, 'pda', f.run)
assert.equal(result.departed, f.run.departure)
assert.deepEqual(f.run.dispatchNos, ['MAIN'])
assert.equal(result.dispatchDetails.length, 1)
assert.deepEqual(f.run.departureDeliveries.map(row => [row.customerId,row.version]), [['11','3'],['22','3']])
assert.equal(f.calls.filter(row => row.method === 'POST').length, 2)
const before = f.calls.length
await assert.rejects(departNativeFleet(f.context, f.trip, 'pda', f.run), /original command/)
assert.equal(f.calls.length, before, 'no fresh key after an original command exists')
test('one completed loading command is recovered read-only with real customer delivery versions',async()=>{
const f=nativeDepartureFixture(),result=await departNativeFleet(f.context,f.trip,'pda',f.run)
assert.equal(result.departed,f.run.departure);assert.deepEqual(f.run.dispatchNos,['MAIN'])
assert.deepEqual(f.run.departureDeliveries.map(r=>[r.customerId,r.version]),[['11','3'],['22','3']]);assert.ok(f.calls.every(c=>c.method==='GET'))
assert.deepEqual(f.run.departureRequest,f.trip.loadingCommand.body)
const before=f.calls.length;await assert.rejects(departNativeFleet(f.context,f.trip,'pda',f.run),/original command/);assert.equal(f.calls.length,before)
})
for(const fault of ['not-loaded','stale-version','quantity','missing-command','changed-command','foreign-delivery'])test(`automatic departure readback rejects ${fault}`,async()=>{
const f=nativeDepartureFixture(fault);await assert.rejects(departNativeFleet(f.context,f.trip,'pda',f.run));assert.ok(f.calls.every(c=>c.method==='GET'))
})
for (const fault of ['stale-task','wrong-main','not-reconciled','not-loaded','not-arranged','changed-owner','changed-quantities','replay-miss','replay-version','foreign-delivery'])
test(`native departure refuses stale or conflicting actual evidence: ${fault}`, async () => {
const f = nativeDepartureFixture(fault)
await assert.rejects(departNativeFleet(f.context, f.trip, 'pda', f.run))
if (['stale-task','wrong-main','not-reconciled','not-loaded','not-arranged'].includes(fault)) {
assert.equal(f.calls.filter(row => row.method === 'POST').length, 0)
assert.equal(f.saved.length, 0)
} else assert.ok(f.run.departureRequest?.idempotencyKey, 'sent requests retain their original key')
})
test('physical loading requires current-run actual per-field evidence and never fills pallet spaces from planned capacity', async () => {
const calls = [], trip = { dispatchNo: 'MAIN', stops: [{ destinationKey: 'ORIGIN:12' }, { destinationKey: 'FBA:A' }] }