Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs
T

165 lines
13 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { selectReturnSources, verifyFleetReturnRedispatch } from './fleet-return-redispatch-acceptance.mjs'
function fixture(fault = '') {
const trip = { tripNo: 'TR-ORIGINAL' }, requests = [], checkpoints = []
let returned = false, originalSigned = false, newSigned = false, departed = false
const relations = ['A', 'B'].map((name, index) => ({ lastMileOrderId: name, inboundOrderId: String(index + 10), inboundCode: `IN-${name}`,
deliveryNo: 'DL-ORIGINAL', outboundNo: 'OUT-ORIGINAL', returnSources: [{ outboundPalletItemId: String(index + 20), inventoryPositionId: String(index + 30), maxReturnPackages: index ? 3 : 4 }] }))
const tail = name => ({ orderType: 'FIRST_LEG', sourceInboundId: `DOMESTIC-${name}`, customerId: 'C', overseasWarehouseId: 'W', inboundCode: `IN-${name}`, packageCount: name === 'A' ? 4 : 3,
subOrderStatus: name === 'A' ? newSigned || fault === 'premature-A' && originalSigned ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY'
: originalSigned && fault !== 'blocked-B' ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY' })
const order = name => name === 'DL-NEW' ? { deliveryNo: name, dispatchNo: 'DP-NEW', packageCount: 2, signedPackages: newSigned ? 2 : null, signedAt: newSigned ? 'new-signature' : null }
: { deliveryNo: 'DL-ORIGINAL', dispatchNo: 'DP-ORIGINAL', outboundNo: 'OUT-ORIGINAL', status: 'DELIVERING', packageCount: 7,
signedPackages: originalSigned ? 5 : null, signedAt: originalSigned ? newSigned && fault === 'rewritten-original' ? 'changed' : 'original-signature' : null }
const envelope = data => ({ http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'request-id', traceId: 'trace-id' })
const context = { marker: 'FRESH', createdResources: { fleetTrips: [trip], fleetAcceptance: { marker: 'FRESH', customerId: 'C', warehouseId: 'W', tripNo: trip.tripNo, departure: { status: 'IN_TRANSIT', dispatches: [{ dispatchNo: 'DP-ORIGINAL' }] } } },
assert() {}, async checkpoint() { checkpoints.push(structuredClone(trip.returnRedispatch)) },
async requestMultipart(method, path, file) { requests.push({ method, path, file: file.name }); return envelope({ fileId: '99' }) },
async request(method, path, body, token) {
requests.push({ method, path, body: structuredClone(body), token })
if (path.startsWith('/api/platform/inbounds?')) {
const code = new URLSearchParams(path.split('?')[1]).get('inboundCode'), name = code.slice(-1)
const delivered = name === 'A' ? newSigned || fault === 'domestic-premature-A' && originalSigned : originalSigned
return envelope({ records: [{ inboundCode: code, processStatus: delivered ? 'DELIVERED' : 'LOADED' }] })
}
if (path.includes('/orders/')) return envelope({ order: tail(path.split('/orders/')[1].split('?')[0]) })
if (path.includes('/return-inbound-options')) return envelope(fault === 'missing-B' ? relations.slice(0, 1) : relations)
if (path.includes('/mark-return')) return envelope({ exceptionNo: 'EX-NEW', version: 0 })
if (path.includes('/start-processing')) return envelope({ status: 'PROCESSING', version: 1 })
if (path.includes('/exception-orders/') && path.includes('/complete')) {
const replayed = returned; returned = true
if (fault === 'unknown-return') throw Error('connection lost after commit')
return envelope({ status: 'COMPLETED', restoredPackages: fault === 'wrong-return' ? 3 : 2, inventoryAdjustmentId: '88', replayed })
}
if (path.endsWith('/pallets') && method === 'POST') { context.palletCode = body.palletCode; return envelope('100') }
if (path.endsWith('/pallets/100')) return envelope({ warehouseId: 'W', locationId: '200', palletCode: context.palletCode })
if (path.includes('/locations?')) return envelope({ records: [{ id: '200', locationCode: 'L-1' }] })
if (path.includes('/dispatches/candidates')) return envelope([{ sourceType: 'RETURN_RECEIPT', returnReceiptId: '300', returnReceiptNo: 'RSR-300', warehousePalletId: '100', orderId: 'A', packageCount: fault === 'wrong-candidate' ? 4 : 2 }])
if (path.endsWith('/drafts/containers')) return envelope({ id: '400' })
if (path.endsWith('/draft-locks/lock')) { assert.equal(body.sourceType, 'RETURN_RECEIPT'); assert.equal(body.planPalletId, undefined); return envelope(null) }
if (path.endsWith('/pre-arrange')) { assert.deepEqual(body.planPalletIds, []); assert.deepEqual(body.returnReceiptIds, ['300']); return envelope({ dispatchNo: 'DP-NEW', packageCount: 2 }) }
if (path.endsWith('/fleet/trips/TR-NEW')) return envelope({ trip: { vehiclePlate: 'NEW', driverName: 'driver', driverPhone: 'phone' } })
if (path.endsWith('/transfer-delivery')) return envelope({ deliveries: [{ deliveryNo: 'DL-NEW' }] })
if (path.endsWith('/depart')) { assert.equal(token, 'private-pda-token'); const replayed = departed; departed = true; return envelope({ tripNo: 'TR-NEW', shippedPackages: 2, replayed }) }
if (path.includes('/delivery-orders/')) return envelope({ order: order(path.split('/delivery-orders/')[1].split('?')[0]) })
throw Error(`Unexpected ${method} ${path}`)
} }
const dependencies = {
async prepareFleetTrip() { return { tripNo: 'TR-NEW' } }, async confirmFleetLoading() { return 1 }, async pickAll() { return true },
async finishFleetTrip() { return { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' } },
async saveReceiptAndComplete(_ctx, delivery) { if (delivery.deliveryNo === 'DL-NEW') newSigned = true; else originalSigned = true; return { order: order(delivery.deliveryNo) } },
}
const input = { customerId: 'C', warehouseId: 'W', pickerToken: 'private-pda-token', dispatchNos: ['DP-ORIGINAL'], deliveryNos: ['DL-ORIGINAL'] }
return { context, trip, input, dependencies, requests, checkpoints }
}
const run = f => verifyFleetReturnRedispatch(f.context, 'test', f.trip, f.input, f.dependencies)
test('fresh return/redispatch chain proves A pending, B complete, then A complete without claiming unique source events', async () => {
const f = fixture(), result = await run(f)
assert.equal(result.status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED')
assert.equal(result.sourceEventsVerified, false)
assert.equal(result.readOnlyEvidenceRequired.length, 3)
assert.equal(result.beforeRedispatch.A.order.subOrderStatus, 'DELIVERY:WAIT_DELIVERY')
assert.equal(result.beforeRedispatch.B.order.subOrderStatus, 'DELIVERY:DELIVERED')
assert.equal(result.afterRedispatch.A.order.subOrderStatus, 'DELIVERY:DELIVERED')
assert.equal(result.afterRedispatch.departed.shippedPackages, 2)
assert.ok(!JSON.stringify(f.checkpoints).includes('private-pda-token'))
assert.ok(f.checkpoints.some(row => row.requests.at(-1)?.status === 'SENDING' && row.requests.at(-1)?.file?.sha256))
})
for (const fault of ['missing-B', 'wrong-return', 'premature-A', 'domestic-premature-A', 'blocked-B', 'wrong-candidate', 'rewritten-original']) {
test(`rejects ${fault} instead of declaring redispatch complete`, async () => {
const f = fixture(fault); await assert.rejects(run(f)); assert.equal(f.trip.returnRedispatch.status, 'FAILED')
if (fault === 'missing-B') assert.ok(f.requests.every(row => row.method === 'GET'))
})
}
test('unknown return outcome persists the exact original key and cannot automatically restart', async () => {
const f = fixture('unknown-return'); await assert.rejects(run(f), /connection lost/)
const last = f.checkpoints.at(-1).requests.at(-1)
assert.equal(last.status, 'UNKNOWN_RESULT'); assert.ok(last.body.idempotencyKey)
const count = f.requests.length; await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count)
})
test('pre-send checkpoint failure cannot send even the first HTTP request', async () => {
const f = fixture(); f.context.checkpoint = async () => { throw Error('disk full') }
await assert.rejects(run(f), /disk full/); assert.equal(f.requests.length, 0)
})
test('fixture selection refuses partial coverage or borrowed source shipments', () => {
const row = name => ({ tail: { orderType: 'FIRST_LEG', sourceInboundId: name, inboundCode: name, packageCount: 4 },
relation: { inboundCode: name, returnSources: [{ maxReturnPackages: 3 }] } })
assert.throws(() => selectReturnSources([row('A'), row('B')]), /cover each selected source completely/)
})
test('WAIT_DELIVERY start submits exactly the version read from the current delivery', async () => {
const f = fixture(), original = f.context.request.bind(f.context)
let started = false
f.context.request = async (method, path, body, ...args) => {
if (method === 'PUT' && path.includes('/delivery-orders/DL-ORIGINAL/status')) {
assert.equal(body.expectedVersion, 17); assert.equal(body.status, 'DELIVERING'); started = true
f.requests.push({ method, path, body })
return { http: 200, body: { code: '200', data: { deliveryStatus: 'DELIVERING' } }, requestId: 'r', traceId: 't' }
}
const response = await original(method, path, body, ...args)
if (!started && response.body?.data?.order?.deliveryNo === 'DL-ORIGINAL') Object.assign(response.body.data.order, { status: 'WAIT_DELIVERY', version: '17' })
if (path.includes('/return-inbound-options')) assert.equal(started, true)
return response
}
await run(f)
assert.equal(f.requests.filter(row => row.method === 'PUT' && row.path.includes('/status')).length, 1)
})
for (const multipart of [false, true]) test(`http:0 records UNKNOWN_RESULT and stops without retry; multipart=${multipart}`, async () => {
const f = fixture(), original = f.context.request.bind(f.context)
if (multipart) f.context.requestMultipart = async (method, path, file) => {
f.requests.push({ method, path, file: file.name }); return { http: 0, body: null, requestId: null, traceId: null }
}
else f.context.request = async (method, path, body, ...args) => {
if (path.includes('/exception-orders/') && path.includes('/complete')) {
f.requests.push({ method, path, body: structuredClone(body) }); return { http: 0, body: null, requestId: null, traceId: null }
}
return original(method, path, body, ...args)
}
await assert.rejects(run(f), /transport outcome unknown/)
const last = f.checkpoints.at(-1).requests.at(-1)
assert.equal(last.status, 'UNKNOWN_RESULT'); assert.equal(last.http, 0)
if (multipart) assert.match(last.file.sha256, /^[a-f0-9]{64}$/)
else assert.ok(last.body.idempotencyKey)
assert.equal(f.requests.filter(row => row.path === last.path && row.method === last.method).length, 1)
const count = f.requests.length
await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count)
})
for (const [name, response] of Object.entries({
server500: { http: 500, body: { code: 'B0001', message: 'Internal error' }, requestId: 'r', traceId: 't' },
proxy502: { http: 502, body: null, requestId: null, traceId: null },
malformed200: { http: 200, body: { unexpected: true }, requestId: 'r', traceId: 't' },
htmlFailure: { http: 403, body: '<html>proxy blocked</html>', requestId: 'r', traceId: 't' },
uncorrelatedSuccess: { http: 200, body: { code: '200', data: { status: 'COMPLETED' } }, requestId: null, traceId: null },
})) test(`ambiguous ${name} persists UNKNOWN_RESULT without replay or further requests`, async () => {
const f = fixture(), original = f.context.request.bind(f.context)
f.context.request = async (method, path, body, ...args) => {
if (path.includes('/mark-return')) { f.requests.push({ method, path, body: structuredClone(body) }); return response }
return original(method, path, body, ...args)
}
await assert.rejects(run(f), /outcome unknown/)
const last = f.checkpoints.at(-1).requests.at(-1)
assert.equal(last.status, 'UNKNOWN_RESULT'); assert.equal(last.http, response.http)
assert.ok(last.body.idempotencyKey)
assert.equal(f.requests.filter(row => row.path.includes('/mark-return')).length, 1)
assert.equal(f.requests.at(-1).path, last.path)
const count = f.requests.length; await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count)
})
test('correlated explicit business rejection remains RESPONDED and also stops without replay', async () => {
const f = fixture(), original = f.context.request.bind(f.context)
f.context.request = async (method, path, body, ...args) => {
if (path.includes('/mark-return')) {
f.requests.push({ method, path, body }); return { http: 409, body: { code: 'A0409', message: 'version conflict' }, requestId: 'r', traceId: 't' }
}
return original(method, path, body, ...args)
}
await assert.rejects(run(f), /HTTP failed/)
assert.equal(f.checkpoints.at(-1).requests.at(-1).status, 'RESPONDED')
assert.equal(f.requests.filter(row => row.path.includes('/mark-return')).length, 1)
})