297 lines
25 KiB
JavaScript
297 lines
25 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { createHash } from 'node:crypto'
|
|
import { verifyFleetCrossCustomer, fixtureContract, continueFleetCrossCustomerAfterLoading, afterLoadingContinuationContract } from './fleet-cross-customer-acceptance.mjs'
|
|
|
|
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('/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})
|
|
}
|
|
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--
|
|
}
|
|
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',
|
|
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==='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('measurements' in body,false)
|
|
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:members.map((m,i)=>({warehousePalletId:m.palletId,palletSpaces:1,weightKg:100,volumeCbm:1,deliveryIndex:i+1})),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}]}))})
|
|
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')) {
|
|
if(method==='GET')return response({version:row.delivery.order.version,categories:[]})
|
|
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/')) {assert.equal(url.searchParams.get('customerId'),members[index].customerId);return response(row.delivery)}
|
|
throw new Error(`Unexpected ${method} ${path}`)
|
|
},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,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)
|
|
})
|
|
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.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))
|
|
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('scanned pallets confirm without a separate pallet-space measurement receipt',async()=>{
|
|
const m=model();delete m.fixture.loadingMeasurements
|
|
await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory})
|
|
assert.ok(m.state.every(row=>row.picked===1&&row.delivery!==null))
|
|
assert.ok(m.calls.filter(call=>call.raw.endsWith('/loading-complete')).every(call=>!('measurements' in call.body)))
|
|
})
|
|
|
|
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.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')
|
|
})
|
|
for(const failure of ['unverified','throws','mutates'])test(`rollback verification ${failure} prevents successful departure`,async()=>{
|
|
const m=model()
|
|
await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory,
|
|
verifyDepartureRollback:async()=>{
|
|
if(failure==='throws')throw Error('Observed rollback failed')
|
|
if(failure==='mutates')m.state[0].stock.balances[0].availablePackages='-1'
|
|
return {verified:failure!=='unverified',scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK'}
|
|
}}))
|
|
assert.equal(m.calls.filter(call=>call.raw.endsWith('/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()=>{
|
|
const m=model(fault);await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}),
|
|
fault==='gate reorders route'?/Rejected departure leaves both complete inventories\/outbounds\/deliveries\/trip unchanged/:undefined)
|
|
assert.equal(m.context.createdResources.fleetCrossCustomer.status,'FAILED')
|
|
assert.equal(m.context.createdResources.fleetCrossCustomer.midTransactionRollback,'NOT_TESTED')
|
|
})
|
|
}
|
|
test('missing reader, reused customer and stale marker reject before any API request',async()=>{
|
|
for(const change of ['reader','customer','marker']){
|
|
const m=model();if(change==='customer')m.fixture.members[1].customerId=m.fixture.members[0].customerId;if(change==='marker')m.fixture.marker='OLD'
|
|
await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:change==='reader'?undefined:m.readInventory}));assert.equal(m.calls.length,0)
|
|
}
|
|
})
|
|
test('module contains no autonomous runner or SQL/environment/credential plumbing',async()=>{
|
|
const source=await readFile(new URL('./fleet-cross-customer-acceptance.mjs',import.meta.url),'utf8')
|
|
assert.doesNotMatch(source,/process\.env|fetch\(|execSync|INSERT INTO|UPDATE ll_|createPicker|createMerchant/)
|
|
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,'DELIVERING')
|
|
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_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
|
|
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')),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)
|
|
})
|
|
|
|
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']) {
|
|
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)
|
|
})
|