优化
This commit is contained in:
@@ -1,71 +1,125 @@
|
||||
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'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { verifyFleetCrossCustomer, fixtureContract, continueFleetCrossCustomerAfterLoading, afterLoadingContinuationContract } from './fleet-cross-customer-acceptance.mjs'
|
||||
|
||||
function model(fault='') {
|
||||
const copy=value=>structuredClone(value),calls=[],keys=new Set()
|
||||
const members=[0,1].map(i=>({marker:'RUN',customerId:`C${i}`,incomingNo:`IN${i}`,dispatchNo:`DS${i}`,outboundNo:`OUT${i}`,deliveryNo:`DL${i}`,
|
||||
balanceId:`B${i}`,positionId:`P${i}`,palletId:`PAL${i}`,palletNo:`PALLET${i}`,receiptVerified:true}))
|
||||
const fixture={marker:'RUN',stage:fixtureContract.stage,tripNo:'TRIP',warehouseId:'WH',pdaToken:'not-a-real-token',members}
|
||||
const trip={trip:{tripNo:'TRIP',warehouseId:'WH',status:'PLANNED',recordVersion:'0',vehicleId:'V',driverId:'D',stops:[{name:'Origin'},
|
||||
{name:'A',destinationKey:'FBA:A'},{name:'B',destinationKey:'FBA:B'}]},dispatches:members.map(m=>({dispatchNo:m.dispatchNo,customerId:m.customerId,review:{status:'PENDING'}}))}
|
||||
const state=members.map(m=>({dispatch:{dispatchNo:m.dispatchNo,overseasWarehouseId:'WH',status:'PENDING_DEPARTURE',packageCount:1,pickedPackageCount:0,
|
||||
items:[{id:`ITEM${m.customerId}`,warehousePalletId:m.palletId}]},delivery:{order:{deliveryNo:m.deliveryNo,customerId:m.customerId,dispatchNo:m.dispatchNo,outboundNo:m.outboundNo,status:'WAIT_DELIVERY',version:'0'}},
|
||||
outbound:{outboundNo:m.outboundNo,dispatchNo:m.dispatchNo,overseasWarehouseId:'WH',status:'WAIT_OUTBOUND'},
|
||||
stock:{scope:{marker:'RUN',customerId:m.customerId,warehouseId:'WH',coverage:'CUSTOMER_WAREHOUSE_COMPLETE'},
|
||||
balances:[{id:m.balanceId,customerId:m.customerId,overseasWarehouseId:'WH',availablePackages:'1',holdPackages:'0',allocatedPackages:'0',pickedPackages:'0',receivingPackages:'0',inTransitPackages:'0',procurementPackages:'0'}],
|
||||
positions:[{id:m.positionId,customerId:m.customerId,overseasWarehouseId:'WH',balanceId:m.balanceId,palletId:m.palletId,palletNo:m.palletNo,availablePackages:'1',holdPackages:'0',allocatedPackages:'0',pickedPackages:'0',receivingPackages:'0'}],ledger:[]}}))
|
||||
const cargo=members.map((m,i)=>({dispatchNo:m.dispatchNo,customerId:m.customerId,warehousePalletId:m.palletId,palletNo:m.palletNo,destinationKey:i?'FBA:B':'FBA:A',packages:1}))
|
||||
const response=data=>({http:200,body:{code:'200',data:copy(data)},requestId:'request',traceId:'trace'})
|
||||
const context={marker:'RUN',token:'admin',createdResources:{},assert(_m,label,_r,condition){assert.ok(condition,label)},
|
||||
async request(method,raw,body,token){
|
||||
calls.push({method,raw,body:copy(body),token});const path=new URL(raw,'https://offline.invalid').pathname
|
||||
let index=members.findIndex(m=>path.includes(m.dispatchNo)||path.includes(m.outboundNo)||path.includes(m.deliveryNo))
|
||||
const row=state[index],member=members[index]
|
||||
if(path.endsWith('/review')){trip.dispatches[index].review.status='APPROVED';trip.trip.recordVersion=String(Number(trip.trip.recordVersion)+1);return response(trip)}
|
||||
if(path.endsWith('/loading-review')){trip.loadingReview={status:'APPROVED'};trip.trip.recordVersion=String(Number(trip.trip.recordVersion)+1);return response(trip)}
|
||||
if(path.endsWith('/loading')){if(method==='POST')trip.trip.recordVersion=String(Number(trip.trip.recordVersion)+1);return response({recordVersion:trip.trip.recordVersion,manifestHash:'HASH',cargo,blockReason:null})}
|
||||
if(path.endsWith('/TRIP'))return response(trip)
|
||||
if(path.endsWith('/pick')){
|
||||
if(keys.has(body.idempotencyKey))return response({replayed:true})
|
||||
keys.add(body.idempotencyKey);row.dispatch.pickedPackageCount=1
|
||||
row.stock.positions[0].availablePackages='0';row.stock.balances[0].availablePackages='0'
|
||||
row.stock.ledger=['ALLOCATE','PICK','SHIP'].map(type=>({id:`${type}${index}`,customerId:member.customerId,overseasWarehouseId:'WH',positionId:member.positionId,businessOrderNo:member.dispatchNo,businessType:type,deltaPackages:type==='SHIP'?'-1':'1'}))
|
||||
if(fault==='duplicate ship')row.stock.ledger.push({...row.stock.ledger[2],id:'EXTRA'})
|
||||
return response({replayed:false})
|
||||
}
|
||||
if(path.endsWith('/complete')){row.dispatch.status='READY_TO_DEPART';return response({status:'READY_TO_DEPART'})}
|
||||
if(path.endsWith('/depart')){
|
||||
if(state.some(s=>s.dispatch.pickedPackageCount!==1)){
|
||||
if(fault==='gate reorders route') [trip.trip.stops[1],trip.trip.stops[2]]=[trip.trip.stops[2],trip.trip.stops[1]]
|
||||
if(fault==='gate mutates second customer'){state[1].stock.balances[0].holdPackages='1';state[1].stock.balances[0].availablePackages='0';state[1].stock.positions[0].holdPackages='1';state[1].stock.positions[0].availablePackages='0'}
|
||||
return {http:fault==='outage disguised as rejection'?503:200,body:{code:fault==='outage disguised as rejection'?'S0503':'A0409',message:fault==='unrelated gate rejection'?'车次版本已变化':'DS1:请完成拣货并核平全部箱数'},requestId:'r',traceId:'t'}
|
||||
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, confirmedLoad = null, stopAtArrangement = false, aggregationReads = 4
|
||||
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 forbidden = () => ({ http: 200, body: { code: 'A0403', message: '该请求标识不属于当前操作员', requestId: 'request', traceId: 'trace' }, requestId: 'request', traceId: 'trace' })
|
||||
const remember = (body,payload,actor,action) => { const value = {...result(body,payload),...(action?{action}:{})}; keys.set(body.idempotencyKey,{body:copy(body),result:copy(value),actor});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)
|
||||
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'})
|
||||
return forbidden()
|
||||
}
|
||||
const replayed=keys.has(body.idempotencyKey);keys.add(body.idempotencyKey);trip.trip.status='IN_TRANSIT'
|
||||
for(const [i,s] of state.entries())if(!(fault==='partial departure'&&i===1)){s.dispatch.status='DEPARTED';s.outbound.status='OUTBOUNDED'}
|
||||
if(fault==='depart deducts again'){row.stock.balances[0].availablePackages='-1';row.stock.positions[0].availablePackages='-1'}
|
||||
return response({tripNo:'TRIP',status:'IN_TRANSIT',shippedPackages:'2',dispatches:members.map(m=>({dispatchNo:m.dispatchNo})),replayed})
|
||||
return response({...prior.result,replayed:true})
|
||||
}
|
||||
if(path.includes('/api/pda/'))return response({dispatchNo:member.dispatchNo,warehouseId:'WH',tripNo:'TRIP',status:row.dispatch.pickedPackageCount?'COMPLETED':'PDA_PENDING',
|
||||
items:[{dispatchItemId:`ITEM${member.customerId}`,palletNo:member.palletNo,scheduledPackages:1,pickedPackages:String(row.dispatch.pickedPackageCount),
|
||||
positions:fault==='missing PDA position'?[]:[{id:member.positionId,balanceId:member.balanceId,availablePackages:row.stock.positions[0].availablePackages}]}]})
|
||||
if(path.includes('/dispatches/'))return response(row.dispatch)
|
||||
if (path === '/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') {
|
||||
if(method==='PUT')return reject('已经真实扫码,不能编辑资源')
|
||||
if(fault==='aggregation delayed'&&state.every(s=>s.delivery?.order.status==='DELIVERED')) {
|
||||
if(aggregationReads===0)stage='DELIVERED';else aggregationReads--
|
||||
}
|
||||
return 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),confirmedLoad,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'?'车次版本已变化':'逐项真实拣货尚未核平')
|
||||
}
|
||||
assert.equal(body.recordVersion,nativeVersion); assert.equal(body.measurements.length,2)
|
||||
stage='AFTER_LOADING';nativeVersion++
|
||||
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')
|
||||
}
|
||||
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},token)
|
||||
}
|
||||
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('/status')){row.delivery.order.status='DELIVERING';row.delivery.order.version=String(Number(row.delivery.order.version)+1);return response({deliveryStatus:'DELIVERING'})}
|
||||
if(path.endsWith('/documents')){
|
||||
if(path.endsWith('/documents')) {
|
||||
if(method==='GET')return response({version:row.delivery.order.version,categories:[]})
|
||||
row.delivery.order.status='DELIVERED';row.delivery.order.version=String(Number(row.delivery.order.version)+1)
|
||||
row.delivery.order.status='DELIVERED';row.delivery.order.version++
|
||||
if(fault==='signs both')state[1-index].delivery.order.status='DELIVERED'
|
||||
if(state.every(s=>s.delivery.order.status==='DELIVERED')&&!['aggregation delayed','aggregation never completes'].includes(fault))stage='DELIVERED'
|
||||
return response({version:row.delivery.order.version})
|
||||
}
|
||||
if(path.includes('/delivery-orders/'))return response(row.delivery)
|
||||
if(path.includes('/delivery-orders/')) {assert.equal(url.searchParams.get('customerId'),members[index].customerId);return response(row.delivery)}
|
||||
throw new Error(`Unexpected ${method} ${path}`)
|
||||
},async requestMultipart(method,path,body){calls.push({method,raw:path,multipart:true});return response({fileId:`FILE-${path.includes('DL0')?0:1}`})}}
|
||||
},async requestMultipart(method,path) {calls.push({method,raw:path,multipart:true});return response({fileId:`FILE-${path.includes('DL0')?0:1}`})} }
|
||||
const readInventory=async scope=>{const value=copy(state[members.findIndex(m=>m.customerId===scope.customerId)].stock);if(fault==='wrong stock scope')value.positions[0].customerId='OTHER';return value}
|
||||
return {context,fixture,readInventory,calls,state}
|
||||
return {context,fixture,readInventory,calls,state,stopAfterLoading(){stopAtArrangement=true},resumeAfterLoading(){stopAtArrangement=false}}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -73,17 +127,29 @@ 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,3)
|
||||
assert.equal(m.calls.filter(c=>c.raw.endsWith('/depart')).length,4)
|
||||
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))
|
||||
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)
|
||||
})
|
||||
test('explicit rollback verifier runs after both picks and before successful departure',async()=>{
|
||||
const m=model();let calls=0
|
||||
const proof=await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory,
|
||||
verifyDepartureRollback:async({ready,departPath,departBody})=>{
|
||||
calls++;assert.equal(ready.trip.trip.status,'PLANNED')
|
||||
assert.ok(ready.members.every(row=>row.dispatch.status==='READY_TO_DEPART'))
|
||||
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')
|
||||
return {verified:true,scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK'}
|
||||
@@ -101,7 +167,7 @@ for(const failure of ['unverified','throws','mutates'])test(`rollback verificati
|
||||
assert.equal(m.calls.filter(call=>call.raw.endsWith('/depart')).length,1)
|
||||
assert.ok(m.state.every(row=>row.delivery.order.status==='WAIT_DELIVERY'))
|
||||
})
|
||||
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']) {
|
||||
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()=>{
|
||||
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)
|
||||
@@ -118,5 +184,152 @@ test('missing reader, reused customer and stale marker reject before any API req
|
||||
test('module contains no autonomous runner or SQL/environment/credential plumbing',async()=>{
|
||||
const source=await readFile(new URL('./fleet-cross-customer-acceptance.mjs',import.meta.url),'utf8')
|
||||
assert.doesNotMatch(source,/process\.env|fetch\(|execSync|INSERT INTO|UPDATE ll_|createPicker|createMerchant/)
|
||||
assert.match(source,/controlledPalletMeasurements/)
|
||||
assert.match(source,/confirmFleetLoading/)
|
||||
})
|
||||
|
||||
async function afterLoadingModel(fault = '') {
|
||||
const m = model(fault)
|
||||
m.stopAfterLoading()
|
||||
await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}),/Explicit test checkpoint after confirmed loading/)
|
||||
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')
|
||||
delete m.context.createdResources.fleetCrossCustomer
|
||||
m.fixture.stage = afterLoadingContinuationContract.stage
|
||||
m.resumeAfterLoading()
|
||||
m.calls.length = 0
|
||||
return {...m,priorProof}
|
||||
}
|
||||
|
||||
const continueLoaded = (m,extra = {}) => continueFleetCrossCustomerAfterLoading(m.context,m.fixture,
|
||||
{execute:true,readInventory:m.readInventory,priorProof:m.priorProof,completionPolling:{maxAttempts:8,intervalMs:1,deadlineMs:1000},...extra})
|
||||
const writes = m => m.calls.filter(call=>!['GET','HEAD','OPTIONS'].includes(call.method))
|
||||
function alterRead(m,match,alter) {
|
||||
const original = m.context.request
|
||||
m.context.request = async function(...args) {
|
||||
const response = await original.apply(this,args)
|
||||
if(args[0]==='GET'&&match(args[1]))alter(response.body.data)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
test('explicit after-loading import/default stays offline and is not a fallback of the fresh entry',async()=>{
|
||||
let called=false
|
||||
assert.equal(await continueFleetCrossCustomerAfterLoading({request(){called=true}},{}),afterLoadingContinuationContract)
|
||||
assert.equal(called,false)
|
||||
const m=await afterLoadingModel()
|
||||
await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}))
|
||||
assert.equal(m.calls.length,0)
|
||||
})
|
||||
|
||||
test('explicit after-loading continuation reconciles original proof and never repeats picking or loading',async()=>{
|
||||
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.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
|
||||
assert.equal(proof.continuation.priorProofSHA256,createHash('sha256').update(JSON.stringify(normalize(original))).digest('hex'))
|
||||
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.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()
|
||||
if(failure==='missing prior')m.priorProof=undefined
|
||||
if(failure==='marker')m.priorProof.marker='OTHER-RUN'
|
||||
if(failure==='gate')m.priorProof.gateProof='NOT_RUN'
|
||||
if(failure==='source identity')m.priorProof.members[0].positionId='FOREIGN-POSITION'
|
||||
if(failure==='prior picking')m.priorProof.snapshots.AFTER_PICK_1.dispatch.counts.pickedPackageCount=1
|
||||
if(failure==='unknown prior write')m.priorProof.requests.push({method:'POST',path:'/unconfirmed-command',status:'UNKNOWN_RESULT'})
|
||||
if(failure==='loading result')delete m.fixture.tripProof.loadingCommand.result
|
||||
if(failure==='loading manifest')m.fixture.tripProof.loadingCommand.body.manifestHash='OTHER-MANIFEST'
|
||||
if(failure==='loading pallet')m.fixture.tripProof.loadingCommand.result.result.confirmedLoad.pallets[0].warehousePalletId='OTHER-PALLET'
|
||||
if(failure==='loading version')m.fixture.tripProof.loadingCommand.body.recordVersion='1'
|
||||
await assert.rejects(continueLoaded(m));assert.equal(m.calls.length,0)
|
||||
})
|
||||
}
|
||||
|
||||
for(const failure of ['inventory drift','current main version','current shipped','existing outbound','current source identity','manifest drift','confirmed load drift','existing arrangement']) {
|
||||
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'})
|
||||
if(failure==='current main version')alterRead(m,path=>path.endsWith('/DS-ONE'),value=>{value.recordVersion=999})
|
||||
if(failure==='current shipped')alterRead(m,path=>path.endsWith('/DS-ONE'),value=>{value.counts.shippedPackageCount=1})
|
||||
if(failure==='existing outbound')alterRead(m,path=>path.endsWith('/DS-ONE'),value=>{if(value.documents)value.documents.outbounds.push({outboundNo:'EXISTING'})})
|
||||
if(failure==='current source identity')alterRead(m,path=>path.endsWith('/items'),value=>{value.items[0].warehousePalletId='FOREIGN'})
|
||||
if(failure==='manifest drift')alterRead(m,path=>path.endsWith('/loading'),value=>{value.manifestHash='OTHER'})
|
||||
if(failure==='confirmed load drift')alterRead(m,path=>path.endsWith('/loading'),value=>{value.confirmedLoad.pallets[0].weightKg=999})
|
||||
if(failure==='existing arrangement')alterRead(m,path=>path.endsWith('/delivery-arrangement'),value=>{value.arrangement={status:'ARRANGED'}})
|
||||
await assert.rejects(continueLoaded(m));assert.equal(writes(m).length,0)
|
||||
})
|
||||
}
|
||||
|
||||
test('read-only aggregation polling accepts delayed completion without repeating receipt uploads or signatures',async()=>{
|
||||
const m=await afterLoadingModel('aggregation delayed')
|
||||
const proof=await continueLoaded(m)
|
||||
assert.equal(proof.completionPolling.status,'COMPLETED')
|
||||
assert.ok(proof.completionPolling.attempts.length>1&&proof.completionPolling.attempts.length<=8)
|
||||
assert.ok(proof.completionPolling.attempts.every(entry=>entry.status==='RESPONDED'))
|
||||
assert.equal(m.calls.filter(call=>call.multipart).length,2)
|
||||
assert.equal(m.calls.filter(call=>call.method==='PUT'&&call.raw.includes('/documents?')).length,2)
|
||||
})
|
||||
|
||||
test('read-only aggregation polling times out as a real failure without changing the dispatch stage',async()=>{
|
||||
const m=await afterLoadingModel('aggregation never completes')
|
||||
await assert.rejects(continueLoaded(m,{completionPolling:{maxAttempts:3,intervalMs:1,deadlineMs:1000}}),/Automatic delivery completion/)
|
||||
const proof=m.context.createdResources.fleetCrossCustomer
|
||||
assert.equal(proof.status,'FAILED');assert.equal(proof.completionPolling.status,'TIMED_OUT')
|
||||
assert.equal(proof.completionPolling.attempts.length,3)
|
||||
assert.ok(proof.completionPolling.attempts.every(entry=>entry.stage==='DELIVERING'))
|
||||
assert.equal(m.calls.filter(call=>call.multipart).length,2)
|
||||
assert.equal(m.calls.filter(call=>call.method==='PUT'&&call.raw.includes('/documents?')).length,2)
|
||||
})
|
||||
|
||||
for(const failure of ['permission','structure'])test(`completion polling terminates immediately on ${failure} errors`,async()=>{
|
||||
const m=await afterLoadingModel(),original=m.context.request
|
||||
let reads=0
|
||||
m.context.request=async function(...args){
|
||||
const response=await original.apply(this,args)
|
||||
if(args[0]==='GET'&&args[1]==='/api/platform/v1/last-leg/dispatches/DS-ONE'
|
||||
&&m.context.createdResources.fleetCrossCustomer?.completionPolling?.status==='WAITING') {
|
||||
reads++
|
||||
if(failure==='permission')return {http:403,body:{code:'A0403'},requestId:'request',traceId:'trace'}
|
||||
response.body.data.documents={outbounds:[],deliveries:[]}
|
||||
}
|
||||
return response
|
||||
}
|
||||
await assert.rejects(continueLoaded(m))
|
||||
assert.equal(reads,1);assert.equal(m.context.createdResources.fleetCrossCustomer.completionPolling.status,'FAILED')
|
||||
assert.equal(m.calls.filter(call=>call.multipart).length,2)
|
||||
assert.equal(m.calls.filter(call=>call.method==='PUT'&&call.raw.includes('/documents?')).length,2)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user