2026-10-03 07:42:18 +08:00
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { verifyFleetCrossCustomer , fixtureContract } from './fleet-cross-customer-acceptance.mjs'
function model ( fault = '' ) {
const copy = value => structuredClone ( value ), calls = [], keys = new Set ()
const members = [ 0 , 1 ]. map ( i =>({ marker : 'RUN' , customerId : `C ${ i } ` , incomingNo : `IN ${ i } ` , dispatchNo : `DS ${ i } ` , outboundNo : `OUT ${ i } ` , deliveryNo : `DL ${ i } ` ,
balanceId : `B ${ i } ` , positionId : `P ${ i } ` , palletId : `PAL ${ i } ` , palletNo : `PALLET ${ i } ` , receiptVerified : true }))
const fixture = { marker : 'RUN' , stage : fixtureContract . stage , tripNo : 'TRIP' , warehouseId : 'WH' , pdaToken : 'not-a-real-token' , members }
const trip = { trip : { tripNo : 'TRIP' , warehouseId : 'WH' , status : 'PLANNED' , recordVersion : '0' , vehicleId : 'V' , driverId : 'D' , stops : [{ name : 'Origin' },
{ name : 'A' , destinationKey : 'FBA:A' },{ name : 'B' , destinationKey : 'FBA:B' }]}, dispatches : members . map ( m =>({ dispatchNo : m . dispatchNo , customerId : m . customerId , review : { status : 'PENDING' }}))}
const state = members . map ( m =>({ dispatch : { dispatchNo : m . dispatchNo , overseasWarehouseId : 'WH' , status : 'PENDING_DEPARTURE' , packageCount : 1 , pickedPackageCount : 0 ,
items : [{ id : `ITEM ${ m . customerId } ` , warehousePalletId : m . palletId }]}, delivery : { order : { deliveryNo : m . deliveryNo , customerId : m . customerId , dispatchNo : m . dispatchNo , outboundNo : m . outboundNo , status : 'WAIT_DELIVERY' , version : '0' }},
outbound : { outboundNo : m . outboundNo , dispatchNo : m . dispatchNo , overseasWarehouseId : 'WH' , status : 'WAIT_OUTBOUND' },
stock : { scope : { marker : 'RUN' , customerId : m . customerId , warehouseId : 'WH' , coverage : 'CUSTOMER_WAREHOUSE_COMPLETE' },
balances : [{ id : m . balanceId , customerId : m . customerId , overseasWarehouseId : 'WH' , availablePackages : '1' , holdPackages : '0' , allocatedPackages : '0' , pickedPackages : '0' , receivingPackages : '0' , inTransitPackages : '0' , procurementPackages : '0' }],
positions : [{ id : m . positionId , customerId : m . customerId , overseasWarehouseId : 'WH' , balanceId : m . balanceId , palletId : m . palletId , palletNo : m . palletNo , availablePackages : '1' , holdPackages : '0' , allocatedPackages : '0' , pickedPackages : '0' , receivingPackages : '0' }], ledger : []}}))
const cargo = members . map (( m , i )=>({ dispatchNo : m . dispatchNo , customerId : m . customerId , warehousePalletId : m . palletId , palletNo : m . palletNo , destinationKey : i ? 'FBA:B' : 'FBA:A' , packages : 1 }))
const response = data =>({ http : 200 , body : { code : '200' , data : copy ( data )}, requestId : 'request' , traceId : 'trace' })
const context = { marker : 'RUN' , token : 'admin' , createdResources : {}, assert ( _m , label , _r , condition ){ assert . ok ( condition , label )},
async request ( method , raw , body , token ){
calls . push ({ method , raw , body : copy ( body ), token }); const path = new URL ( raw , 'https://offline.invalid' ). pathname
let index = members . findIndex ( m => path . includes ( m . dispatchNo ) || path . includes ( m . outboundNo ) || path . includes ( m . deliveryNo ))
const row = state [ index ], member = members [ index ]
if ( path . endsWith ( '/review' )){ trip . dispatches [ index ]. review . status = 'APPROVED' ; trip . trip . recordVersion = String ( Number ( trip . trip . recordVersion ) + 1 ); return response ( trip )}
if ( path . endsWith ( '/loading-review' )){ trip . loadingReview = { status : 'APPROVED' }; trip . trip . recordVersion = String ( Number ( trip . trip . recordVersion ) + 1 ); return response ( trip )}
if ( path . endsWith ( '/loading' )){ if ( method === 'POST' ) trip . trip . recordVersion = String ( Number ( trip . trip . recordVersion ) + 1 ); return response ({ recordVersion : trip . trip . recordVersion , manifestHash : 'HASH' , cargo , blockReason : null })}
if ( path . endsWith ( '/TRIP' )) return response ( trip )
if ( path . endsWith ( '/pick' )){
if ( keys . has ( body . idempotencyKey )) return response ({ replayed : true })
keys . add ( body . idempotencyKey ); row . dispatch . pickedPackageCount = 1
row . stock . positions [ 0 ]. availablePackages = '0' ; row . stock . balances [ 0 ]. availablePackages = '0'
row . stock . ledger = [ 'ALLOCATE' , 'PICK' , 'SHIP' ]. map ( type =>({ id : ` ${ type }${ index } ` , customerId : member . customerId , overseasWarehouseId : 'WH' , positionId : member . positionId , businessOrderNo : member . dispatchNo , businessType : type , deltaPackages : type === 'SHIP' ? '-1' : '1' }))
if ( fault === 'duplicate ship' ) row . stock . ledger . push ({... row . stock . ledger [ 2 ], id : 'EXTRA' })
return response ({ replayed : false })
}
if ( path . endsWith ( '/complete' )){ row . dispatch . status = 'READY_TO_DEPART' ; return response ({ status : 'READY_TO_DEPART' })}
if ( path . endsWith ( '/depart' )){
if ( state . some ( s => s . dispatch . pickedPackageCount !== 1 )){
if ( fault === 'gate reorders route' ) [ trip . trip . stops [ 1 ], trip . trip . stops [ 2 ]] = [ trip . trip . stops [ 2 ], trip . trip . stops [ 1 ]]
if ( fault === 'gate mutates second customer' ){ state [ 1 ]. stock . balances [ 0 ]. holdPackages = '1' ; state [ 1 ]. stock . balances [ 0 ]. availablePackages = '0' ; state [ 1 ]. stock . positions [ 0 ]. holdPackages = '1' ; state [ 1 ]. stock . positions [ 0 ]. availablePackages = '0' }
return { http : fault === 'outage disguised as rejection' ? 503 : 200 , body : { code : fault === 'outage disguised as rejection' ? 'S0503' : 'A0409' , message : fault === 'unrelated gate rejection' ? '车次版本已变化' : 'DS1:请完成拣货并核平全部箱数' }, requestId : 'r' , traceId : 't' }
}
const replayed = keys . has ( body . idempotencyKey ); keys . add ( body . idempotencyKey ); trip . trip . status = 'IN_TRANSIT'
for ( const [ i , s ] of state . entries ()) if ( ! ( fault === 'partial departure' && i === 1 )){ s . dispatch . status = 'DEPARTED' ; s . outbound . status = 'OUTBOUNDED' }
if ( fault === 'depart deducts again' ){ row . stock . balances [ 0 ]. availablePackages = '-1' ; row . stock . positions [ 0 ]. availablePackages = '-1' }
return response ({ tripNo : 'TRIP' , status : 'IN_TRANSIT' , shippedPackages : '2' , dispatches : members . map ( m =>({ dispatchNo : m . dispatchNo })), replayed })
}
if ( path . includes ( '/api/pda/' )) return response ({ dispatchNo : member . dispatchNo , warehouseId : 'WH' , tripNo : 'TRIP' , status : row . dispatch . pickedPackageCount ? 'COMPLETED' : 'PDA_PENDING' ,
items : [{ dispatchItemId : `ITEM ${ member . customerId } ` , palletNo : member . palletNo , scheduledPackages : 1 , pickedPackages : String ( row . dispatch . pickedPackageCount ),
positions : fault === 'missing PDA position' ? [] : [{ id : member . positionId , balanceId : member . balanceId , availablePackages : row . stock . positions [ 0 ]. availablePackages }]}]})
if ( path . includes ( '/dispatches/' )) return response ( row . dispatch )
if ( path . includes ( '/outbound-orders/' )) return response ({ outbound : row . outbound , deliveries : [ row . delivery . order ]})
if ( path . endsWith ( '/status' )){ row . delivery . order . status = 'DELIVERING' ; row . delivery . order . version = String ( Number ( row . delivery . order . version ) + 1 ); return response ({ deliveryStatus : 'DELIVERING' })}
if ( path . endsWith ( '/documents' )){
if ( method === 'GET' ) return response ({ version : row . delivery . order . version , categories : []})
row . delivery . order . status = 'DELIVERED' ; row . delivery . order . version = String ( Number ( row . delivery . order . version ) + 1 )
if ( fault === 'signs both' ) state [ 1 - index ]. delivery . order . status = 'DELIVERED'
return response ({ version : row . delivery . order . version })
}
if ( path . includes ( '/delivery-orders/' )) return response ( row . delivery )
throw new Error ( `Unexpected ${ method } ${ path } ` )
}, async requestMultipart ( method , path , body ){ calls . push ({ method , raw : path , multipart : true }); return response ({ fileId : `FILE- ${ path . includes ( 'DL0' ) ? 0 : 1 } ` })}}
const readInventory = async scope =>{ const value = copy ( state [ members . findIndex ( m => m . customerId === scope . customerId )]. stock ); if ( fault === 'wrong stock scope' ) value . positions [ 0 ]. customerId = 'OTHER' ; return value }
return { context , fixture , readInventory , calls , state }
}
test ( 'default call and import do not request, create or activate anything' , async ()=>{
let called = false ; const result = await verifyFleetCrossCustomer ({ request (){ called = true }},{})
assert . equal ( result , fixtureContract ); assert . equal ( called , false )
})
test ( 'current DTO-shaped two-customer trip rejects unready gate, departs once and signs independently' , async ()=>{
const m = model (); const proof = await verifyFleetCrossCustomer ( m . context , m . fixture ,{ execute : true , readInventory : m . readInventory })
assert . equal ( proof . status , 'VERIFIED' ); assert . equal ( proof . gateProof , 'PREREQUISITE_REJECTION_VERIFIED' ); assert . equal ( proof . midTransactionRollback , 'NOT_TESTED' )
assert . equal ( m . calls . filter ( c => c . raw . endsWith ( '/depart' )). length , 3 )
assert . deepEqual ( m . state . map ( s => s . delivery . order . status ),[ 'DELIVERED' , 'DELIVERED' ])
assert . ok ( m . calls . filter ( c => c . raw . includes ( '/api/pda/' )). every ( c => c . token === m . fixture . pdaToken ))
assert . ok ( ! JSON . stringify ( proof ). includes ( m . fixture . pdaToken ))
})
2026-10-04 10:32:26 +08:00
test ( 'explicit rollback verifier runs after both picks and before successful departure' , async ()=>{
const m = model (); let calls = 0
const proof = await verifyFleetCrossCustomer ( m . context , m . fixture ,{ execute : true , readInventory : m . readInventory ,
verifyDepartureRollback : async ({ ready , departPath , departBody })=>{
calls ++ ; assert . equal ( ready . trip . trip . status , 'PLANNED' )
assert . ok ( ready . members . every ( row => row . dispatch . status === 'READY_TO_DEPART' ))
assert . equal ( m . calls . filter ( call => call . raw . endsWith ( '/depart' )). length , 1 )
assert . ok ( departPath . includes ( m . fixture . members [ 0 ]. dispatchNo )); assert . equal ( departBody . idempotencyKey , 'RUN-CROSS-DEPART' )
return { verified : true , scope : 'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK' }
}})
assert . equal ( calls , 1 ); assert . equal ( proof . midTransactionRollback , 'VERIFIED' )
})
for ( const failure of [ 'unverified' , 'throws' , 'mutates' ]) test ( `rollback verification ${ failure } prevents successful departure` , async ()=>{
const m = model ()
await assert . rejects ( verifyFleetCrossCustomer ( m . context , m . fixture ,{ execute : true , readInventory : m . readInventory ,
verifyDepartureRollback : async ()=>{
if ( failure === 'throws' ) throw Error ( 'Observed rollback failed' )
if ( failure === 'mutates' ) m . state [ 0 ]. stock . balances [ 0 ]. availablePackages = '-1'
return { verified : failure !== 'unverified' , scope : 'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK' }
}}))
assert . equal ( m . calls . filter ( call => call . raw . endsWith ( '/depart' )). length , 1 )
assert . ok ( m . state . every ( row => row . delivery . order . status === 'WAIT_DELIVERY' ))
})
2026-10-03 07:42:18 +08:00
for ( const fault of [ 'wrong stock scope' , 'missing PDA position' , 'duplicate ship' , 'gate mutates second customer' , 'gate reorders route' , 'outage disguised as rejection' , 'unrelated gate rejection' , 'partial departure' , 'depart deducts again' , 'signs both' ]) {
test ( `fails closed without widening the claimed evidence: ${ fault } ` , async ()=>{
const m = model ( fault ); await assert . rejects ( verifyFleetCrossCustomer ( m . context , m . fixture ,{ execute : true , readInventory : m . readInventory }),
fault === 'gate reorders route' ? /Rejected departure leaves both complete inventories\/outbounds\/deliveries\/trip unchanged/ : undefined )
assert . equal ( m . context . createdResources . fleetCrossCustomer . status , 'FAILED' )
assert . equal ( m . context . createdResources . fleetCrossCustomer . midTransactionRollback , 'NOT_TESTED' )
})
}
test ( 'missing reader, reused customer and stale marker reject before any API request' , async ()=>{
for ( const change of [ 'reader' , 'customer' , 'marker' ]){
const m = model (); if ( change === 'customer' ) m . fixture . members [ 1 ]. customerId = m . fixture . members [ 0 ]. customerId ; if ( change === 'marker' ) m . fixture . marker = 'OLD'
await assert . rejects ( verifyFleetCrossCustomer ( m . context , m . fixture ,{ execute : true , readInventory : change === 'reader' ? undefined : m . readInventory })); assert . equal ( m . calls . length , 0 )
}
})
test ( 'module contains no autonomous runner or SQL/environment/credential plumbing' , async ()=>{
const source = await readFile ( new URL ( './fleet-cross-customer-acceptance.mjs' , import . meta . url ), 'utf8' )
assert . doesNotMatch ( source , /process\.env|fetch\(|execSync|INSERT INTO|UPDATE ll_|createPicker|createMerchant/ )
assert . match ( source , /controlledPalletMeasurements/ )
})