2026-10-19 17:56:30 +08:00
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fulfillment , beginDispatchCommand , patchDispatchCommand , finishDispatchCommand , discardRejectedDispatchCommand , restoreFulfillmentDraft , updateFulfillmentDraft , resetFulfillmentSession } from '../src/stores/fulfillment.js'
import { requireDispatchVersion , dispatchVersionAtLeast , normalizeDispatchTask } from '../src/api/fulfillment.js'
const source = ( await readFile ( new URL ( '../src/pages/dispatch/dispatch.vue' , import . meta . url ), 'utf8' )). match ( /<script>([\s\S]*?)<\/script>/ )[ 1 ]
2026-10-20 10:36:04 +08:00
const item = ( overrides = {}) => ({ dispatchItemId : 'item-1' , sourceKey : 'source-1' , sourceBusinessType : 'RELABEL_ORDER' , sourceBusinessNo : 'RL-1' , customerId : 'customer-1' , customerName : '客户甲' , palletNo : 'PALLET-1' , scheduledPackages : 2 , pickedPackages : 0 , positions : [{ positionId : 'position-1' , availablePackages : 2 , allocatedPackages : 2 , pickedPackages : 0 }], returnCartons : [], ... overrides })
2026-10-19 17:56:30 +08:00
test ( 'native string quantities normalize without changing Long identity or version' , () => {
const id = '9007199254740993'
const value = normalizeDispatchTask ( JSON . parse ( JSON . stringify ({ dispatchNo : 'PC/1' , recordVersion : id , items : [ item ({ dispatchItemId : id , scheduledPackages : '12' , pickedPackages : '0' , positions : [{ positionId : id , balanceId : id , availablePackages : '12' , allocatedPackages : '12' , pickedPackages : '0' }], returnCartons : [{ returnCartonId : id , productQuantity : '24' }] })] })))
assert . equal ( value . recordVersion , id ); assert . equal ( value . items [ 0 ]. dispatchItemId , id )
assert . equal ( value . items [ 0 ]. scheduledPackages , 12 ); assert . equal ( value . items [ 0 ]. pickedPackages , 0 )
assert . deepEqual ( value . items [ 0 ]. positions [ 0 ], { positionId : id , balanceId : id , availablePackages : 12 , allocatedPackages : 12 , pickedPackages : 0 })
assert . equal ( value . items [ 0 ]. returnCartons [ 0 ]. returnCartonId , id ); assert . equal ( value . items [ 0 ]. returnCartons [ 0 ]. productQuantity , 24 )
assert . equal ( normalizeDispatchTask ({ dispatchNo : 'PC/1' , items : [ item ({ scheduledPackages : String ( Number . MAX_SAFE_INTEGER ) })] }). items [ 0 ]. scheduledPackages , Number . MAX_SAFE_INTEGER )
})
test ( 'invalid, missing and unsafe native quantities fail at every item, position and carton boundary' , () => {
for ( const invalid of [ undefined , null , true , '' , ' ' , '01' , '+1' , '-1' , '1.0' , '1e2' , - 1 , 0.5 , NaN , Infinity , '9007199254740992' , 9007199254740992 ]) {
for ( const field of [ 'scheduledPackages' , 'pickedPackages' ]) assert . throws (() => normalizeDispatchTask ({ dispatchNo : 'PC/1' , items : [ item ({ [ field ] : invalid })] }), /排车数量/ )
for ( const field of [ 'availablePackages' , 'allocatedPackages' , 'pickedPackages' ]) assert . throws (() => normalizeDispatchTask ({ dispatchNo : 'PC/1' , items : [ item ({ positions : [{ ... item (). positions [ 0 ], [ field ] : invalid }] })] }), /排车数量/ )
assert . throws (() => normalizeDispatchTask ({ dispatchNo : 'PC/1' , items : [ item ({ returnCartons : [{ productQuantity : invalid }] })] }), /排车数量/ )
}
})
const task = ( overrides = {}) => normalizeDispatchTask ({ taskNo : 'TASK-1' , dispatchNo : 'PC/1' , recordVersion : 0 , status : 'ACTIVE' , stage : 'LOADING' , pickingReconciled : false , actions : { PICK : { allowed : true }, LOADING_COMPLETE : { allowed : true }, DEPART : { allowed : false } }, items : [ item ()], ... overrides })
function storage () { const values = new Map (); globalThis . uni = { setStorageSync : ( key , value ) => values . set ( key , structuredClone ( value )), getStorageSync : key => structuredClone ( values . get ( key )), removeStorageSync : key => values . delete ( key ), showModal : async () => ({ confirm : true }) }; resetFulfillmentSession (); restoreFulfillmentDraft ( 'operator:warehouse' ); return values }
function page ( dependencies = {}) {
const deps = { PortalHeader : {}, ScanTargetInput : {}, DispatchHandoverStatus : {}, session : { currentUser : {} }, fulfillment , requireDispatchVersion , dispatchVersionAtLeast , beginDispatchCommand , patchDispatchCommand , finishDispatchCommand , discardRejectedDispatchCommand , updateFulfillmentDraft , playSuccessSound () {}, playErrorSound () {}, toDispatchError : error => ({ message : error . message }), ... dependencies }
const definition = new Function (... Object . keys ( deps ), source . replace ( /^\s*import .*$/gm , '' ). replace ( 'export default' , 'return' ))(... Object . values ( deps ))
const state = { ... definition . data (), current : task () }
for ( const [ name , method ] of Object . entries ( definition . methods )) state [ name ] = method . bind ( state )
for ( const [ name , getter ] of Object . entries ( definition . computed )) Object . defineProperty ( state , name , { get : () => getter . call ( state ) })
return state
}
const proof = ( command , version = 1 ) => ({ dispatchNo : command . dispatchNo , action : command . action , idempotencyKey : command . body . idempotencyKey , recordVersion : version , stage : command . action === 'LOADING_COMPLETE' ? 'AFTER_LOADING' : command . action === 'DEPART' ? 'DELIVERING' : 'LOADING' , result : { confirmed : true } })
test ( 'full pending command survives restoration, rejects another operation, and isolates the next operator' , () => {
storage (); const pending = beginDispatchCommand ( 'PC/1' , 'PICK' , { recordVersion : 0 , dispatchItemId : 'item-1' , packages : 1 })
const original = structuredClone ( JSON . parse ( JSON . stringify ( pending )))
fulfillment . dispatch . pendingCommand = null ; restoreFulfillmentDraft ( 'operator:warehouse' )
assert . deepEqual ( JSON . parse ( JSON . stringify ( fulfillment . dispatch . pendingCommand )), original )
assert . throws (() => beginDispatchCommand ( 'PC/2' , 'DEPART' , { recordVersion : 2 }), /原排车/ )
restoreFulfillmentDraft ( 'another:warehouse' ); assert . equal ( fulfillment . dispatch . pendingCommand , null )
})
test ( 'real picking requires the native gate, physical source and current stage; empty or merely counted tasks cannot complete loading' , async () => {
storage (); const state = page (); state . selectPallet ( state . current . items [ 0 ]); assert . equal ( state . canPick , true )
for ( const allowed of [ false , 'true' , undefined ]) { state . current . actions . PICK . allowed = allowed ; assert . equal ( state . canPick , false ) }
state . current . actions . PICK . allowed = true ; state . current . stage = 'BEFORE_LOADING' ; assert . equal ( state . canPick , false )
state . current = task ({ items : [] , pickingReconciled : true }); assert . equal ( state . canComplete , false )
state . current = task ({ items : [ item ({ pickedPackages : 2 })] }); assert . equal ( state . canComplete , false )
state . current . pickingReconciled = true ; assert . equal ( state . canComplete , true ); assert . equal ( state . canDepart , false )
})
test ( 'return carton pick keeps its exact item, quantity and explicit version zero without inventing a position' , async () => {
storage (); let sent
const state = page ({ pickDispatchItem : async (... args ) => { sent = args ; return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchTask : async () => task ({ recordVersion : 1 }) })
state . current = task ({ items : [ item ({ returnCartons : [{ returnCartonId : 'rc-1' , cartonMark : 'RC-1' , status : 'OUTBOUND_PENDING' , productQuantity : 5 }] })] })
state . handleScan ( 'RC-1' ); await state . submitPick ()
assert . equal ( sent [ 0 ], 'PC/1' ); assert . equal ( sent [ 1 ], 'item-1' )
assert . deepEqual ({ ... sent [ 2 ], idempotencyKey : 'key' }, { recordVersion : 0 , positionId : null , packages : 1 , productQuantity : 5 , cartonMark : 'RC-1' , remark : null , idempotencyKey : 'key' })
assert . equal ( fulfillment . dispatch . pendingCommand , null )
})
test ( 'unknown POST queries the original key and retries the unchanged payload; a subsequent equal physical pick gets a new key' , async () => {
storage (); const sent = []; let fail = true
const state = page ({ pickDispatchItem : async ( no , id , body ) => { sent . push ({ no , id , body }); if ( fail ) { fail = false ; throw { network : true , message : 'offline' } } return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchCommand : async () => { throw { code : 'HTTP_404' } }, getDispatchTask : async () => task ({ recordVersion : 1 }) })
state . selectPallet ( state . current . items [ 0 ]); await state . submitPick (); assert . ok ( fulfillment . dispatch . pendingCommand )
state . current . recordVersion = 80 ; await state . recoverCommand (); assert . deepEqual ( sent [ 0 ], sent [ 1 ]); assert . equal ( sent [ 1 ]. body . recordVersion , 0 )
state . selectPallet ( state . current . items [ 0 ]); await state . submitPick (); assert . notEqual ( sent [ 2 ]. body . idempotencyKey , sent [ 1 ]. body . idempotencyKey )
})
test ( 'proven loading completion survives a failed GET and later departure by the other endpoint without another loading POST' , async () => {
storage (); let posts = 0 , reads = 0
beginDispatchCommand ( 'PC/1' , 'LOADING_COMPLETE' , { recordVersion : 0 , manifestHash : 'manifest' , measurements : [] })
const state = page ({ completeDispatchLoading : async () => { posts ++ ; return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchTask : async () => { if ( ++ reads === 1 ) throw Error ( 'read failed' ); return task ({ stage : 'DELIVERING' , recordVersion : 3 }) } })
await state . recoverCommand (); assert . equal ( fulfillment . dispatch . pendingCommand . result . stage , 'AFTER_LOADING' )
await state . recoverCommand (); assert . equal ( posts , 1 ); assert . equal ( state . current . stage , 'DELIVERING' ); assert . equal ( fulfillment . dispatch . pendingCommand , null )
})
test ( 'later rejection cannot erase an earlier unknown write and older details cannot release a proven result' , async () => {
storage (); beginDispatchCommand ( 'PC/1' , 'DEPART' , { recordVersion : 0 }); patchDispatchCommand ({ attempted : true })
const state = page ({ getDispatchCommand : async () => { throw { code : '404' } }, departDispatch : async () => { throw { code : 'A0409' , message : 'conflict' } } })
await state . recoverCommand (); assert . equal ( fulfillment . dispatch . pendingCommand . rejected , false )
state . discardRejected (); assert . ok ( fulfillment . dispatch . pendingCommand )
patchDispatchCommand ({ result : proof ( fulfillment . dispatch . pendingCommand , '9007199254740993' ) })
const stale = page ({ getDispatchTask : async () => task ({ recordVersion : '9007199254740992' }) }); await stale . recoverCommand (); assert . ok ( fulfillment . dispatch . pendingCommand ); assert . match ( stale . error . message , /版本早于/ )
})
test ( 'actual loading confirmation leaves reference measurements blank and blocks missing unloading mapping' , async () => {
storage (); const preview = { dispatchNo : 'PC/1' , recordVersion : 0 , manifestHash : 'manifest' , pickingReconciled : true , loadingComplete : { allowed : true }, pallets : [{ warehousePalletId : 'pallet-1' , palletNo : 'PALLET-1' , palletSpaces : 1 , deliveryIndex : null , weightKg : 100 , volumeCbm : 2 , evidence : [{ field : 'weightKg' , actual : false , sourceType : 'ORDER_SNAPSHOT_ALLOCATION' }] }] }
let posts = 0 ; const state = page ({ getDispatchLoading : async () => preview , completeDispatchLoading : async () => { posts ++ } })
state . current = task ({ pickingReconciled : true , items : [ item ({ pickedPackages : 2 })] }); await state . prepareLoading ()
assert . equal ( state . loadingRows [ 0 ]. weightKg , null ); assert . equal ( state . loadingRows [ 0 ]. volumeCbm , null )
Object . assign ( state . loadingRows [ 0 ], { weightKg : 92 , volumeCbm : 1.5 }); await state . completeLoading (); assert . equal ( posts , 0 ); assert . match ( state . error . message , /卸货站/ )
})
test ( 'missing versions surface a local error before picking or departure and never produce a write' , async () => {
storage (); const state = page (); state . selectPallet ( state . current . items [ 0 ]); state . current . recordVersion = undefined
await state . submitPick (); assert . match ( state . error . message , /版本/ ); assert . equal ( fulfillment . dispatch . pendingCommand , null )
state . current = task ({ recordVersion : undefined , stage : 'AFTER_LOADING' , actions : { DEPART : { allowed : true } } }); await state . depart (); assert . match ( state . error . message , /版本/ ); assert . equal ( fulfillment . dispatch . pendingCommand , null )
})
test ( 'a first server error remains unknown and cannot be discarded as a definite business rejection' , async () => {
storage (); beginDispatchCommand ( 'PC/1' , 'PICK' , { recordVersion : 0 , dispatchItemId : 'item-1' , packages : 1 })
const state = page ({ pickDispatchItem : async () => { throw { code : 'A0500' , message : 'server failure' } } })
await state . recoverCommand (); state . discardRejected (); assert . equal ( fulfillment . dispatch . pendingCommand . rejected , false )
})
test ( 'position picking is bounded by this dispatch allocation and skips already picked allocations' , () => {
storage (); const state = page (); state . current . items [ 0 ]. positions = [{ positionId : 'already-picked' , availablePackages : 20 , allocatedPackages : 1 , pickedPackages : 1 }, { positionId : 'actual-position' , availablePackages : 20 , allocatedPackages : 1 , pickedPackages : 0 }]
state . selectPallet ( state . current . items [ 0 ]); assert . equal ( state . selectedPosition . positionId , 'actual-position' ); assert . equal ( state . selectedRemaining , 1 )
updateFulfillmentDraft ( 'dispatch' , { packages : 2 }); assert . equal ( state . canPick , false )
})
2026-10-20 10:36:04 +08:00
test ( 'a shared pallet exposes both relabel sources and picking the second preserves its exact position and quantity' , async () => {
storage (); const rows = [ item ({ scheduledPackages : 3 }), item ({ dispatchItemId : 'item-2' , sourceKey : 'source-2' , sourceBusinessNo : 'RL-2' , customerId : 'customer-2' , customerName : '客户乙' , scheduledPackages : 5 , positions : [{ positionId : 'position-2' , availablePackages : 5 , allocatedPackages : 5 , pickedPackages : 0 }] })]
let sent
const state = page ({ pickDispatchItem : async (... args ) => { sent = args ; return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchTask : async () => task ({ recordVersion : 1 , items : rows . map (( row , index ) => index ? { ... row , pickedPackages : 5 } : row ) }) })
state . current = task ({ items : rows }); state . handleScan ( 'PALLET-1' )
assert . equal ( fulfillment . dispatch . dispatchItemId , '' ); assert . deepEqual ( state . sourceChoices . map ( row => [ row . sourceBusinessNo , row . customerId , state . pickablePackages ( row )]), [[ 'RL-1' , 'customer-1' , 2 ], [ 'RL-2' , 'customer-2' , 5 ]])
state . chooseSource ( state . sourceChoices [ 1 ]); state . updatePackages ({ detail : { value : '5' } }); await state . submitPick ()
assert . equal ( sent [ 1 ], 'item-2' ); assert . equal ( sent [ 2 ]. positionId , 'position-2' ); assert . equal ( sent [ 2 ]. packages , 5 ); assert . equal ( state . canComplete , false )
})
test ( 'an ambiguous source carton requires explicit source selection and never picks the first matching item' , async () => {
storage (); let sent
const rows = [ item ({ returnCartons : [{ returnCartonId : 'carton-1' , cartonMark : 'SHARED-MARK' , status : 'OUTBOUND_PENDING' , productQuantity : 2 }] }), item ({ dispatchItemId : 'item-2' , sourceKey : 'source-2' , sourceBusinessNo : 'RL-2' , customerId : 'customer-2' , returnCartons : [{ returnCartonId : 'carton-2' , cartonMark : 'SHARED-MARK' , status : 'OUTBOUND_PENDING' , productQuantity : 7 }] })]
const state = page ({ pickDispatchItem : async (... args ) => { sent = args ; return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchTask : async () => task ({ recordVersion : 1 , items : rows }) })
state . current = task ({ items : rows }); state . handleScan ( 'SHARED-MARK' )
assert . equal ( state . sourceChoices . length , 2 ); assert . equal ( fulfillment . dispatch . dispatchItemId , '' ); assert . equal ( state . canPick , false )
state . chooseSource ( state . sourceChoices [ 1 ]); await state . submitPick ()
assert . equal ( sent [ 1 ], 'item-2' ); assert . equal ( sent [ 2 ]. cartonMark , 'SHARED-MARK' ); assert . equal ( sent [ 2 ]. productQuantity , 7 ); assert . equal ( sent [ 2 ]. positionId , null )
})
test ( 'selection restoration cannot reuse the same item ID for a different logical source' , () => {
storage (); const state = page (); state . selectPallet ( state . current . items [ 0 ]); assert . equal ( state . canPick , true )
state . current . items [ 0 ]. sourceKey = 'different-source' ; assert . equal ( state . selectedItem , null ); assert . equal ( state . canPick , false )
})
for ( const sourceType of [ 'INBOUND_PLAN' , 'RETURN_RECEIPT' ]) test ( `ordinary ${ sourceType } null business fields survive real wire normalization and exact picking` , async () => {
storage (); let sent
const raw = JSON . parse ( JSON . stringify ({ dispatchNo : 'PC/1' , taskNo : 'TASK-1' , recordVersion : '0' , status : 'ACTIVE' , stage : 'LOADING' , actions : { PICK : { allowed : true } }, pickingReconciled : false , items : [ item ({ sourceKey : ` ${ sourceType } :source-1` , sourceBusinessType : null , sourceBusinessNo : null , scheduledPackages : '4' , pickedPackages : '0' , positions : [{ positionId : '9007199254740993' , balanceId : '9007199254740994' , availablePackages : '4' , allocatedPackages : '4' , pickedPackages : '0' }] })] }))
const normalized = normalizeDispatchTask ( raw )
assert . equal ( normalized . items [ 0 ]. sourceBusinessType , null ); assert . equal ( normalized . items [ 0 ]. sourceBusinessNo , null )
const state = page ({ pickDispatchItem : async (... args ) => { sent = args ; return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchTask : async () => normalizeDispatchTask ({ ... raw , recordVersion : 1 }) })
state . current = normalized ; state . handleScan ( 'PALLET-1' ); assert . equal ( state . canPick , true )
state . updatePackages ({ detail : { value : '4' } }); await state . submitPick ()
assert . equal ( sent [ 1 ], 'item-1' ); assert . equal ( sent [ 2 ]. positionId , '9007199254740993' ); assert . equal ( sent [ 2 ]. packages , 4 ); assert . equal ( sent [ 2 ]. recordVersion , '0' )
assert . equal ( 'customerId' in sent [ 2 ], false )
})
test ( 'RELABEL_ORDER still requires its true business number and all sources require sourceKey' , () => {
storage (); const state = page ()
state . selectPallet ( item ({ sourceBusinessNo : null })); assert . equal ( fulfillment . dispatch . dispatchItemId , '' ); assert . match ( state . error . message , /来源业务身份/ )
state . selectPallet ( item ({ sourceKey : '' , sourceBusinessType : null , sourceBusinessNo : null })); assert . equal ( fulfillment . dispatch . dispatchItemId , '' )
})
test ( 'duplicate matching carton facts inside one source fail closed rather than taking its first carton' , () => {
storage (); const state = page ()
state . current = task ({ items : [ item ({ returnCartons : [ 'one' , 'two' ]. map ( returnCartonId => ({ returnCartonId , cartonMark : 'DUPLICATE' , status : 'OUTBOUND_PENDING' , productQuantity : 1 })) })] })
state . handleScan ( 'DUPLICATE' ); assert . match ( state . error . message , /重复待拣箱唛/ ); assert . equal ( fulfillment . dispatch . dispatchItemId , '' ); assert . equal ( state . canPick , false )
})
test ( 'two picked logical sources use the single authoritative physical loading row without duplicating measurements' , async () => {
storage (); const rows = [ item ({ pickedPackages : 2 }), item ({ dispatchItemId : 'item-2' , sourceKey : 'source-2' , sourceBusinessNo : 'RL-2' , pickedPackages : 2 })]
const preview = { dispatchNo : 'PC/1' , recordVersion : 0 , manifestHash : 'one-physical-pallet' , pickingReconciled : true , loadingComplete : { allowed : true }, pallets : [{ warehousePalletId : '21' , palletNo : 'PALLET-1' , palletSpaces : 1 , deliveryIndex : 1 , weightKg : 100 , volumeCbm : 2 , evidence : [{ field : 'weightKg' , actual : true }, { field : 'volumeCbm' , actual : true }] }] }
let sent
const state = page ({ getDispatchLoading : async () => preview , completeDispatchLoading : async (... args ) => { sent = args ; return proof ( fulfillment . dispatch . pendingCommand ) }, getDispatchTask : async () => task ({ recordVersion : 1 , stage : 'AFTER_LOADING' , items : rows , pickingReconciled : true }) })
state . current = task ({ items : rows , pickingReconciled : true }); await state . prepareLoading (); assert . equal ( state . loadingRows . length , 1 )
await state . completeLoading (); assert . deepEqual ( sent [ 1 ]. measurements , [{ warehousePalletId : '21' , palletSpaces : 1 , weightKg : 100 , volumeCbm : 2 , deliveryIndex : 1 }])
})