2026-08-24 15:38:28 +08:00
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
2026-09-30 15:16:39 +08:00
import { expandOverseasCartonCandidates , resolveCartonFixtures , run , uniqueContainerProducer ,
2026-09-30 17:13:11 +08:00
crossContainerCartonProbe , normalizeReceivingEvidence , receivingEvidenceValid ,
2026-10-02 20:14:17 +08:00
receivingCompletionProof , captureReceivingEvidence , receivingEventRecoveryInventoryUnchanged ,
2026-10-08 21:36:53 +08:00
probeCartonResolveNegatives , awaitReceivingCandidate , convertReceivingContainers ,
awaitProducerConsumption , awaitPickupReadyCandidates } from './phase4-overseas-pda.mjs'
function conversionContext ({ existing = false , eta = null , wrongIdentity = false } = {}) {
const bookingId = '2108183323464437762' , historicalId = '2099376270288678913'
const producer = { containerNo : 'TSTU1800264' , containerBookingId : bookingId ,
resourceKey : 'THIS-RUN-P4-FBA-SECOND:FBA:TSTU1800264' }
const summary = { containerNo : producer . containerNo , sourceContainerBookingId : wrongIdentity ? historicalId : bookingId ,
totalOrderCount : 1 , inboundCodeCount : 1 , missingInboundCodeCount : 0 ,
planCount : existing ? 1 : 0 , pendingPlanCount : existing ? 0 : 1 , allPlansReady : existing }
const historical = { ... summary , sourceContainerBookingId : historicalId , planCount : 1 , pendingPlanCount : 0 , allPlansReady : true }
const plans = [{ lines : [{ inboundCode : 'THIS-RUN-INBOUND' }], pallets : [{ palletNo : 'PLT-THIS-RUN' , warehousePalletId : '88' }] }]
const calls = [], steps = []
let converted = existing
const context = { marker : 'THIS-RUN' , token : 'offline' , calls , steps ,
createdResources : { phase4OverseasPda : { generatedPalletsByInbound : {} }, phase4TailOrderControls : { generatedPlans : plans } },
addStep : ( _module , name , _response , passed ) => steps . push ({ name , passed }),
assert : ( _module , name , _response , passed ) => steps . push ({ name , passed }),
request : async ( method , path , body , _token , headers ) => {
calls . push ({ method , path , body , headers })
let data
if ( path . includes ( '/container-summaries?' )) data = { records : [ historical , summary ] }
else if ( path . endsWith ( `/container-summaries/ ${ bookingId } ` )) data = { summary : converted ? { ... summary , planCount : 1 , pendingPlanCount : 0 , allPlansReady : true } : summary }
else if ( path . endsWith ( '/container-arrival-times/query' )) data = [{ sourceContainerBookingId : body . sourceContainerBookingIds [ 0 ], estimatedWarehouseArrivalAt : eta , version : 0 }]
else if ( path . endsWith ( '/expected-arrival' )) {
if ( existing || path . includes ( historicalId )) return { http : 200 , body : { code : 'A0409' , message : '该柜已生成入库计划,预计到仓时间不可修改' } }
data = { sourceContainerBookingId : bookingId , estimatedWarehouseArrivalAt : body . estimatedWarehouseArrivalAt , version : 1 }
} else if ( path . includes ( '/container-preview?' )) data = { sourceContainerBookingId : bookingId , ready : true , totalOrderCount : 1 ,
pendingOrderCount : existing ? 0 : 1 , existingPlannedPalletCount : existing ? 1 : 0 , snapshotVersion : 1 , snapshotHash : 'snapshot' }
else if ( path . endsWith ( '/convert-container' )) { converted = true ; data = plans }
else throw Error ( `Unexpected offline request: ${ method } ${ path } ` )
return { http : 200 , body : { code : '200' , data }, requestId : 'offline-request' , traceId : 'offline-trace' }
} }
return { context , producer , bookingId , historicalId }
}
test ( 'r1 same-number historical booking cannot replace the current producer during ETA and plan conversion' , async () => {
const { context , producer , bookingId , historicalId } = conversionContext ()
assert . equal ( await convertReceivingContainers ( context , [ producer ]), true )
assert . ok ( context . steps . every ( step => step . passed ))
assert . equal ( context . calls [ 0 ]. path , `/api/platform/v1/last-leg/orders/container-summaries/ ${ bookingId } ` )
assert . ok ( context . calls . every ( call => ! call . path . includes ( historicalId ) && ! call . path . includes ( 'container-summaries?' )))
assert . deepEqual ( context . calls . filter ( call => call . path . endsWith ( '/convert-container' )). map ( call => call . body . sourceContainerBookingId ), [ bookingId , bookingId ])
assert . equal ( context . calls . filter ( call => call . method === 'PUT' ). length , 1 )
})
test ( 'existing current-run plans reuse their ETA without writes; missing locked ETA blocks rather than repairs' , async () => {
for ( const eta of [ '2026-10-10T10:00:00' , null ]) {
const { context , producer } = conversionContext ({ existing : true , eta })
assert . equal ( await convertReceivingContainers ( context , [ producer ]), Boolean ( eta ))
assert . equal ( context . calls . filter ( call => call . method === 'PUT' || call . path . endsWith ( '/convert-container' )). length , 0 )
assert . equal ( context . steps . every ( step => step . passed ), Boolean ( eta ))
}
})
test ( 'missing producer booking or mismatched detail identity fails before any ETA query or write' , async () => {
for ( const missing of [ false , true ]) {
const { context , producer } = conversionContext ({ wrongIdentity : true })
if ( missing ) delete producer . containerBookingId
assert . equal ( await convertReceivingContainers ( context , [ producer ]), false )
assert . ok ( context . steps . some ( step => ! step . passed ))
assert . ok ( context . calls . every ( call => call . method === 'GET' ))
}
})
test ( 'producer consumption waits for exact booking projection; same-number history is never consumed' , async () => {
const { context , producer , historicalId } = conversionContext ()
const actualRequest = context . request
let attempt = 0
const delays = []
context . request = async (... args ) => {
const result = await actualRequest (... args )
if ( ++ attempt === 1 ) result . body . data . summary . sourceContainerBookingId = historicalId
else result . body . data . summary . sourceContainerBookingId = producer . containerBookingId
return result
}
assert . equal ( await awaitProducerConsumption ( context , [ producer ], { attempts : 2 , delay : async ms => delays . push ( ms ) }), true )
assert . deepEqual ( delays , [ 1000 ])
assert . equal ( context . calls . length , 2 )
assert . ok ( context . calls . every ( call => call . method === 'GET' && call . path . endsWith ( `/ ${ producer . containerBookingId } ` )))
const wrong = conversionContext ({ wrongIdentity : true })
assert . equal ( await awaitProducerConsumption ( wrong . context , [ wrong . producer ], { attempts : 1 }), false )
assert . equal ( wrong . context . calls . length , 1 )
})
test ( 'pickup readiness ignores historical same-number rows in either order and cannot borrow their operable state' , async () => {
const { producer , historicalId } = conversionContext ()
const current = { containerNo : producer . containerNo , sourceContainerBookingId : producer . containerBookingId , operable : true }
const historical = { ... current , sourceContainerBookingId : historicalId , operable : false }
for ( const rows of [[ current , historical ], [ historical , current ], [{ ... historical , operable : true }]]) {
const context = candidateContext ([ candidateResponse ( rows )])
assert . equal ( await awaitPickupReadyCandidates ( context , 'offline' , [ producer ], [ producer ], { attempts : 1 }), rows . length === 2 )
assert . equal ( context . calls . length , 1 )
assert . equal ( context . calls [ 0 ]. method , 'GET' )
}
})
2026-10-02 20:14:17 +08:00
function candidateContext ( responses ) {
const calls = [], steps = []
return { calls , steps , createdResources : {}, addStep : ( _module , name , _response , passed )=> steps . push ({ name , passed }),
request : async ( method , path , _body , token )=>{ calls . push ({ method , path , token }); return responses [ Math . min ( calls . length - 1 , responses . length - 1 )]}}
}
const candidateResponse = data => ({ http : 200 , body : { code : '200' , data }, requestId : 'offline-request' , traceId : 'offline-trace' })
const exactCandidate = { containerNo : 'TSTU6403402' , sourceContainerBookingId : '2105838069516079106' , operable : true , totalPackages : 1 }
const exactProducer = { containerNo : exactCandidate . containerNo , containerBookingId : exactCandidate . sourceContainerBookingId }
test ( 'original r2 async shipping gate race waits same booking and container until visible without writing' , async ()=>{
const context = candidateContext ([ candidateResponse ([]), candidateResponse ([]), candidateResponse ([ exactCandidate ])]), delays = []
const result = await awaitReceivingCandidate ( context , 'offline-picker' , exactProducer ,{ attempts : 3 , delay : async ms => delays . push ( ms )})
assert . deepEqual ( result . candidate , exactCandidate )
assert . deepEqual ( delays ,[ 1000 , 1000 ])
assert . equal ( context . calls . length , 3 )
assert . ok ( context . calls . every ( call => call . method === 'GET' && call . token === 'offline-picker' && call . path . endsWith ( '/candidates' )))
const proof = context . createdResources . phase4OverseasPda . candidateWaitEvidence [ 0 ]
assert . equal ( proof . outcome , 'READY' ); assert . deepEqual ( proof . attempts . map ( v => v . exactMatches ),[ 0 , 0 , 1 ])
assert . ok ( proof . attempts . every ( v => v . requestId && v . traceId ))
})
test ( 'same container from another booking, other container and nonoperable original cannot satisfy wait' , async ()=>{
for ( const candidate of [{... exactCandidate , sourceContainerBookingId : '999' },
{... exactCandidate , containerNo : 'ANOTHER' },{... exactCandidate , operable : false }]) {
const context = candidateContext ([ candidateResponse ([ candidate ])]); let waits = 0
const result = await awaitReceivingCandidate ( context , 'offline' , exactProducer ,{ attempts : 2 , delay : async ()=> waits ++ })
assert . equal ( result . candidate , null ); assert . equal ( result . reason , 'TIMED_OUT' )
assert . equal ( context . calls . length , 2 ); assert . equal ( waits , 1 )
}
})
test ( 'ambiguous exact candidates and authentication or malformed replies stop without masking server errors' , async ()=>{
for ( const response of [ candidateResponse ([ exactCandidate , exactCandidate ]),{ http : 401 , body : { code : '401' }}, candidateResponse ({ records : [ exactCandidate ]})]) {
const context = candidateContext ([ response ]); let waits = 0
const result = await awaitReceivingCandidate ( context , 'offline' , exactProducer ,{ attempts : 3 , delay : async ()=> waits ++ })
assert . equal ( result . candidate , null ); assert . equal ( context . calls . length , 1 ); assert . equal ( waits , 0 )
assert . ok ([ 'AMBIGUOUS' , 'QUERY_FAILED' ]. includes ( result . reason ))
}
})
test ( 'candidate waits are per invocation with no cross-mode cache and enforce exact scope/bounds' , async ()=>{
const context = candidateContext ([ candidateResponse ([ exactCandidate ]), candidateResponse ([])])
assert . ok (( await awaitReceivingCandidate ( context , 'first' , exactProducer ,{ attempts : 1 })). candidate )
assert . equal (( await awaitReceivingCandidate ( context , 'second' , exactProducer ,{ attempts : 1 })). candidate , null )
for ( const producer of [{ containerNo : 'TSTU6403402' },{}]) await assert . rejects ( awaitReceivingCandidate ( context , 'offline' , producer ))
await assert . rejects ( awaitReceivingCandidate ( context , 'offline' , exactProducer ,{ attempts : 31 }))
assert . equal ( context . calls . length , 2 )
})
2026-09-30 17:13:11 +08:00
test ( 'single-container continuation retains malformed/unknown probes without requiring unshipped cancelled CHILD; default chain still blocks missing real other container' , async () => {
const requests = [], gaps = [], checks = []
const context = { marker : 'ONE-CANCEL' , createdResources : {},
request : async ( _method , _path , body ) => { requests . push ( body . cartonMark ); return { http : 200 , body : { code : 'A0404' , message : 'Carton mark was not found' }, requestId : 'request' , traceId : 'trace' } },
addStep : ( _module , _name , _response , passed ) => checks . push ( passed ),
addCoverageGap : (... args ) => gaps . push ( args ), assert : () => {} }
const current = { containerNo : 'REAL' , inbounds : [{ inboundCode : 'ONE' , destinationType : 'FBA' }] }
await probeCartonResolveNegatives ( context , 'token' , current , false )
assert . equal ( requests . length , 3 )
assert . ok ( checks . every ( Boolean ))
assert . equal ( gaps . length , 0 )
await probeCartonResolveNegatives ( context , 'token' , current )
assert . equal ( gaps . length , 1 )
assert . match ( gaps [ 0 ][ 3 ], /另一真实柜/ )
assert . match ( source , /probeCartonResolveNegatives\(context, token, current.data, runAuthorizationProbes\)/ )
})
function receivingProofFixture () {
const scope = { customerId : '1' , warehouseId : '2' , bookingId : '3' , containerNo : 'REAL' ,
inboundCodes : [ 'ONE' , 'TWO' ], sourceInboundIds : [ '101' , '102' ], expectedPackagesByInbound : { ONE : 2 , TWO : 2 },
expectedTransitPackages : 4 , completeKey : 'REAL-COMPLETE' , freshKey : 'REAL-FRESH' }
const buckets = { availablePackages : 0 , holdPackages : 0 , allocatedPackages : 0 , pickedPackages : 0 , receivingPackages : 0 }
const balance = ( bid , available , transit ) => ({ id : bid , customerId : '1' , overseasWarehouseId : '2' ,
destinationWarehouseId : '4' , destinationWarehouseCode : 'FBA' , inboundOrderId : bid === '10' ? '101' : '102' ,
inboundCode : bid === '10' ? 'ONE' : 'TWO' , skuCode : 'SAME-SKU' , ... buckets , availablePackages : available , inTransitPackages : transit , procurementPackages : 0 })
const position = ( pid , bid ) => ({ id : pid , balanceId : bid , customerId : '1' , overseasWarehouseId : '2' ,
destinationWarehouseCode : 'FBA' , palletId : '70' , palletNo : 'DP-REAL' , batchNo : 'BATCH' ,
warehouseLocationId : '80' , warehouseLocationCode : 'LOC' , status : 'AVAILABLE' , ... buckets , availablePackages : 2 })
const receipt = ( rid , bid , pid , code , tail , task ) => ({ id : rid , customerId : '1' , planId : '90' , taskId : task ,
allocationId : '91' , lineId : '92' , orderId : tail , inboundCode : code , skuCode : 'SAME-SKU' ,
warehousePalletId : '70' , palletNo : 'DP-REAL' , warehouseLocationId : '80' , warehouseLocationCode : 'LOC' ,
batchNo : 'BATCH' , receivedPackages : 2 , inventoryBalanceId : bid , inventoryPositionId : pid ,
inventoryOperationKey : `PDA_STOCK_IN: ${ task } :91:SAME-SKU` })
const receipts = [ receipt ( '1001' , '10' , '201' , 'ONE' , '101' , '301' ), receipt ( '1002' , '20' , '202' , 'TWO' , '102' , '302' )]
const operations = receipts . map (( row , index ) => ({ id : String ( 400 + index ), customerId : '1' ,
idempotencyKey : row . inventoryOperationKey , operationType : 'STOCK_IN' , requestHash : 'a' . repeat ( 64 ), status : 'COMPLETED' }))
const ledgers = receipts . map (( row , index ) => ({ id : String ( 500 + index ), customerId : '1' , balanceId : row . inventoryBalanceId ,
positionId : row . inventoryPositionId , overseasWarehouseId : '2' , inboundOrderId : row . orderId , inboundCode : row . inboundCode ,
palletId : '70' , palletNo : 'DP-REAL' , batchNo : 'BATCH' , skuCode : 'SAME-SKU' , businessType : 'STOCK_IN' , businessOrderNo : 'TAIL-REAL' ,
sourceEventId : 'EVENT-REAL' , idempotencyKey : ` ${ row . inventoryOperationKey } : ${ operations [ index ]. id } ` ,
statusFrom : 'IN_TRANSIT' , statusTo : 'AVAILABLE' , beforePackages : 0 , deltaPackages : 2 , afterPackages : 2 }))
const before = { balances : [ balance ( '10' , 0 , 2 ), balance ( '20' , 0 , 2 )], positions : [], palletComposition : [],
stockReceipts : [], inventoryOperations : [], stockInLedger : [], receivingOperations : [],
httpGroups : [ balance ( '10' , 0 , 4 )], httpLedger : [] }
const after = { balances : [ balance ( '10' , 2 , 0 ), balance ( '20' , 2 , 0 )], positions : [ position ( '201' , '10' ), position ( '202' , '20' )],
palletComposition : [ position ( '201' , '10' ), position ( '202' , '20' )], stockReceipts : receipts , inventoryOperations : operations , stockInLedger : ledgers ,
receivingOperations : [{ id : '601' , sessionId : '701' , action : 'COMPLETE' , idempotencyKey : 'REAL-COMPLETE' , requestHash : 'b' . repeat ( 64 ) }],
httpGroups : [ balance ( '10' , 4 , 0 )], httpLedger : ledgers }
const stages = { BEFORE_COMPLETE : normalizeReceivingEvidence ( before ), AFTER_COMPLETE : normalizeReceivingEvidence ( after ),
AFTER_SAME_KEY_REPLAY : normalizeReceivingEvidence ( after ), AFTER_FRESH_KEY_REJECTION : normalizeReceivingEvidence ( after ) }
return { scope , before , after , stages }
}
test ( 'receiving completion freezes all raw balances when group MIN represents two tickets on one pallet' , () => {
const { scope , stages } = receivingProofFixture ()
assert . equal ( receivingEvidenceValid ( stages . BEFORE_COMPLETE , scope ), true )
assert . equal ( receivingEvidenceValid ( stages . AFTER_COMPLETE , scope ), true )
const proof = receivingCompletionProof ( scope , stages )
assert . equal ( proof . passed , true , JSON . stringify ( proof ))
assert . equal ( stages . AFTER_COMPLETE . balances . length , 2 )
assert . equal ( stages . AFTER_COMPLETE . httpGroups . length , 1 )
assert . equal ( stages . AFTER_COMPLETE . httpGroups [ 0 ]. availablePackages , 4 )
assert . equal ( stages . AFTER_COMPLETE . positions . length , 2 )
})
2026-10-02 20:14:17 +08:00
test ( 'unrelated canonical UNBOUND stock remains in all snapshots but new receiving still requires a real pallet' , () => {
const { scope , stages } = receivingProofFixture ()
const unboundPosition = { ... stages . AFTER_COMPLETE . positions [ 0 ], id : '301' , balanceId : '30' ,
palletId : '0' , palletNo : 'UNBOUND' , availablePackages : 1 }
for ( const snapshot of Object . values ( stages )) {
const balance = { ... snapshot . balances [ 0 ], id : '30' , inboundOrderId : '30' ,
inboundCode : 'UNBOUND-SOURCE' , skuCode : 'UNRELATED' , availablePackages : 1 , inTransitPackages : 0 }
snapshot . balances . push ( balance )
snapshot . httpGroups . push ({ ... balance })
snapshot . positions . push ({ ... unboundPosition })
snapshot . palletComposition . push ({ ... unboundPosition })
assert . equal ( receivingEvidenceValid ( snapshot , scope ), true )
}
assert . equal ( receivingCompletionProof ( scope , stages ). passed , true )
stages . AFTER_COMPLETE . stockReceipts [ 0 ]. warehousePalletId = '0'
assert . equal ( receivingEvidenceValid ( stages . AFTER_COMPLETE , scope ), false )
const malformed = structuredClone ( stages . BEFORE_COMPLETE )
malformed . positions [ 0 ]. palletNo = 'UNKNOWN'
malformed . palletComposition [ 0 ]. palletNo = 'UNKNOWN'
assert . equal ( receivingEvidenceValid ( malformed , scope ), false )
})
2026-09-30 17:13:11 +08:00
test ( 'receiving proof rejects missing raw source, wrong customer/warehouse, prefix source, missing bucket, ledger key, duplicate receipt and operation mismatch' , () => {
const mutations = [
row => row . balances . pop (), row => row . positions [ 0 ]. customerId = 'OTHER' ,
row => row . balances [ 0 ]. overseasWarehouseId = 'OTHER' , row => row . stockReceipts [ 0 ]. inboundCode = 'ONE-CHILD' ,
row => row . balances [ 0 ]. holdPackages = null , row => row . stockInLedger [ 0 ]. idempotencyKey = row . inventoryOperations [ 0 ]. idempotencyKey ,
row => row . stockReceipts . push ({ ... row . stockReceipts [ 0 ] }), row => row . inventoryOperations [ 0 ]. status = 'PROCESSING' ,
row => row . palletComposition [ 0 ]. availablePackages += 1 , row => row . stockInLedger [ 0 ]. deltaPackages += 1 ,
row => row . receivingOperations [ 0 ]. idempotencyKey = 'ANOTHER' ,
]
for ( const mutate of mutations ) {
const { scope , after , stages } = receivingProofFixture ()
mutate ( after ); stages . AFTER_COMPLETE = normalizeReceivingEvidence ( after )
assert . equal ( receivingCompletionProof ( scope , stages ). passed , false )
}
})
test ( 'same-key replay and fresh-key rejection must preserve every bucket, position, pallet and immutable row' , () => {
for ( const stage of [ 'AFTER_SAME_KEY_REPLAY' , 'AFTER_FRESH_KEY_REJECTION' ]) {
for ( const mutate of [ row => row . balances [ 0 ]. availablePackages ++ , row => row . stockReceipts . pop (),
row => row . receivingOperations . push ({ ... row . receivingOperations [ 0 ], id : '999' }),
row => row . stockInLedger [ 0 ]. sourceEventId = 'OTHER' ]) {
const { scope , stages } = receivingProofFixture (); mutate ( stages [ stage ])
assert . equal ( receivingCompletionProof ( scope , stages ). passed , false )
}
}
})
test ( 'normalization is deterministic, preserves zero buckets, and excludes token/operator/provider fields' , () => {
const { after } = receivingProofFixture ()
after . token = 'SECRET' ; after . stockReceipts [ 0 ]. password = 'SECRET' ; after . receivingOperations [ 0 ]. operatorKey = 'PERSON'
const normal = normalizeReceivingEvidence ( after )
const reversed = { ... after , balances : [... after . balances ]. reverse (), stockReceipts : [... after . stockReceipts ]. reverse () }
assert . deepEqual ( normalizeReceivingEvidence ( reversed ), normal )
assert . equal ( normal . balances [ 0 ]. procurementPackages , 0 )
assert . doesNotMatch ( JSON . stringify ( normal ), /SECRET|PERSON|password|operatorKey|token/ )
assert . equal ( normalizeReceivingEvidence ({ ... after , stockReceipts : undefined }), null )
})
2026-10-02 20:14:17 +08:00
test ( 'reverse event recovery requires complete owned inventory evidence and every immutable fact unchanged' , () => {
const { scope , stages } = receivingProofFixture ()
const before = stages . AFTER_FRESH_KEY_REJECTION
assert . equal ( receivingEventRecoveryInventoryUnchanged ( scope , before , structuredClone ( before )), true )
assert . equal ( receivingEventRecoveryInventoryUnchanged ( scope , before , null ), false )
assert . equal ( receivingEventRecoveryInventoryUnchanged ({ ... scope , customerId : 'OTHER' }, before , before ), false )
for ( const mutate of [ row => row . balances [ 0 ]. availablePackages ++ , row => row . stockReceipts . pop (),
row => row . positions [ 0 ]. warehouseLocationCode = 'ANOTHER' , row => row . stockInLedger [ 0 ]. sourceEventId = 'OTHER' ,
row => row . receivingOperations . push ({ ... row . receivingOperations [ 0 ], id : '999' })]) {
const changed = structuredClone ( before )
mutate ( changed )
assert . equal ( receivingEventRecoveryInventoryUnchanged ( scope , before , changed ), false )
}
})
2026-09-30 17:13:11 +08:00
test ( 'missing read-only receipt/key reader is a blocker and retains an unproven boundary without inventing HTTP' , async () => {
const checks = [], target = { stages : {} }
const context = { context : {}, addCoverageGap () {}, assert ( m , n , r , passed ) { checks . push ( passed ) },
async request () { assert . fail ( 'missing provider cannot pretend public DTO supplies raw keys' ) } }
const { scope } = receivingProofFixture ()
assert . equal ( await captureReceivingEvidence ( context , scope , 'BEFORE_COMPLETE' , target ), null )
assert . deepEqual ( checks , [ false ]); assert . equal ( target . stages . BEFORE_COMPLETE . captured , false )
})
test ( 'capture independently reads complete unfiltered customer warehouse pages and saves only sanitized live facts' , async () => {
const { scope , after } = receivingProofFixture (); const calls = [], checks = []
const context = { context : { async phase4ReceivingEvidenceReader ( actual ) {
assert . equal ( actual . stage , 'AFTER_COMPLETE' ); assert . deepEqual ( actual . inboundCodes , [ 'ONE' , 'TWO' ])
return { ... after , token : 'SECRET' }
} }, async request ( method , requestPath ) {
calls . push ( requestPath )
const data = requestPath . includes ( '/balances?' ) ? after . httpGroups : after . httpLedger
return { http : 200 , body : { code : '200' , data : { records : data , total : data . length } } }
}, addStep ( m , n , r , passed ) { checks . push ( passed ) }, assert ( m , n , r , passed ) { checks . push ( passed ) }, addCoverageGap () { assert . fail ( 'valid fixture must not gap' ) } }
const target = { stages : {} }
const snapshot = await captureReceivingEvidence ( context , scope , 'AFTER_COMPLETE' , target )
assert . equal ( receivingEvidenceValid ( snapshot , scope ), true ); assert . ok ( checks . every ( Boolean ))
assert . ok ( calls . every ( requestPath => requestPath . includes ( 'customerId=1' ) && requestPath . includes ( 'overseasWarehouseId=2' ) && ! requestPath . includes ( 'inboundCode=' )))
assert . doesNotMatch ( JSON . stringify ( target ), /SECRET/ )
})
2026-09-30 15:16:39 +08:00
test ( 'same-root FBA cross-container rejection uses explicit real receiving prefix while preserving original physical box code' ,() => {
const current = { containerNo : 'ROOT-CABINET' , inbounds : [{ inboundCode : 'ROOT' }]}
const child = { containerNo : 'CHILD-CABINET' , containerBookingId : '20' , sourceOrderId : '101' , sourceInboundId : '201' ,
sourceStatus : 'LOADED' , inboundCode : 'CHILD' , destinationType : 'FBA' , fulfillmentType : 'STANDARD' ,
cartonMarks : [ 'ROOT#2-1' ], receivingCartonMarks : [ 'CHILD#PDA-CONTRACT-1' ]}
const before = JSON . stringify ( child ), probe = crossContainerCartonProbe ([ child ], current )
assert . equal ( probe . cartonMark , 'CHILD#PDA-CONTRACT-1' )
assert . equal ( probe . mode , 'FBA_RECEIVING_PREFIX' ); assert . equal ( probe . sourceOrderId , '101' )
assert . equal ( JSON . stringify ( child ), before )
for ( const fault of [
{... child , receivingCartonMarks : undefined }, {... child , receivingCartonMarks : [ 'ROOT#PDA-CONTRACT-1' ]},
{... child , receivingCartonMarks : [ 'CHILD#PDA-CONTRACT-' ]}, {... child , receivingCartonMarks : [ 'CHILD#PDA-CONTRACT-0' ]},
{... child , containerNo : 'ROOT-CABINET' }, {... child , sourceOrderId : null }, {... child , sourceInboundId : null },
{... child , containerBookingId : null }, {... child , sourceStatus : 'WAIT_SPLIT' }, {... child , inboundCode : 'ROOT' },
]) assert . equal ( crossContainerCartonProbe ([ fault ], current ), null )
assert . equal ( crossContainerCartonProbe ([], current ), null )
assert . equal ( crossContainerCartonProbe ([ child ],{ containerNo : 'ROOT-CABINET' , inbounds : []}), null )
})
test ( 'physical OVERSEAS and dropship probes still require another real loaded producer and keep the exact sealed code' ,() => {
const other = { containerNo : 'OTHER' , containerBookingId : '20' , sourceOrderId : '101' , sourceInboundId : '201' ,
sourceStatus : 'LOADED' , inboundCode : 'OTHER-SOURCE' , destinationType : 'OVERSEAS' , fulfillmentType : 'STANDARD' ,
cartonMarks : [ 'ORIGINAL-PHYSICAL#2-1' ]}
const current = { containerNo : 'CURRENT' , inbounds : [{ inboundCode : 'CURRENT-SOURCE' }]}
assert . equal ( crossContainerCartonProbe ([ other ], current ). cartonMark , 'ORIGINAL-PHYSICAL#2-1' )
assert . equal ( crossContainerCartonProbe ([ other ], current ). mode , 'PHYSICAL_CARTON' )
assert . equal ( crossContainerCartonProbe ([{... other , sourceInboundId : null }], current ), null )
})
2026-09-14 12:40:40 +08:00
2026-09-20 19:45:24 +08:00
test ( 'receiving selects one current booking and supplies mandatory booking identity' , () => {
2026-10-02 20:14:17 +08:00
assert . match ( source , /item.containerNo === containerNo && id\(item.sourceContainerBookingId\) === bookingId/ )
2026-09-20 19:45:24 +08:00
assert . match ( source , /matchingCandidates.length === 1/ )
assert . match ( source , /if \(!longId\(sourceContainerBookingId\)\)/ )
assert . match ( source , /sourceContainerBookingId=\$\{encodeURIComponent\(sourceContainerBookingId\)\}/ )
assert . equal (( source . match ( /\/prepare\$\{bookingQuery\}/g ) || []). length , 3 )
})
2026-09-14 12:40:40 +08:00
test ( 'one physical container is received once even when matrix roles overlap' , () => {
const fba = { containerNo : 'TEST-C1' , fulfillmentType : 'STANDARD' }
const overseas = { containerNo : 'TEST-C2' }
assert . deepEqual ([ null , fba , { ... fba }, overseas ]. filter ( uniqueContainerProducer ), [ fba , overseas ])
assert . doesNotMatch ( source , /dropship \|\| matrix/ )
})
2026-08-24 15:38:28 +08:00
const here = path . dirname ( fileURLToPath ( import . meta . url ))
const source = fs . readFileSync ( path . join ( here , 'phase4-overseas-pda.mjs' ), 'utf8' )
2026-09-09 13:20:11 +08:00
test ( 'dropship original counted cartons must resolve exactly before any receiving or inventory checks' , async () => {
const inbounds = [{ inboundCode : 'REAL-INBOUND' , destinationType : 'OVERSEAS' , totalPackages : 2 }]
const producer = { fulfillmentType : 'ONE_CLICK_DROPSHIP' , inboundCode : 'REAL-INBOUND' , destinationType : 'OVERSEAS' ,
sealedCartonMarks : [ 'REAL-INBOUND#2-2' , 'REAL-INBOUND#2-1' ] }
for ( const fault of [ null , 'missing-carton' , 'wrong-carton' , 'wrong-inbound' , 'wrong-destination' ]) {
const calls = []; const checks = []
const context = {
2026-09-29 19:48:34 +08:00
async request ( method , requestPath , body ) {
2026-09-09 13:20:11 +08:00
calls . push ({ method , requestPath , body })
const data = { cartonMark : body . cartonMark , inboundCode : 'REAL-INBOUND' , destinationType : 'OVERSEAS' }
if ( fault === 'wrong-carton' ) data . cartonMark = 'ANOTHER-CARTON'
if ( fault === 'wrong-inbound' ) data . inboundCode = 'ANOTHER-INBOUND'
if ( fault === 'wrong-destination' ) data . destinationType = 'FBA'
return { http : 200 , body : { code : fault === 'missing-carton' ? 'A0404' : '200' ,
message : 'Carton mark was not found' , data }, requestId : 'request' , traceId : 'trace' }
},
assert ( module , name , response , passed ) { checks . push ({ name , passed }) },
addStep ( module , name , response , passed ) { checks . push ({ name , passed }) },
addCoverageGap () {},
}
const result = await resolveCartonFixtures ( context , 'token' , inbounds , [ 'OLD-CANDIDATE#20-1' ], producer )
assert . ok ( calls . every ( call => call . method === 'POST' && call . requestPath . endsWith ( '/current/cartons/resolve' )))
assert . ok ( calls . every ( call => producer . sealedCartonMarks . includes ( call . body . cartonMark )))
if ( fault ) {
assert . equal ( result , null , fault )
assert . ok ( checks . some ( check => ! check . passed ), fault )
assert . ok ( ! checks . some ( check => check . name . includes ( '排除未实装箱码候选' )), fault )
} else {
assert . deepEqual ( result . map ( item => item . cartonMark ), producer . sealedCartonMarks )
assert . equal ( calls . length , 2 )
assert . ok ( checks . every ( check => check . passed ))
}
}
for ( const sealedCartonMarks of [ undefined , [], [ 'REAL-INBOUND#2-1' ], [ 'REAL-INBOUND#2-1' , 'REAL-INBOUND#2-1' ]]) {
const checks = []
const result = await resolveCartonFixtures ({ assert ( m , n , r , passed ) { checks . push ( passed ) },
2026-09-29 19:48:34 +08:00
async request () { assert . fail ( 'incomplete physical fixture must not issue HTTP' ) } },
2026-09-09 13:20:11 +08:00
'token' , inbounds , producer . sealedCartonMarks , { ... producer , sealedCartonMarks })
assert . equal ( result , null )
assert . deepEqual ( checks , [ false ])
}
})
test ( 'current loading event binds each pallet carton group to an explicit source detail' , () => {
const model = fs . readFileSync ( path . join ( here , '../../common-event-contract/src/main/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2.java' ), 'utf8' )
const producer = fs . readFileSync ( path . join ( here , '../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java' ), 'utf8' )
assert . match ( model , /record SkuPackageSnapshot\(String sourceInboundDetailId, String skuCode/ )
assert . match ( model , /sourceInboundDetailId == null \|\| sourceInboundDetailId\.isBlank\(\)/ )
assert . match ( producer , /SkuPackageSnapshot\(detail\.getSourceInboundDetailId\(\)\.toString\(\), detail\.getSkuCode\(\)/ )
2026-09-15 14:30:47 +08:00
assert . match ( producer , /groupingBy\(PalletRow::getSourceInboundDetailId/ )
assert . match ( producer , /map\(PalletRow::getActualSkuQuantity\)/ )
assert . match ( producer , /cartonQuantities\(detailRows\.getValue\(\), marks\)/ )
2026-09-09 13:20:11 +08:00
const measurement = fs . readFileSync ( path . join ( here , 'inbound-measurement.mjs' ), 'utf8' )
const loading = fs . readFileSync ( path . join ( here , 'phase3-loading.mjs' ), 'utf8' )
const finalize = fs . readFileSync ( path . join ( here , 'phase3-loading-finalize.mjs' ), 'utf8' )
assert . match ( measurement , /sealedCartonMarks\.push\(resolved\.boxCode\)/ )
assert . match ( loading , /phase3Loading\.cartonMarks = marks/ )
assert . match ( finalize , /sealedCartonMarks: Array\.isArray\(fixture\.sealedCartonMarks\)/ )
const entry = fs . readFileSync ( path . join ( here , '../admin-test-env-acceptance.mjs' ), 'utf8' )
assert . match ( entry , /sealedCartonMarks: Array\.isArray\(fixture\.sealedCartonMarks\)/ )
assert . match ( entry , /runPhase4ProducerVariant\('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true\)/ )
assert . match ( source , /resolveCartonFixtures\(context, token, current\.data\.inbounds, producerCartonMarks, preferredProducer\)/ )
assert . ok ( source . indexOf ( 'const cartonFixtures = await resolveCartonFixtures' ) < source . indexOf ( 'const completed = await pda' ))
})
2026-08-24 19:58:35 +08:00
test ( 'PDA module is injected, token-scoped, traceable, and contains no direct network or secret source' , () => {
2026-08-24 15:38:28 +08:00
assert . match ( source , /export async function run\(context\)/ )
2026-09-29 19:48:34 +08:00
assert . match ( source , /request\(/ )
assert . match ( source , /requestMultipart\(/ )
2026-08-24 15:38:28 +08:00
assert . match ( source , /requestId=/ )
assert . match ( source , /traceId=/ )
2026-08-24 19:58:35 +08:00
assert . doesNotMatch ( source , /https?:\/\// )
assert . doesNotMatch ( source , /fetch\(|process\.env/ )
2026-09-30 17:13:11 +08:00
assert . doesNotMatch ( source , /planNo/ )
// Allocation identity is read-only evidence; PDA command URLs/bodies remain container-scoped.
assert . doesNotMatch ( source , /\/allocations\// )
2026-08-24 15:38:28 +08:00
})
2026-08-24 19:58:35 +08:00
test ( 'PDA module covers the container-number receiving contract and excludes outbound domains' , () => {
for ( const token of [ '/candidates' , '/prepare' , '/current/unload-photos/files' , '/current/sign' ,
2026-08-26 17:27:09 +08:00
'/current/cartons/resolve' , '/current/pallets/resolve' ,
2026-09-09 18:33:17 +08:00
'/current/receipts/bulk' , '/current/receipts/carton' , '/current/receipts/pallet' , '/current/complete' ]) assert . ok ( source . includes ( token ), token )
2026-08-25 10:50:58 +08:00
for ( const token of [ '/inventory/resolve' , '/locations/resolve' , '/pallets/resolve' , '/operations?page=1&size=50' ]) {
assert . ok ( source . includes ( token ), token )
}
2026-08-24 19:58:35 +08:00
for ( const role of [ 'LICENSE_PLATE' , 'CONTAINER_SEAL' , 'DOOR_OPEN' ]) assert . ok ( source . includes ( role ), role )
for ( const forbidden of [ '/outbound' , '/dispatch' , '/delivery' , '/internal/' ]) assert . equal ( source . includes ( forbidden ), false , forbidden )
assert . match ( source , /同键同载荷重放/ )
2026-09-09 00:53:40 +08:00
assert . match ( source , /inbound-plans\/container-preview/ )
2026-09-18 14:14:10 +08:00
assert . match ( source , /orders\/container-arrival-times\/query/ )
assert . match ( source , /container-summaries\/\$\{encodeURIComponent\(bookingId\)\}\/expected-arrival/ )
assert . match ( source , /estimatedWarehouseArrivalAt, expectedVersion: Number\(arrivalTime\.version\)/ )
2026-09-09 00:53:40 +08:00
assert . match ( source , /snapshotVersion: Number\(preview\.snapshotVersion\)/ )
assert . match ( source , /snapshotHash: String\(preview\.snapshotHash\)/ )
2026-09-09 03:00:48 +08:00
assert . match ( source , /rememberGeneratedPlans\(context, plans\)/ )
assert . match ( source , /resolveGeneratedPallet\(context, inbound\)/ )
2026-08-24 19:58:35 +08:00
assert . match ( source , /同键异载荷冲突/ )
assert . match ( source , /跨柜切换锁返回409/ )
2026-09-02 03:32:35 +08:00
assert . match ( source , /item\.operable === true && item\.allPlansReady === true/ )
2026-08-24 21:00:22 +08:00
assert . match ( source , /第二仓库操作员抢占同柜返回409/ )
2026-08-28 10:38:58 +08:00
assert . match ( source , /OVERSEAS_WAREHOUSE_PDA_OPERATOR/ )
2026-08-24 21:00:22 +08:00
assert . match ( source , /lastleg:pda:operate/ )
2026-08-25 17:25:38 +08:00
assert . match ( source , /安全投影已收敛/ )
assert . match ( source , /projectionReady = success\(projectionResponse\) \|\| notFound\(projectionResponse\)/ )
assert . match ( source , /setTimeout\(resolve, 500\)/ )
2026-09-11 00:24:35 +08:00
assert . match ( source , /重复累计不产生超收/ )
2026-08-24 19:58:35 +08:00
assert . match ( source , /重复箱新键被拒绝/ )
2026-08-25 09:27:45 +08:00
assert . match ( source , /key\(context, `fba-bulk-\$\{index\}`\)/ )
2026-09-11 00:24:35 +08:00
assert . match ( source , /inbounds\.reduce\(\(total, inbound\) => total \+ Number\(inbound\.totalPackages \|\| 0\), 0\)/ )
assert . match ( source , /resolvedInboundCodes\.includes\(scannedInboundCode\)/ )
assert . match ( source , /duplicate\.data\?\.result === 'ALREADY_RECEIVED'/ )
2026-08-26 17:27:09 +08:00
assert . match ( source , /if \(packageCount <= 0\) return true/ )
2026-08-25 09:27:45 +08:00
assert . match ( source , /key\(context, `overseas-carton-\$\{index\}`\)/ )
2026-08-24 19:58:35 +08:00
assert . match ( source , /模式硬拒绝/ )
assert . match ( source , /current返回404正常空态/ )
2026-08-24 21:46:19 +08:00
assert . match ( source , /安全扫描返回A0400/ )
2026-08-26 15:24:48 +08:00
assert . match ( source , /warehouse-admin\?tenantId=0&.*capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE/ )
assert . match ( source , /193000000000200003.*193000000000200004/ )
2026-09-29 19:48:34 +08:00
assert . doesNotMatch ( source , /request\('POST', '\/api\/platform\/v1\/warehouse-admin'/ )
assert . doesNotMatch ( source , /request\('GET', '\/api\/platform\/v1\/warehouse-admin'/ )
2026-08-26 20:39:03 +08:00
assert . match ( source , /receiving-containers\/current\/pallets\/resolve/ )
2026-08-24 21:46:19 +08:00
assert . match ( source , /overseasWarehouseId, password/ )
assert . match ( source , /inbound\?\.overseasWarehouseId/ )
assert . doesNotMatch ( source , /const warehouseId = id\(inbound\?\.destinationWarehouseId\)/ )
2026-09-09 02:54:09 +08:00
assert . match ( source , /禁止在PDA收货时临时新建脱离计划的托盘/ )
assert . match ( source , /rememberGeneratedPlans\(context, plans\)/ )
assert . match ( source , /resolveGeneratedPallet\(context, inbound\)/ )
assert . match ( source , /generatedPalletsByInbound/ )
2026-09-23 23:01:35 +08:00
assert . match ( source , /standardFbaProducers = \(Array\.isArray\(matrix\?\.containers\) \? matrix\.containers : \[\]\)/ )
2026-09-09 19:16:18 +08:00
assert . match ( source , /item\?\.fulfillmentType !== 'ONE_CLICK_DROPSHIP'/ )
2026-09-24 05:08:46 +08:00
assert . match ( source , /FBA-OPERATIONS/ )
2026-10-02 20:14:17 +08:00
assert . match ( source , /FBA-ROUTE/ )
assert . match ( source , /\.slice\(0, 4\)/ )
2026-09-15 13:01:44 +08:00
assert . match ( source , /const producers = \[dropship, \.\.\.standardFbaProducers,/ )
2026-09-09 02:54:09 +08:00
assert . doesNotMatch ( source , /创建当前柜实收托盘|创建当前柜实收库位|async function createPallet/ )
2026-08-24 19:58:35 +08:00
assert . match ( source , /retained as acceptance evidence/ )
2026-08-24 23:28:24 +08:00
assert . match ( source , /preferredProducer\?\.cartonMarks\?\.length/ )
2026-08-25 17:54:50 +08:00
assert . match ( source , /const cartonCandidates =/ )
2026-08-26 17:27:09 +08:00
assert . doesNotMatch ( source , /current\/skus\/resolve/ )
assert . doesNotMatch ( source , /skuCode:/ )
2026-09-02 01:47:00 +08:00
assert . match ( source , /FBA箱码与OVERSEAS真实manifest箱码必须HTTP 200\/code 200并返回当前柜唯一入仓码/ )
assert . match ( source , /排除未实装箱码候选/ )
2026-08-26 17:27:09 +08:00
assert . match ( source , /const matches = inbounds\.filter/ )
2026-08-26 17:29:07 +08:00
assert . match ( source , /!Array\.isArray\(resolved\?\.inboundCode\)/ )
2026-08-26 17:50:56 +08:00
assert . match ( source , /matches\.length === 1/ )
2026-08-26 17:29:07 +08:00
assert . match ( source , /destinations\.size === 1/ )
2026-08-26 17:50:56 +08:00
assert . doesNotMatch ( source , /\.split\([^\n]*['"]#['"]/ )
assert . match ( source , /`\$\{item\.inboundCode\}#PDA-CONTRACT-\$\{index \+ 1\}`/ )
for ( const negative of [ '无#箱码被服务端拒绝' , '空#前缀箱码被服务端拒绝' ,
'未命中入仓码前缀被服务端拒绝' , '其他柜箱码前缀被当前锁定柜拒绝' ,
'OVERSEAS缺少真实manifest行时不得按标准箱码推导' ]) {
assert . ok ( source . includes ( negative ), negative )
}
assert . doesNotMatch ( source , /FBA标准箱码序号超范围被拒绝/ )
assert . match ( source , /FBA与OVERSEAS箱码resolve均为纯查询且零状态写入/ )
assert . match ( source , /receivingSnapshot\(afterResolve\.data\) === beforeResolve/ )
2026-08-26 17:27:09 +08:00
assert . match ( source , /const fixturesConserved = overseasFixtures\.length === expectedPending/ )
2026-08-25 17:54:50 +08:00
assert . match ( source , /OVERSEAS实装箱唛与待收箱数守恒/ )
2026-08-26 17:27:09 +08:00
assert . match ( source , /for \(const \[index, fixture\] of overseasFixtures\.entries\(\)\)/ )
2026-08-25 17:54:50 +08:00
assert . doesNotMatch ( source , /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/ )
2026-08-24 23:35:26 +08:00
assert . match ( source , /last-leg\/orders\?page=1&size=200&containerNo=/ )
assert . match ( source , /phase4OverseasPda\.orderId = id\(order\.id\)/ )
assert . match ( source , /phase4OverseasPda\.customerId = id\(order\.customerId\)/ )
2026-08-25 10:50:58 +08:00
assert . match ( source , /现场库存查询只返回Token绑定仓且包含库存位置/ )
assert . match ( source , /作业历史包含本轮柜号收货动作且分页稳定/ )
2026-08-25 15:27:26 +08:00
assert . match ( source , /awaitProducerConsumption\(context, allProducers\)/ )
assert . match ( source , /last-leg\/orders\?page=1&size=200&containerNo=/ )
2026-08-26 17:27:09 +08:00
assert . match ( source , /跨区广播消费观察/ )
2026-08-26 17:29:07 +08:00
assert . match ( source , /跨区广播消费观察 \$\{containerNo\}`,[\s\S]*?response,[\s\S]*?true,/ )
2026-08-26 17:27:09 +08:00
assert . match ( source , /OBSERVATION_OPTIONS = \{ scenario: SCENARIO, deploymentBlocker: false \}/ )
assert . doesNotMatch ( source , /blockerGap\(context, '跨区消费生产矩阵'/ )
2026-08-25 15:27:26 +08:00
assert . match ( source , /awaitPickupReadyCandidates\(context, primary\.token, producers, allProducers\)/ )
assert . match ( source , /missingPickupReady=/ )
assert . match ( source , /item\?\.shippingReady === false/ )
assert . match ( source , /!expectedSet\.has\(id\(item\?\.containerNo\)\)/ )
assert . match ( source , /unexpectedAwaiting=/ )
2026-09-02 15:25:30 +08:00
assert . match ( source , /candidatesByContainer\.get\(containerNo\)\?\.operable === true/ )
assert . match ( source , /candidatesByContainer\.get\(containerNo\)\?\.operable !== true/ )
2026-08-25 15:27:26 +08:00
assert . match ( source , /missingPickupReady\.length === 0 && unexpectedAwaiting\.length === 0/ )
assert . doesNotMatch ( source , /shippingReady == null|shippingReady !== true/ )
2026-09-02 03:32:35 +08:00
assert . match ( source , /nonBlank\(item\?\.resourceKey\)/ )
assert . match ( source , /item\?\.shippingReady !== false/ )
2026-08-25 15:27:26 +08:00
assert . match ( source , /unexpectedHistorical=/ )
assert . doesNotMatch ( source , /awaitProducerCandidates/ )
2026-08-24 19:58:35 +08:00
})
2026-09-02 01:47:00 +08:00
test ( 'OVERSEAS carton discovery probes the frozen sequence and lets the server exclude skipped cartons' , () => {
assert . deepEqual ( expandOverseasCartonCandidates ([
{ inboundCode : 'NYEXX2609020042' , destinationType : 'OVERSEAS' },
{ inboundCode : 'FBA2609020001' , destinationType : 'FBA' },
], [ 'NYEXX2609020042#8-5' , 'NYEXX2609020042#8-2' , 'NYEXX2609020042#8-1' ]), [
'NYEXX2609020042#8-1' , 'NYEXX2609020042#8-2' , 'NYEXX2609020042#8-3' , 'NYEXX2609020042#8-4' ,
'NYEXX2609020042#8-5' , 'NYEXX2609020042#8-6' , 'NYEXX2609020042#8-7' , 'NYEXX2609020042#8-8' ,
])
assert . deepEqual ( expandOverseasCartonCandidates ([
{ inboundCode : 'NYEXX2609020042' , destinationType : 'OVERSEAS' },
], [ 'NYEXX2609020042#2001-1' ]), [])
})
2026-08-26 17:27:09 +08:00
test ( 'real-device legacy FBA carton rejection is retained as regression evidence' , () => {
const evidence = {
http : 200 ,
code : 'A0409' ,
message : 'FBA destination must use bulk count' ,
requestId : '5e925a3d-8184-41e1-b0d2-f4e1aae86639' ,
traceId : '62fdf603b65fd7f43ec452bb8a6ca185' ,
}
assert . deepEqual ( evidence , {
http : 200 ,
code : 'A0409' ,
message : 'FBA destination must use bulk count' ,
requestId : '5e925a3d-8184-41e1-b0d2-f4e1aae86639' ,
traceId : '62fdf603b65fd7f43ec452bb8a6ca185' ,
})
assert . match ( source , /a0409Message\(response, \/\^FBA destination must use bulk count\$\// )
assert . match ( source , /a0409Message\(response, \/\^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden\$\// )
})
2026-08-26 17:50:56 +08:00
test ( 'real-device deployed FBA resolve A0404 is retained until server-side inbound-prefix matching lands' , () => {
const evidence = {
http : 200 ,
code : 'A0404' ,
message : 'Carton mark was not found' ,
requestId : 'a716e2c2-dc10-4d07-8014-7438afb63090' ,
traceId : '6ab8fcfcd812056ad6570d3b1652157b' ,
databaseFact : 'current FBA has no ll_inbound_plan_carton manifest' ,
}
assert . equal ( evidence . code , 'A0404' )
assert . equal ( evidence . message , 'Carton mark was not found' )
assert . match ( evidence . databaseFact , /no ll_inbound_plan_carton manifest/ )
assert . match ( source , /a0404Message\(response, \/\^Carton mark was not found\$\// )
})
2026-08-26 15:58:54 +08:00
test ( 'PDA receiving lifecycle is ordered from candidate claim through photos, mode-specific receipts, pallet and complete' , () => {
const orderedTokens = [
2026-10-02 20:14:17 +08:00
'const candidates = await awaitReceivingCandidate' , "'选择柜号并建立WAIT_SIGN会话'" ,
2026-08-26 15:58:54 +08:00
'uploadPhotos(context, token)' , "'绑定车牌封签开柜三角色照片并签收'" ,
2026-09-09 02:54:09 +08:00
'resolveGeneratedPallet(context, inbound)' , "`解析${inbound.destinationType}实物托盘`" , 'receiveFba(context, token' ,
2026-08-26 15:58:54 +08:00
'receiveOverseas(context, token' , "'回查柜内逐入仓码实收进度'" ,
"'完成当前柜并写入库存与卸柜事件'" , "'完成后current返回404正常空态'" ,
]
let cursor = - 1
for ( const token of orderedTokens ) {
const next = source . indexOf ( token , cursor + 1 )
assert . ok ( next > cursor , ` ${ orderedTokens [ Math . max ( 0 , orderedTokens . indexOf ( token ) - 1 )] } -> ${ token } ` )
cursor = next
}
for ( const token of [
'/current/receipts/bulk' , '/current/receipts/carton' ,
"'FBA模式硬拒绝逐箱收货'" , "'OVERSEAS模式硬拒绝批量收货'" ,
'PHOTO_ROLES.every' , "finalStatus === 'COMPLETED'" , 'completedContainers.push' ,
2026-09-09 03:00:48 +08:00
'completedContainerFacts.push' , 'expectedPalletCost' , 'physicalPallets' , 'unitPrice' ,
2026-08-26 15:58:54 +08:00
]) assert . ok ( source . includes ( token ), token )
})
2026-08-28 10:38:58 +08:00
test ( 'missing OVERSEAS_WAREHOUSE_PDA_OPERATOR overseas authority is a deployment blocker' , async () => {
2026-08-24 15:38:28 +08:00
const calls = []; const gaps = []; const steps = []
const context = {
2026-08-24 19:58:35 +08:00
marker : 'PDA-CONTRACT' , context : {}, createdResources : {},
2026-09-29 19:48:34 +08:00
async request (... args ) {
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/ . test ( String ( args [ 1 ]));
const handler = route ? async function (... args ) { calls . push ( args ); throw new Error ( 'must not request without credentials' ) } : async function ( method , requestPath ) {
2026-08-24 21:00:22 +08:00
calls . push ({ method , requestPath })
return { method , path : requestPath , http : 200 , body : { code : '200' , data : [] },
requestId : 'request-contract-1' , traceId : 'trace-contract-0001' }
2026-09-29 19:48:34 +08:00
};
return handler . apply ( this , args );
},
async requestMultipart (... args ) { calls . push ( args ); throw new Error ( 'must not request without credentials' ) },
2026-08-24 21:00:22 +08:00
apiSuccess ( module , name , response ) { return response . body . data },
2026-08-24 15:38:28 +08:00
addStep (... args ) { steps . push ( args ) },
2026-08-24 19:58:35 +08:00
assert ( module , name , response , passed , evidence , options ) { steps . push ({ module , name , passed , evidence , options }) },
2026-08-24 15:38:28 +08:00
addCoverageGap (... args ) { gaps . push ( args ) },
}
await run ( context )
2026-08-24 21:00:22 +08:00
assert . ok ( calls . some (( call ) => call . requestPath === '/api/platform/v1/iam/roles' ))
assert . ok ( calls . some (( call ) => call . requestPath === '/api/platform/v1/iam/permissions' ))
assert . ok ( gaps . some (( gap ) => gap [ 2 ] === '海外PDA仓库操作员权限' ))
assert . ok ( steps . some (( step ) => step ? . passed === false && step ? . options ? . deploymentBlocker ))
2026-08-24 19:58:35 +08:00
})
2026-10-02 20:14:17 +08:00
test ( 'missing original producer identity blocks before candidate lookup and still cleans temporary identities' , async () => {
2026-08-24 19:58:35 +08:00
const calls = []; const gaps = []; const steps = []
const response = ( data , requestPath , method = 'GET' ) => ({ method , path : requestPath , http : 200 ,
body : { code : '200' , data }, requestId : 'req-contract' , traceId : 'trace-contract' })
2026-08-24 21:00:22 +08:00
let userSequence = 0
const roleId = '4000005' ; const permissionId = '5107015'
2026-08-24 19:58:35 +08:00
const context = {
2026-09-29 19:48:34 +08:00
marker : 'PDA-CONTRACT' , token : '' , createdResources : {}, context : {},
2026-08-24 21:00:22 +08:00
async findAvailablePhone () { userSequence += 1 ; return `1750000000 ${ userSequence } ` },
2026-09-29 19:48:34 +08:00
async request (... args ) {
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/ . test ( String ( args [ 1 ]));
const handler = route ? async function ( method , requestPath , body , token , headers ) {
calls . push ({ channel : 'overseas' , method , requestPath })
if ( requestPath . endsWith ( '/current' )) return { method , path : requestPath , http : 200 ,
body : { code : 'A0404' , message : 'not found' }, requestId : 'req-current' , traceId : 'trace-current' }
return response ([], requestPath , method )
} : async function ( method , requestPath , body ) {
2026-08-24 19:58:35 +08:00
calls . push ({ channel : 'domestic' , method , requestPath })
2026-08-24 21:00:22 +08:00
if ( method === 'GET' && requestPath . endsWith ( '/roles' )) return response ([
2026-08-28 10:38:58 +08:00
{ id : roleId , roleCode : 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' , status : 'ACTIVE' , permissionIds : [ permissionId ] },
2026-08-24 21:00:22 +08:00
], requestPath , method )
if ( method === 'GET' && requestPath . endsWith ( '/permissions' )) return response ([
{ id : permissionId , permissionCode : 'lastleg:pda:operate' },
], requestPath , method )
2026-08-26 15:24:48 +08:00
if ( method === 'GET' && requestPath . startsWith ( '/api/platform/v1/warehouse-admin?' )) return response ({
records : [{ id : '193000000000200003' , warehouseName : '洛杉矶仓' , warehouseType : 'OVERSEAS' , status : 'ACTIVE' }],
page : 1 , size : 200 , total : 1 , pages : 1 ,
}, requestPath , method )
2026-08-24 21:00:22 +08:00
if ( method === 'POST' && requestPath . endsWith ( '/users' )) return response ({
2026-08-24 21:46:19 +08:00
membershipId : String ( 9000 + userSequence ), username : body . username , tenantId : '0' , roleIds : [ roleId ],
overseasWarehouseId : String ( body . overseasWarehouseId ), status : 'ACTIVE' ,
2026-08-24 21:00:22 +08:00
}, requestPath , method )
2026-09-29 19:48:34 +08:00
if ( method === 'POST' && requestPath === '/api/auth/login' ) return response ({ accessToken : `pda-token- ${ userSequence } ` }, requestPath , method )
2026-08-24 21:00:22 +08:00
if ( method === 'PUT' && /\/users\/\d+\/status$/ . test ( requestPath )) return response ({ status : 'DISABLED' }, requestPath , method )
throw new Error ( `unexpected domestic request ${ method } ${ requestPath } ` )
2026-09-29 19:48:34 +08:00
};
return handler . apply ( this , args );
},
2026-08-24 21:00:22 +08:00
apiSuccess ( module , name , result ) { return result . body . data },
2026-08-24 19:58:35 +08:00
addStep ( module , name , result , passed , evidence , options ) { steps . push ({ module , name , result , passed , evidence , options }) },
assert ( module , name , result , passed , evidence , options ) { steps . push ({ module , name , result , passed , evidence , options }) },
addCoverageGap (... args ) { gaps . push ( args ) },
}
await run ( context )
2026-10-02 20:14:17 +08:00
assert . equal ( calls . some (( call ) => call . requestPath . endsWith ( '/candidates' )), false )
2026-08-24 19:58:35 +08:00
assert . ok ( steps . some (( step ) => step . name . includes ( '正常空态' ) && step . passed ))
2026-10-02 20:14:17 +08:00
assert . ok ( steps . some (( step ) => step . name . includes ( '本轮原柜候选身份覆盖前置' ) && ! step . passed && step . options . deploymentBlocker ))
2026-08-24 19:58:35 +08:00
assert . equal ( gaps . length , 1 )
assert . equal ( context . createdResources . phase4OverseasPda . finalStatus , null )
2026-08-24 21:00:22 +08:00
assert . equal ( context . createdResources . phase4OverseasPda . pickerMembershipIds . length , 2 )
assert . equal ( calls . filter (( call ) => / \ / users \ / \ d + \ / status$ / . test ( call . requestPath )). length , 2 )
2026-08-24 15:38:28 +08:00
})