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'
2026-10-20 00:29:25 +08:00
function model ( fault = '' ) {
const copy = value => value == null ? value : structuredClone ( value ), calls = [], keys = new Map ()
const members = [ 0 , 1 ]. map ( i => ({ marker : 'RUN' , customerId : `C ${ i } ` , incomingNo : `IN ${ i } ` , dispatchNo : 'DS-ONE' , dispatchItemId : `ITEM ${ i } ` ,
balanceId : `B ${ i } ` , positionId : `P ${ i } ` , palletId : `PAL ${ i } ` , palletNo : `PALLET ${ i } ` , receiptVerified : true }))
const stops = [{ name : 'Origin' }, { name : 'A' , destinationKey : 'FBA:A' }, { name : 'B' , destinationKey : 'FBA:B' }]
const tripProof = { marker : 'FT-CURRENT' , dispatchNo : 'DS-ONE' , tripNo : 'TRIP' , warehouseId : 'WH' , vehicleId : 'V' , driverId : 'D' , stops ,
reassignment : { status : 'PENDING' }, updateRequest : { plannedDepartureAt : new Date ( Date . now () + 3600000 ). toISOString () } }
const actualEvidence = ( field , value , i ) => ({ field , value , actual : true , sourceType : 'WAREHOUSE_TRANSFER' , sourceBusinessNo : `IN ${ i } ` , observedAt : '2026-10-19T00:00:00Z' })
const fixture = { marker : 'RUN' , stage : fixtureContract . stage , dispatchNo : 'DS-ONE' , tripNo : 'TRIP' , tripProof , warehouseId : 'WH' , pdaToken : 'not-a-real-token' , members ,
loadingMeasurements : { marker : 'RUN' , environment : 'TEST' , dispatchNo : 'DS-ONE' , pallets : members . map (( m , i ) => ({ warehousePalletId : m . palletId ,
palletSpaces : 1 , deliveryIndex : i + 1 , evidence : [ actualEvidence ( 'palletSpaces' , 1 , i )] })) } }
const trip = { trip : { tripNo : 'TRIP' , warehouseId : 'WH' , status : 'PLANNED' , recordVersion : '0' , vehicleId : 'V' , driverId : 'D' , stops }, dispatches : [{ dispatchNo : 'DS-ONE' }] }
let nativeVersion = 5 , stage = 'LOADING' , arranged = false
const state = members . map ( m => ({ picked : 0 , delivery : null , outbound : null ,
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 : [{ id : `STOCK- ${ m . customerId } ` , customerId : m . customerId , overseasWarehouseId : 'WH' , positionId : m . positionId , businessType : 'STOCK_IN' , businessOrderNo : m . incomingNo , deltaPackages : '1' }] } }))
const counts = () => ({ itemCount : 2 , palletCount : 2 , packageCount : 2 , pickedPackageCount : state . reduce (( n , s ) => n + s . picked , 0 ), shippedPackageCount : [ 'DELIVERING' , 'DELIVERED' ]. includes ( stage ) ? 2 : 0 })
const items = () => members . map (( m , i ) => ({ dispatchItemId : m . dispatchItemId , customerId : m . customerId , warehousePalletId : m . palletId ,
palletNo : m . palletNo , destination : { key : i ? 'FBA:B' : 'FBA:A' }, scheduledPackages : 1 , pickedPackages : state [ i ]. picked }))
const deliveryRefs = () => state . filter ( s => s . delivery ). map (( s , i ) => ({ customerId : members [ i ]. customerId , deliveryNo : s . delivery . order . deliveryNo ,
outboundNo : s . outbound . outboundNo , packageCount : 1 , status : s . delivery . order . status , destination : { key : i ? 'FBA:B' : 'FBA:A' } }))
const detail = () => ({ dispatchNo : 'DS-ONE' , status : 'ACTIVE' , stage , recordVersion : nativeVersion , reviewState : 'APPROVED' ,
reviews : { dispatchReview : { status : 'APPROVED' }, loadingReview : { status : 'APPROVED' } }, counts : counts (),
documents : { deliveries : deliveryRefs (), outbounds : state . filter ( s => s . outbound ). map ( s => s . outbound ) } })
const result = ( body , payload , replayed = false ) => ({ dispatchNo : 'DS-ONE' , status : 'ACTIVE' , stage , recordVersion : nativeVersion ,
counts : counts (), idempotencyKey : body . idempotencyKey , result : payload , replayed })
const response = data => ({ http : 200 , body : { code : '200' , data : copy ( data ) }, requestId : 'request' , traceId : 'trace' })
const reject = message => ({ http : 200 , body : { code : 'A0409' , message }, requestId : 'request' , traceId : 'trace' })
const remember = ( body , payload ) => { const value = result ( body , payload ); keys . set ( body . idempotencyKey ,{ body : copy ( body ), result : copy ( value )}); return response ( value ) }
const context = { marker : 'RUN' , token : 'admin' , createdResources : { fleetTrips : [ tripProof ] }, assert ( _m , label , _r , condition ) { assert . ok ( condition , label ) },
apiSuccess ( _m , _l , r ) { return r . body . data },
async request ( method , raw , body , token ) {
calls . push ({ method , raw , body : copy ( body ), token }); const url = new URL ( raw , 'https://offline.invalid' ), path = url . pathname
if ( method === 'POST' && keys . has ( body ? . idempotencyKey )) {
const prior = keys . get ( body . idempotencyKey ); assert . deepEqual ( body , prior . body ); return response ({... prior . result , replayed : true })
2026-10-03 07:42:18 +08:00
}
2026-10-20 00:29:25 +08:00
if ( path === '/api/platform/v1/last-leg/fleet/trips/TRIP' ) return response ( trip )
if ( path === '/api/platform/v1/last-leg/dispatches/DS-ONE/items' ) return response ({ dispatchNo : 'DS-ONE' , recordVersion : nativeVersion , items : items ()})
if ( path === '/api/platform/v1/last-leg/dispatches/DS-ONE' ) return method === 'PUT' ? reject ( '已经真实扫码,不能编辑资源' ) : response ( detail ())
if ( path . endsWith ( '/loading' ) && method === 'GET' ) return response ({ dispatchNo : 'DS-ONE' , status : 'ACTIVE' , stage , recordVersion : nativeVersion , manifestHash : 'REAL-MANIFEST' ,
pickingReconciled : state . every ( s => s . picked === 1 ), loadingComplete : { allowed : state . every ( s => s . picked === 1 )}, pallets : members . map (( m , i )=>({ warehousePalletId : m . palletId , palletNo : m . palletNo ,
scheduledPackages : 1 , pickedPackages : state [ i ]. picked , palletSpaces : null , weightKg : 100 , volumeCbm : 1 , deliveryIndex : i + 1 ,
evidence : [ actualEvidence ( 'weightKg' , 100 , i ), actualEvidence ( 'volumeCbm' , 1 , i )]}))})
if ( path . endsWith ( '/loading-complete' )) {
if ( ! state . every ( s => s . picked === 1 )) {
if ( fault === 'outage disguised as rejection' ) return { http : 503 , body : { code : 'S0503' }, requestId : 'r' , traceId : 't' }
return reject ( fault === 'unrelated gate rejection' ? '车次版本已变化' : '逐项真实拣货尚未核平' )
2026-10-03 07:42:18 +08:00
}
2026-10-20 00:29:25 +08:00
assert . equal ( body . recordVersion , nativeVersion ); assert . equal ( body . measurements . length , 2 )
stage = 'AFTER_LOADING' ; nativeVersion ++ ; return remember ( body ,{ confirmedLoad : { manifestHash : body . manifestHash , pallets : body . measurements }})
2026-10-03 07:42:18 +08:00
}
2026-10-20 00:29:25 +08:00
if ( path . endsWith ( '/pick' )) {
const index = members . findIndex ( m => path . includes ( `/items/ ${ m . dispatchItemId } /` )), m = members [ index ], row = state [ index ]
assert . equal ( body . recordVersion , nativeVersion ); assert . equal ( body . positionId , m . positionId ); row . picked = 1 ; nativeVersion ++
row . stock . positions [ 0 ]. availablePackages = '0' ; row . stock . balances [ 0 ]. availablePackages = '0'
row . stock . ledger . push (...[ 'ALLOCATE' , 'PICK' , 'SHIP' ]. map ( type =>({ id : ` ${ type }${ index } ` , customerId : m . customerId , overseasWarehouseId : 'WH' , positionId : m . positionId , businessOrderNo : 'DS-ONE' , businessType : type , deltaPackages : type === 'SHIP' ? '-1' : '1' })))
if ( fault === 'duplicate ship' ) row . stock . ledger . push ({... row . stock . ledger . at ( - 1 ), id : 'EXTRA' })
return remember ( body ,{ dispatchItemId : m . dispatchItemId , pickedPackages : 1 })
}
if ( path === '/api/pda/v1/last-leg/dispatches/DS-ONE' ) return response ({ dispatchNo : 'DS-ONE' , warehouseId : 'WH' , status : 'ACTIVE' , stage , recordVersion : nativeVersion , counts : counts (), pickingReconciled : state . every ( s => s . picked === 1 ),
items : items (). map (( item , i )=>({... item , positions : fault === 'missing PDA position' ? [] : [{ positionId : members [ i ]. positionId , balanceId : members [ i ]. balanceId , availablePackages : state [ i ]. stock . positions [ 0 ]. availablePackages }]}))})
if ( path . endsWith ( '/delivery-arrangement' )) return response ({ dispatchNo : 'DS-ONE' , recordVersion : nativeVersion , status : 'ACTIVE' , stage , cargoHash : 'CARGO' , loadingCompletionHash : 'LOADED' , arrange : { allowed : true },
groups : members . map (( m , i )=>({ groupKey : `GROUP ${ i } ` , customerId : m . customerId , groupCargoHash : `GROUP-HASH ${ i } ` , destination : { key : i ? 'FBA:B' : 'FBA:A' }, cargo : [{ customerId : m . customerId , warehousePalletId : m . palletId }], blockReasons : [], requiredServices : [ 'DELIVERY_FEE' ]}))})
if ( path . endsWith ( '/transfer-delivery' )) {
assert . equal ( stage , 'AFTER_LOADING' ); assert . equal ( body . recordVersion , nativeVersion ); assert . equal ( body . groups . length , 2 )
assert . ok ( body . groups . every ( g => g . fees . length === 1 && g . fees [ 0 ]. amount === 30 ))
for ( const [ i , s ] of state . entries ()) { const m = members [ i ]; s . outbound = { outboundNo : `OUT ${ i } ` , dispatchNo : 'DS-ONE' , overseasWarehouseId : 'WH' , customerId : m . customerId , status : 'WAIT_OUTBOUND' }
s . delivery = { order : { deliveryNo : `DL ${ i } ` , customerId : fault === 'foreign delivery' ? 'OTHER' : m . customerId , dispatchNo : 'DS-ONE' , outboundNo : `OUT ${ i } ` , status : 'WAIT_DELIVERY' , version : 0 }} }
arranged = true ; nativeVersion ++ ; return remember ( body ,{ dispatchNo : 'DS-ONE' , outbounds : state . map (( s , i )=>({... s . outbound , packageCount : 1 })), deliveries : deliveryRefs ()})
}
if ( path . endsWith ( '/depart' )) {
if ( ! arranged ) {
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 reject ( '请先完成装车并一次安排全部客户派送' )
}
assert . equal ( body . recordVersion , nativeVersion ); stage = 'DELIVERING' ; nativeVersion ++ ; trip . trip . status = 'IN_TRANSIT'
for ( const [ i , s ] of state . entries ()) if ( ! ( fault === 'partial departure' && i === 1 )){ s . outbound . status = 'OUTBOUNDED' ; s . delivery . order . status = 'DELIVERING' }
if ( fault === 'depart deducts again' ){ state [ 0 ]. stock . balances [ 0 ]. availablePackages = '-1' ; state [ 0 ]. stock . positions [ 0 ]. availablePackages = '-1' }
return remember ( body ,{ shippedPackages : 2 , departedAt : '2026-10-19T01:00:00Z' , deliveries : deliveryRefs (), outbounds : state . map ( s => s . outbound )})
}
const index = members . findIndex (( _m , i )=> path . includes ( `/OUT ${ i } ` ) || path . includes ( `/DL ${ i } ` )), row = state [ index ]
2026-10-03 07:42:18 +08:00
if ( path . includes ( '/outbound-orders/' )) return response ({ outbound : row . outbound , deliveries : [ row . delivery . order ]})
2026-10-20 00:29:25 +08:00
if ( path . endsWith ( '/documents' )) {
2026-10-03 07:42:18 +08:00
if ( method === 'GET' ) return response ({ version : row . delivery . order . version , categories : []})
2026-10-20 00:29:25 +08:00
row . delivery . order . status = 'DELIVERED' ; row . delivery . order . version ++
2026-10-03 07:42:18 +08:00
if ( fault === 'signs both' ) state [ 1 - index ]. delivery . order . status = 'DELIVERED'
2026-10-20 00:29:25 +08:00
if ( state . every ( s => s . delivery . order . status === 'DELIVERED' )) stage = 'DELIVERED'
2026-10-03 07:42:18 +08:00
return response ({ version : row . delivery . order . version })
}
2026-10-20 00:29:25 +08:00
if ( path . includes ( '/delivery-orders/' )) { assert . equal ( url . searchParams . get ( 'customerId' ), members [ index ]. customerId ); return response ( row . delivery )}
2026-10-03 07:42:18 +08:00
throw new Error ( `Unexpected ${ method } ${ path } ` )
2026-10-20 00:29:25 +08:00
}, async requestMultipart ( method , path ) { calls . push ({ method , raw : path , multipart : true }); return response ({ fileId : `FILE- ${ path . includes ( 'DL0' ) ? 0 : 1 } ` })} }
2026-10-03 07:42:18 +08:00
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-20 00:29:25 +08:00
assert . equal ( new Set ( proof . members . map ( row => row . dispatchNo )). size , 1 )
assert . equal ( new Set ( proof . members . map ( row => row . deliveryNo )). size , 2 )
assert . equal ( new Set ( proof . members . map ( row => row . outboundNo )). size , 2 )
assert . ok ( ! m . calls . some ( c => c . raw . endsWith ( '/complete' ) || c . raw . includes ( '/dispatches/bind' )))
})
test ( 'missing physical pallet-space evidence blocks after both real picks without fabricating loading or delivery' , async ()=>{
const m = model (); delete m . fixture . loadingMeasurements
await assert . rejects ( verifyFleetCrossCustomer ( m . context , m . fixture ,{ execute : true , readInventory : m . readInventory }), /palletSpaces/ )
assert . ok ( m . state . every ( row => row . picked === 1 && row . delivery === null ))
assert . equal ( m . calls . filter ( call => call . raw . endsWith ( '/loading-complete' )). length , 1 , 'only the earlier expected unready rejection was sent' )
assert . equal ( m . calls . some ( call => call . raw . endsWith ( '/transfer-delivery' )), false )
2026-10-03 07:42:18 +08:00
})
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 })=>{
2026-10-20 00:29:25 +08:00
calls ++ ; assert . equal ( ready . trip . trip . status , 'PLANNED' ); assert . equal ( ready . dispatch . stage , 'AFTER_LOADING' )
assert . ok ( ready . members . every ( row => row . outbound . outbound . status === 'WAIT_OUTBOUND' ))
2026-10-04 10:32:26 +08:00
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-20 00:29:25 +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' , 'foreign delivery' ]) {
2026-10-03 07:42:18 +08:00
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/ )
2026-10-20 00:29:25 +08:00
assert . match ( source , /confirmFleetLoading/ )
2026-10-03 07:42:18 +08:00
})