This commit is contained in:
2026-10-21 14:30:41 +08:00
parent 0af977bdef
commit 40a03978cd
53 changed files with 1004 additions and 1291 deletions
@@ -43,8 +43,8 @@ function model(fault = '') {
if (method === 'POST' && keys.has(body?.idempotencyKey)) {
const prior = keys.get(body.idempotencyKey); assert.deepEqual(body,prior.body)
if(token!==prior.actor) {
if(fault==='foreign departure actor accepted'&&path.endsWith('/depart'))return response({...prior.result,replayed:true})
if(fault==='foreign departure actor mutates'&&path.endsWith('/depart'))state[0].stock.ledger.push({...state[0].stock.ledger[0],id:'FOREIGN-ACTOR-MUTATION'})
if(fault==='foreign departure actor accepted'&&path.endsWith('/loading-complete'))return response({...prior.result,replayed:true})
if(fault==='foreign departure actor mutates'&&path.endsWith('/loading-complete'))state[0].stock.ledger.push({...state[0].stock.ledger[0],id:'FOREIGN-ACTOR-MUTATION'})
return forbidden()
}
return response({...prior.result,replayed:true})
@@ -56,6 +56,7 @@ function model(fault = '') {
if(fault==='aggregation delayed'&&state.every(s=>s.delivery?.order.status==='DELIVERED')) {
if(aggregationReads===0)stage='DELIVERED';else aggregationReads--
}
if(stopAtArrangement&&stage==='DELIVERING'&&fixture.tripProof.loadingCommand?.afterReplay)throw Error('Explicit test checkpoint after confirmed loading')
return response(detail())
}
if (path.endsWith('/loading') && method === 'GET') return response({dispatchNo:'DS-ONE',status:'ACTIVE',stage,recordVersion:nativeVersion,manifestHash:'REAL-MANIFEST',
@@ -64,11 +65,15 @@ function model(fault = '') {
evidence:[actualEvidence('weightKg',100,i),actualEvidence('volumeCbm',1,i)]}))})
if (path.endsWith('/loading-complete')) {
if (!state.every(s=>s.picked===1)) {
if(fault==='gate reorders route')[trip.trip.stops[1],trip.trip.stops[2]]=[trip.trip.stops[2],trip.trip.stops[1]]
if(fault==='gate mutates second customer')state[1].stock.balances[0].holdPackages='1'
if(fault==='outage disguised as rejection')return {http:503,body:{code:'S0503'},requestId:'r',traceId:'t'}
return reject(fault==='unrelated gate rejection'?'车次版本已变化':'逐项真实拣货尚未核平')
}
assert.equal(body.recordVersion,nativeVersion); assert.equal(body.measurements.length,2)
stage='AFTER_LOADING';nativeVersion++
stage='DELIVERING';nativeVersion++;trip.trip.status='IN_TRANSIT'
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:'OUTBOUNDED',packageCount:1};s.delivery={order:{deliveryNo:`DL${i}`,customerId:fault==='foreign delivery'?'OTHER':m.customerId,dispatchNo:'DS-ONE',outboundNo:`OUT${i}`,status:fault==='partial departure'&&i===1?'WAIT_DELIVERY':'DELIVERING',version:0}} }
if(fault==='depart deducts again'){state[0].stock.balances[0].availablePackages='-1';state[0].stock.positions[0].availablePackages='-1'}
confirmedLoad={manifestHash:body.manifestHash,pallets:copy(body.measurements),operatorId:'PDA-ACTOR',confirmedAt:'2026-10-19T01:00:00Z'}
return remember(body,{confirmedLoad,loadingCompletionHash:'LOADED'},token,'LOADING_COMPLETE')
}
@@ -82,29 +87,6 @@ function model(fault = '') {
}
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')) {
if(stopAtArrangement)throw Error('Explicit test checkpoint after confirmed loading')
return response({dispatchNo:'DS-ONE',recordVersion:nativeVersion,status:'ACTIVE',stage,cargoHash:'CARGO',loadingCompletionHash:'LOADED',arrange:{allowed:true},arrangement:arranged?{status:'ARRANGED'}:null,
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()},token)
}
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)},token)
}
const index=members.findIndex((_m,i)=>path.includes(`/OUT${i}`)||path.includes(`/DL${i}`)),row=state[index]
if(path.includes('/outbound-orders/'))return response({outbound:row.outbound,deliveries:[row.delivery.order]})
if(path.endsWith('/documents')) {
@@ -127,7 +109,7 @@ test('default call and import do not request, create or activate anything',async
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,4)
assert.equal(m.calls.some(c=>c.raw.endsWith('/depart')),false)
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))
@@ -148,10 +130,10 @@ test('explicit rollback verifier runs after both picks and before successful dep
const m=model();let calls=0
const proof=await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory,
verifyDepartureRollback:async({ready,departPath,departBody})=>{
calls++;assert.equal(ready.trip.trip.status,'PLANNED');assert.equal(ready.dispatch.stage,'AFTER_LOADING')
assert.ok(ready.members.every(row=>row.outbound.outbound.status==='WAIT_OUTBOUND'))
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')
calls++;assert.equal(ready.trip.trip.status,'PLANNED');assert.equal(ready.dispatch.stage,'LOADING')
assert.ok(ready.members.every(row=>!row.outbound))
assert.equal(m.calls.filter(call=>call.raw.endsWith('/loading-complete')).length,1)
assert.ok(departPath.includes(m.fixture.members[0].dispatchNo));assert.ok(departBody.idempotencyKey)
return {verified:true,scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK'}
}})
assert.equal(calls,1);assert.equal(proof.midTransactionRollback,'VERIFIED')
@@ -164,8 +146,8 @@ for(const failure of ['unverified','throws','mutates'])test(`rollback verificati
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'))
assert.equal(m.calls.filter(call=>call.raw.endsWith('/loading-complete')).length,1)
assert.ok(m.state.every(row=>row.delivery===null))
})
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','foreign departure actor accepted','foreign departure actor mutates']) {
test(`fails closed without widening the claimed evidence: ${fault}`,async()=>{
@@ -194,7 +176,7 @@ async function afterLoadingModel(fault = '') {
const priorProof = structuredClone(m.context.createdResources.fleetCrossCustomer)
assert.equal(priorProof.status,'FAILED')
assert.equal(priorProof.gateProof,'PREREQUISITE_REJECTION_VERIFIED')
assert.equal(m.fixture.tripProof.loadingCommand.result.stage,'AFTER_LOADING')
assert.equal(m.fixture.tripProof.loadingCommand.result.stage,'DELIVERING')
delete m.context.createdResources.fleetCrossCustomer
m.fixture.stage = afterLoadingContinuationContract.stage
m.resumeAfterLoading()
@@ -227,7 +209,7 @@ test('explicit after-loading continuation reconciles original proof and never re
const m=await afterLoadingModel(),original=structuredClone(m.priorProof),loading=structuredClone(m.fixture.tripProof.loadingCommand)
const proof=await continueLoaded(m)
assert.equal(proof.status,'VERIFIED');assert.equal(proof.continued,true)
assert.equal(proof.continuation.mode,'EXPLICIT_AFTER_LOADING')
assert.equal(proof.continuation.mode,'EXPLICIT_COMPLETED_LOADING')
assert.equal(proof.continuation.gateProofSource,'PRIOR_PROOF')
assert.match(proof.continuation.priorProofSHA256,/^[a-f0-9]{64}$/)
const normalize=value=>Array.isArray(value)?value.map(normalize):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,normalize(value[key])])):value
@@ -235,33 +217,12 @@ test('explicit after-loading continuation reconciles original proof and never re
assert.deepEqual(m.priorProof,original);assert.deepEqual(m.fixture.tripProof.loadingCommand,loading)
assert.equal(proof.snapshots.AFTER_PICK_1,undefined,'no historical loading snapshot is fabricated in the new proof')
assert.ok(proof.snapshots.CONTINUATION_INITIAL);assert.deepEqual(proof.continuation.originalAfterPickSnapshot,original.snapshots.AFTER_PICK_1)
assert.equal(writes(m).some(call=>call.raw.endsWith('/pick')||call.raw.endsWith('/loading-complete')),false)
assert.equal(writes(m).filter(call=>call.raw.endsWith('/transfer-delivery')).length,2,'one arrangement plus the exact original-key replay')
const departures=writes(m).filter(call=>call.raw.endsWith('/depart'))
assert.deepEqual(departures.map(call=>call.token),[m.fixture.pdaToken,m.fixture.pdaToken,m.context.token])
assert.equal(writes(m).some(call=>call.raw.endsWith('/pick')),false)
assert.equal(writes(m).some(call=>call.raw.endsWith('/transfer-delivery')||call.raw.endsWith('/depart')),false)
assert.deepEqual(m.state.map(row=>row.delivery.order.status),['DELIVERED','DELIVERED'])
assert.equal(JSON.stringify(proof).includes(m.fixture.pdaToken),false)
})
test('after-loading accepts the official NON_NULL preview when the unarranged field is omitted',async()=>{
const m=await afterLoadingModel()
let omitted=false
alterRead(m,path=>path.endsWith('/delivery-arrangement'),value=>{
delete value.arrangement
omitted=!Object.hasOwn(value,'arrangement')
})
const proof=await continueLoaded(m)
assert.equal(omitted,true);assert.equal(proof.status,'VERIFIED');assert.equal(proof.continued,true)
assert.equal(writes(m).some(call=>call.raw.endsWith('/pick')||call.raw.endsWith('/loading-complete')),false)
})
test('after-loading rejects even an empty existing arrangement object before any business write',async()=>{
const m=await afterLoadingModel()
alterRead(m,path=>path.endsWith('/delivery-arrangement'),value=>{value.arrangement={}})
await assert.rejects(continueLoaded(m),/Continuation has no prior arrangement/)
assert.equal(writes(m).length,0)
})
for(const failure of ['missing prior','marker','gate','source identity','prior picking','unknown prior write','loading result','loading manifest','loading pallet','loading version']) {
test(`after-loading refuses incomplete or foreign source proof before any request: ${failure}`,async()=>{
const m=await afterLoadingModel()
@@ -279,7 +240,7 @@ for(const failure of ['missing prior','marker','gate','source identity','prior p
})
}
for(const failure of ['inventory drift','current main version','current shipped','existing outbound','current source identity','manifest drift','confirmed load drift','existing arrangement']) {
for(const failure of ['inventory drift','current main version','current shipped','existing outbound','current source identity']) {
test(`after-loading refuses changed current facts before arranging or departing: ${failure}`,async()=>{
const m=await afterLoadingModel()
if(failure==='inventory drift')m.state[0].stock.ledger.push({...m.state[0].stock.ledger[0],id:'UNEXPECTED-MOVEMENT'})