feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution

This commit is contained in:
2026-10-19 23:40:42 +08:00
parent e0677e7ae7
commit 9dd34791f7
346 changed files with 29245 additions and 12602 deletions
@@ -2,272 +2,322 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { prepareFleetTrip, confirmFleetLoading, controlledPalletMeasurements, finishFleetTrip } from './fleet-trip-fulfillment.mjs'
const stops = [{ name: 'Origin' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }]
const cargo = (customerId = 'A') => ({ dispatchNo: `DP-${customerId}`, customerId, warehousePalletId: '9007199254740993', palletNo: 'PLT-1', destinationKey: 'FBA:ONT8', destinationLabel: 'ONT8' })
const D = '/api/platform/v1/last-leg/dispatches/DP-A'
const P = '/api/pda/v1/last-leg/dispatches/DP-A'
const F = '/api/platform/v1/last-leg/fleet/trips/TR-A'
const stops = [{ name: '真实起运仓' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }]
const VID = '9007199254740993', DRIVER = '9007199254740995', PALLET = '9007199254740997'
const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) } })
const conflict = message => ({ http: 200, body: { code: 'A0409', message } })
const evidence = (field, value) => ({ field, value, actual: true, sourceType: 'TEST_PRODUCER_MEASUREMENT', sourceBusinessNo: 'RECEIPT-A', observedAt: '2026-10-18T10:00:00Z' })
const pallet = () => ({ warehousePalletId: PALLET, palletNo: 'PLT-A', scheduledPackages: 3, pickedPackages: 3,
palletSpaces: null, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1, missingFields: ['palletSpaces'],
evidence: [evidence('weightKg', '187.25'), evidence('volumeCbm', '1.375')] })
const receipt = () => ({ environment: 'TEST', marker: 'TEST-NATIVE', dispatchNo: 'DP-A', pallets: [
{ warehousePalletId: PALLET, palletSpaces: 2, deliveryIndex: 1, evidence: [evidence('palletSpaces', 2)] },
] })
test('test loading measures physical pallet once across customers and rejects conflicting unloading points', () => {
assert.deepEqual(controlledPalletMeasurements([cargo(), cargo('B')], stops), [
{ warehousePalletId: '9007199254740993', palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex: 1 },
])
assert.throws(() => controlledPalletMeasurements([{ ...cargo(), warehousePalletId: null }], stops), /缺少真实托盘/)
assert.throws(() => controlledPalletMeasurements([cargo()], [{ name: 'Origin' }]), /不在车次路线/)
assert.throws(() => controlledPalletMeasurements([cargo(), { ...cargo('B'), destinationKey: 'FBA:LAX9' }], [...stops, { destinationKey: 'FBA:LAX9' }]), /须先拆托/)
})
test('fixture prepares actual qualification upload and review before assignment and copies authoritative destination keys', async () => {
const calls = []
let version = 0
const context = {
marker: 'TEST-FLEET', createdResources: {},
apiSuccess(_module, _label, response) { return response.body.data },
async request(method, path, body) {
calls.push({ method, path, body })
let data
if (path.endsWith('/master-data/vehicles')) {
assert.equal(body.status, 'IN_SERVICE', 'VehicleCreate requires an explicit status and trip assignment requires an in-service vehicle')
data = { id: 'vehicle-id' }
}
else if (path.endsWith('/drivers')) data = { id: 'driver-id', recordVersion: 0 }
else if (path.includes('/dispatches/') && path.endsWith('/review')) data = { trip: { recordVersion: version++ }, dispatches: [{ dispatchNo: 'DP-A', review: { status: 'APPROVED' } }] }
else if (path.endsWith('/review')) data = { reviewStatus: 'APPROVED' }
else if (path.endsWith('/loading')) data = { cargo: [cargo()] }
else { data = { trip: { tripNo: 'TR-FIXTURE', recordVersion: version++, stops: body.stops || stops } } }
return { http: 200, body: { code: '200', data } }
},
async requestMultipart(method, path, file) {
calls.push({ method, path, file })
assert.equal(file.type, 'image/png'); assert.ok(file.content.byteLength > 0)
return { http: 200, body: { code: '200', data: { id: 'driver-id', recordVersion: 1, licenseFileId: 'file-id' } } }
},
test('real Jackson long counts accept safe decimal strings without changing pallet IDs', () => {
const expected = controlledPalletMeasurements([pallet()], stops, receipt().pallets)
for (const counts of [{ scheduledPackages: '3', pickedPackages: '3' }, { scheduledPackages: '3', pickedPackages: 3 }]) {
const actual = controlledPalletMeasurements([{ ...pallet(), ...counts }], stops, receipt().pallets)
assert.deepEqual(actual, expected)
assert.equal(actual[0].warehousePalletId, PALLET)
}
const proof = await prepareFleetTrip(context, 'test', 'DP-A', '92')
assert.equal(proof.tripNo, 'TR-FIXTURE')
assert.equal(proof.reassignment.status, 'PENDING')
assert.ok(calls.findIndex(call => call.path.includes('/license?')) < calls.findIndex(call => call.path.endsWith('/review')))
assert.ok(calls.findIndex(call => call.path.endsWith('/review')) < calls.findIndex(call => call.path.endsWith('/assignment')))
assert.equal(calls.find(call => call.path.endsWith('/plan')).body.stops[1].destinationKey, 'FBA:ONT8')
assert.equal(context.createdResources.fleetTrips[0].licenseFileId, 'file-id')
assert.ok(calls.every(call => !call.path.endsWith('/depart')))
for (const value of [null, undefined, '', '03', '3.0', '3e0', true, -1, '9007199254740993', 9007199254740992]) {
assert.throws(() => controlledPalletMeasurements([{ ...pallet(), scheduledPackages: value }], stops, receipt().pallets), /箱数/)
assert.throws(() => controlledPalletMeasurements([{ ...pallet(), pickedPackages: value }], stops, receipt().pallets), /箱数/)
}
assert.throws(() => controlledPalletMeasurements([{ ...pallet(), pickedPackages: '2' }], stops, receipt().pallets), /核平/)
})
test('unready actual dispatch prevents load confirmation rather than bypassing it with test values', async () => {
const calls = []
const context = { request: async (method, path) => { calls.push({ method, path }); return { http: 200, body: { code: '200', data: { blockReason: '拣货未完成' } } } },
apiSuccess(_module, _name, response) { return response.body.data } }
await assert.rejects(confirmFleetLoading(context, 'test', { tripNo: 'TR-1', stops }), /整车未就绪/)
assert.equal(calls.length, 1)
assert.equal(calls[0].method, 'GET')
})
function reassignmentServer(fault = '') {
const proof = { tripNo: 'TR-FRESH', dispatchNo: 'DP-A', vehicleId: '101', driverId: '201', stops,
reassignment: { status: 'PENDING' } }
const trip = { tripNo: proof.tripNo, recordVersion: 4, vehicleId: '101', driverId: '201', vehiclePlate: 'ORIGINAL',
driverName: 'Original driver', driverPhone: '123', status: 'PLANNED', stops, maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 }
const dispatch = { dispatchNo: proof.dispatchNo, overseasWarehouseId: '92', status: 'READY_TO_DEPART',
packageCount: 10, pickedPackageCount: 10, shippedPackageCount: 0, remainingPackageCount: 0,
estimatedArrivalAt: '2026-10-03T12:00:00', items: [{ id: '9007199254740995', inboundCode: 'IN-TEST', packageCount: 10 }],
vehiclePlate: trip.vehiclePlate, driverName: trip.driverName, driverPhone: trip.driverPhone }
const calls = [], assertions = [], dispatches = [{ dispatchNo: proof.dispatchNo, customerId: 'A', review: { status: 'APPROVED' } }]
let load = null, currentCargo = [cargo()], assigned = false
const ok = data => ({ http: 200, body: { code: '200', data: fault === 'string-versions'
? JSON.parse(JSON.stringify(data, (key, value) => key === 'recordVersion' ? String(value) : value)) : structuredClone(data) } })
if (fault === 'string-versions') trip.recordVersion = 8
const loading = () => ({ recordVersion: trip.recordVersion, manifestHash: 'a'.repeat(64), cargo: currentCargo, confirmedLoad: load, blockReason: null })
function server(fault = '') {
const calls = [], assertions = [], saved = new Map()
const detail = { dispatchNo: 'DP-A', status: 'ACTIVE', stage: 'BEFORE_LOADING', reviewState: 'DRAFT', recordVersion: '9007199254740998',
counts: { packageCount: 3, pickedPackageCount: 0, shippedPackageCount: 0 }, reviews: {},
dispatchInfo: { estimatedArrivalAt: '2026-10-21T10:00:00Z', fbaAppointments: [{ appointmentNo: 'FBA-ORIGINAL' }] }, vehicleInfo: {} }
const trip = { tripNo: 'TR-A', status: 'PLANNED', warehouseId: '92', recordVersion: 4, tripPurpose: 'DELIVERY', stops }
let driverVersion = 0, uploadComplete = false, driverReviewed = false, nextPlate = '', checkpointError = false, nextTrip, nextDetail
const claims = new Map([['VEHICLE', 'TR-A'], ['DRIVER', 'TR-A']])
const bump = () => detail.recordVersion = String(BigInt(detail.recordVersion) + 1n)
const result = (action, body, extra = {}) => ({ dispatchNo: 'DP-A', status: detail.status, stage: detail.stage, recordVersion: detail.recordVersion,
action, idempotencyKey: body.idempotencyKey, result: extra, replayed: false, counts: structuredClone(detail.counts) })
const remember = (action, body, extra) => { const r = result(action, body, extra); saved.set(body.idempotencyKey, { body: structuredClone(body), result: structuredClone(r) }); return ok(r) }
const context = {
marker: 'TEST-REASSIGN',
marker: 'TEST-NATIVE', createdResources: {},
checkpoint: async () => { if (checkpointError) throw new Error('checkpoint failed') },
assert(_module, label, _response, passed) { assertions.push({ label, passed }) },
apiSuccess(_module, _label, response) { return response.body.code === '200' ? response.body.data : null },
apiSuccess(_module, _label, response) { return response?.body?.code === '200' ? response.body.data : null },
async requestMultipart(method, path, file) {
calls.push({ method, path }); assert.equal(file.type, 'image/png')
return ok({ licenseFileId: 'FILE-NEW', recordVersion: 1 })
calls.push({ method, path, file: { name: file.name, size: file.content.byteLength } })
assert.equal(file.type, 'image/png'); assert.match(file.name, /TEST-LICENSE/)
uploadComplete = true; driverVersion++
return ok({ id: DRIVER, licenseFileId: '9007199254740999', recordVersion: driverVersion })
},
async request(method, path, body) {
calls.push({ method, path, body: structuredClone(body) })
if (path.endsWith('/loading')) {
if (method === 'GET') return ok(loading())
if (String(body.recordVersion) !== String(trip.recordVersion) && fault !== 'accept-old-version') {
if (fault === 'reject-but-write') load = { manifestHash: body.manifestHash, pallets: body.pallets }
return { http: 200, body: { code: 'A0409', message: fault === 'unrelated-conflict' ? '权限未配置' : '车次已被修改,请刷新后重试' } }
async request(method, path, body, token) {
calls.push({ method, path, body: structuredClone(body), actor: token ? 'PDA' : 'PLATFORM' })
if (fault === 'unknown-edit' && method === 'PUT' && path === D) throw new Error('transport result unknown')
if (path.endsWith('/master-data/vehicles')) {
assert.equal(body.status, 'IN_SERVICE'); assert.match(body.remark, /TEST/); nextPlate = body.plateNumber
return ok({ id: VID })
}
if (path.endsWith('/fleet/drivers')) return ok({ id: DRIVER, recordVersion: 0 })
if (path.endsWith(`/drivers/${DRIVER}/review`)) {
assert.ok(uploadComplete); assert.equal(body.recordVersion, driverVersion); driverReviewed = true
return ok({ id: DRIVER, reviewStatus: 'APPROVED', recordVersion: ++driverVersion })
}
if (path.startsWith('/api/platform/v1/last-leg/fleet/trips?')) {
const rows = fault === 'missing-trip' ? [] : [trip, ...(nextTrip ? [nextTrip] : [])]
if (fault === 'duplicate-trip') rows.push({ ...trip, tripNo: 'TR-B' })
return ok({ records: rows, page: 1, size: 200, total: rows.length, pages: 1 })
}
if (method === 'GET' && path === D) return ok(detail)
if (method === 'GET' && path === `${D.replace('DP-A', 'DP-NEXT')}`) return ok(nextDetail)
if (method === 'GET' && path === `${D.replace('DP-A', 'DP-NEXT')}/items`) return ok({ dispatchNo: 'DP-NEXT', recordVersion: nextDetail.recordVersion, items: [] })
if (method === 'POST' && path === D.replace('/DP-A', '')) {
assert.deepEqual(body.cargo, []); assert.deepEqual(body.batchAllocations, [])
assert.equal(body.vehicle.vehicleId, VID); assert.equal(body.driver.driverId, DRIVER)
assert.equal(claims.size, 0)
if (fault === 'unknown-next-create') throw new Error('next create unknown')
nextTrip = { ...trip, tripNo: 'TR-NEXT', status: 'PLANNED', recordVersion: 0, vehicleRelease: null, driverRelease: null }
nextDetail = { dispatchNo: 'DP-NEXT', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: 0,
counts: { itemCount: 0, palletCount: 0, packageCount: 0, pickedPackageCount: 0, shippedPackageCount: 0 } }
if (fault === 'new-empty-has-cargo') nextDetail.counts.itemCount = 1
claims.set('VEHICLE', 'TR-NEXT'); claims.set('DRIVER', 'TR-NEXT')
return ok({ ...nextDetail, result: { dispatchNo: 'DP-NEXT' } })
}
if (method === 'POST' && path === `${D.replace('DP-A', 'DP-NEXT')}/cancel`) {
assert.equal(body.recordVersion, nextDetail.recordVersion)
nextDetail.status = 'CANCELLED'; nextDetail.recordVersion++; claims.clear()
return ok(nextDetail)
}
if (method === 'GET' && path === `${D}/items`) return ok({ dispatchNo: 'DP-A', recordVersion: detail.recordVersion, items: [{ warehousePalletId: PALLET, pickedPackages: detail.counts.pickedPackageCount, customerId: 'CUSTOMER-A' }] })
if (method === 'GET' && [D, P].some(prefix => path === `${prefix}/loading`)) {
return ok({ dispatchNo: 'DP-A', status: detail.status, stage: detail.stage, recordVersion: detail.recordVersion, pickingReconciled: detail.counts.pickedPackageCount === 3,
manifestHash: 'REAL-MANIFEST', loadingComplete: { allowed: detail.counts.pickedPackageCount === 3, blockReason: '拣货未核平' },
pallets: [pallet()], missingFields: ['PLT-A:palletSpaces'] })
}
if (method === 'PUT' && path === D) {
if (detail.stage !== 'BEFORE_LOADING') {
if (fault === 'edit-mutates-on-rejection') detail.counts.pickedPackageCount--
return conflict('仅装车前可编辑,请显式退回;已经扫码不得改派')
}
load = { manifestHash: body.manifestHash, pallets: body.pallets, cargo: currentCargo }
if (assigned && fault === 'wrong-final-measurement') load.pallets = [{ ...body.pallets[0], weightKg: 200 }]
trip.recordVersion++; return ok(loading())
assert.ok(driverReviewed); assert.equal(body.recordVersion, detail.recordVersion)
assert.equal(body.vehicle.mode, 'MASTER'); assert.equal(body.vehicle.vehicleId, VID)
assert.equal(body.driver.driverId, DRIVER); assert.ok(body.driver.evidence.expectedTaskEndAt)
detail.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: VID, plate: nextPlate, carrierName: 'TEST自有车', capacity: { palletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 } },
driver: { mode: 'MASTER', driverId: DRIVER, name: 'TEST司机', phone: '19900000000' } }
Object.assign(trip, { vehicleId: VID, vehiclePlate: nextPlate, driverId: DRIVER })
bump(); return remember('EDIT', body)
}
if (path.endsWith('/master-data/vehicles')) {
assert.equal(body.status, 'IN_SERVICE'); assert.equal(body.maxLoadKg, trip.maxLoadKg)
return ok({ id: '102' })
if (method === 'POST' && path === `${D}/return-to-review`) {
assert.equal(detail.counts.pickedPackageCount, 0); assert.equal(body.recordVersion, detail.recordVersion)
detail.stage = 'BEFORE_LOADING'; detail.reviewState = 'DRAFT'; bump(); return remember('RETURN_TO_REVIEW', body)
}
if (path.endsWith('/drivers')) return ok({ id: '202', recordVersion: 0 })
if (path.includes('/dispatches/') && path.endsWith('/review')) {
assert.equal(String(body.recordVersion), String(trip.recordVersion)); trip.recordVersion++
dispatches[0].review = { status: 'APPROVED' }; return ok({ trip, dispatches })
if (method === 'POST' && ['submit-review', 'dispatch-review', 'loading-review'].some(action => path === `${D}/${action}`)) {
assert.equal(body.recordVersion, detail.recordVersion); assert.match(body.idempotencyKey, /^[a-f0-9]{64}$/)
if (path.endsWith('/submit-review')) { detail.reviewState = 'DISPATCH_PENDING'; detail.reviews.dispatchReview = { status: 'PENDING' } }
else if (path.endsWith('/dispatch-review')) { assert.equal(detail.reviewState, 'DISPATCH_PENDING'); detail.reviewState = 'LOADING_PENDING'; detail.reviews.dispatchReview.status = 'APPROVED' }
else { assert.equal(detail.reviewState, 'LOADING_PENDING'); detail.reviewState = 'APPROVED'; detail.stage = 'LOADING'; detail.reviews.loadingReview = { status: 'APPROVED' } }
bump(); return remember('REVIEW', body)
}
if (path.endsWith('/loading-review')) {
assert.equal(String(body.recordVersion), String(trip.recordVersion)); assert.ok(load); trip.recordVersion++
return ok({ trip, dispatches, loadingReview: { status: 'APPROVED' } })
if (method === 'POST' && [D, P].some(prefix => path === `${prefix}/loading-complete`)) {
if (saved.has(body.idempotencyKey)) {
assert.deepEqual(body, saved.get(body.idempotencyKey).body)
const replay = { ...saved.get(body.idempotencyKey).result, replayed: true }
if (fault === 'replay-new-version') replay.recordVersion = String(BigInt(replay.recordVersion) + 1n)
return ok(replay)
}
assert.equal(detail.stage, 'LOADING'); assert.equal(detail.counts.pickedPackageCount, 3)
assert.equal(body.recordVersion, detail.recordVersion); assert.equal(body.manifestHash, 'REAL-MANIFEST')
assert.deepEqual(body.measurements, [{ warehousePalletId: PALLET, palletSpaces: 2, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }])
detail.stage = 'AFTER_LOADING'; bump(); return remember('LOADING_COMPLETE', body, { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })
}
if (path.endsWith('/review')) return ok({ reviewStatus: 'APPROVED' })
if (path.endsWith('/assignment')) {
assert.equal(String(body.recordVersion), String(trip.recordVersion))
Object.assign(trip, { vehicleId: body.vehicleId, driverId: body.driverId, vehiclePlate: 'REPLACEMENT', driverName: 'Replacement driver', driverPhone: '456' })
trip.recordVersion++; assigned = true; dispatches[0].review = { status: 'PENDING' }
if (fault !== 'keeps-old-load') load = null
Object.assign(dispatch, { vehiclePlate: trip.vehiclePlate, driverName: trip.driverName, driverPhone: trip.driverPhone })
if (fault === 'changes-picking') dispatch.pickedPackageCount = 0
if (fault === 'changes-customer') currentCargo = [{ ...cargo(), customerId: 'B' }]
if (fault === 'unknown-assignment-outcome') throw new Error('connection interrupted after commit')
return ok({ trip, dispatches })
if (method === 'GET' && path.startsWith(`${F}/resource-occupancy?`)) {
const query = new URL(`https://unused.test${path}`).searchParams, kind = query.get('kind')
return ok([{ resourceId: kind === 'VEHICLE' ? VID : DRIVER, occupiedTripNo: claims.get(kind) ?? null, available: true }])
}
if (path.endsWith(`/dispatches/${proof.dispatchNo}`)) return ok(dispatch)
if (path.endsWith(`/trips/${proof.tripNo}`)) return ok({ trip, dispatches })
throw new Error(`unexpected request ${method} ${path}`)
},
}
return { proof, calls, assertions, context, trip }
}
test('fresh-run load checks reassignment, unchanged work, stale version rejection and fresh confirmation before returning departure version', async () => {
const server = reassignmentServer()
const version = await confirmFleetLoading(server.context, 'test', server.proof)
assert.equal(server.proof.reassignment.status, 'VERIFIED')
assert.equal(version, 9)
assert.equal(server.proof.vehicleId, '102'); assert.equal(server.proof.driverId, '202')
assert.equal(server.proof.reassignment.originalTrip.vehicleId, '101')
assert.equal(server.proof.reassignment.dispatchAfter.pickedPackageCount, 10)
assert.equal(server.proof.confirmedLoad.pallets[0].weightKg, 100)
assert.ok(server.assertions.every(row => row.passed))
assert.equal(server.calls.filter(row => row.method === 'POST' && row.path.endsWith('/loading')).length, 3)
assert.ok(server.calls.every(row => !/\/depart|\/pick|\/complete$/.test(row.path)), 'reassignment does not repeat physical warehouse work')
})
test('string-serialized versions compare numerically across 9 to 10 and remain unchanged in requests', async () => {
const server = reassignmentServer('string-versions')
assert.equal(await confirmFleetLoading(server.context, 'test', server.proof), '13')
assert.equal(server.proof.reassignment.status, 'VERIFIED')
assert.equal(server.proof.reassignment.assignmentRequest.recordVersion, '9')
assert.equal(server.calls.filter(row => row.method === 'POST' && row.path.endsWith('/loading')).at(-1).body.recordVersion, '11')
})
for (const [fault, message] of [
['keeps-old-load', /原装载失效/], ['changes-picking', /保留拣货/], ['changes-customer', /客户归属/],
['accept-old-version', /旧版本不能/], ['unrelated-conflict', /旧版本不能/],
['reject-but-write', /拒绝后仍未确认/], ['wrong-final-measurement', /装载确认返回/],
['unknown-assignment-outcome', /connection interrupted/],
]) test(`reassignment stops on ${fault} and retains requests without silently retrying`, async () => {
const server = reassignmentServer(fault)
await assert.rejects(confirmFleetLoading(server.context, 'test', server.proof), message)
assert.equal(server.proof.reassignment.status, 'CHECKING')
assert.equal(server.proof.reassignment.assignmentRequest.vehicleId, '102')
const callCount = server.calls.length
await assert.rejects(confirmFleetLoading(server.context, 'test', server.proof), /已开始/)
assert.equal(server.calls.length, callCount)
})
test('retained dispatch loading does not create replacement resources or mutate assignment', async () => {
const server = reassignmentServer()
delete server.proof.reassignment
const version = await confirmFleetLoading(server.context, 'test', server.proof)
assert.equal(version, 6)
assert.equal(server.calls.length, 3)
assert.ok(server.calls.every(row => (row.path.endsWith('/loading') || row.path.endsWith('/loading-review'))))
})
function closureServer(fault = '') {
const proof = { tripNo: 'TR-ORIGINAL', vehicleId: '9007199254740993', driverId: '9007199254740995', warehouseId: '92', stops }
const trips = new Map([[proof.tripNo, { tripNo: proof.tripNo, recordVersion: 5, vehicleId: proof.vehicleId, driverId: proof.driverId,
status: 'IN_TRANSIT', completedAt: null, vehicleRelease: null, driverRelease: null }]])
const claims = new Map([[`VEHICLE:${proof.vehicleId}`, proof.tripNo], [`DRIVER:${proof.driverId}`, proof.tripNo]])
const remembered = new Map(), calls = [], assertions = []
let sequence = 0
const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) } })
const fail = message => ({ http: 200, body: { code: 'A0409', message } })
const context = {
marker: 'CONTROLLED-TEST',
assert(_module, label, _response, passed) { assertions.push({ label, passed }) },
apiSuccess(_module, _label, response) { return response.body.code === '200' ? response.body.data : null },
async request(method, path, body) {
calls.push({ method, path, body: structuredClone(body) })
if (path.endsWith('/master-data/vehicles')) {
assert.equal(body.status, 'IN_SERVICE', 'the independent driver occupancy probe needs a valid in-service vehicle')
return ok({ id: 'SPARE-VEHICLE' })
}
if (method === 'POST' && path.endsWith('/fleet/trips')) {
const tripNo = `TR-PROBE-${++sequence}`
const trip = { tripNo, recordVersion: 0, status: 'PLANNED', vehicleId: null, driverId: null }
trips.set(tripNo, trip); return ok({ trip })
}
const match = path.match(/\/trips\/([^/]+)(.*)$/), trip = trips.get(match?.[1]), suffix = match?.[2]
assert.ok(trip, `unexpected request ${path}`)
if (method === 'GET' && suffix === '/completion') return ok({ recordVersion: trip.recordVersion, canComplete: true, outcomes: [cargo()] })
if (method === 'GET' && !suffix) return ok({ trip })
if (suffix === '/complete' || suffix === '/resources/release') {
const key = `${trip.tripNo}:${body.idempotencyKey}`
if (remembered.has(key)) {
assert.deepEqual(body, remembered.get(key), 'retry must preserve exact original body')
if (fault === 'old-release-clears-new-claim' && suffix === '/resources/release') claims.delete(`${body.resourceKind}:${body.resourceKind === 'VEHICLE' ? trip.vehicleId : trip.driverId}`)
if (method === 'GET' && path === `${F}/completion`) return ok({ tripNo: 'TR-A', recordVersion: trip.recordVersion, canComplete: fault !== 'incomplete', outcomes: [{ deliveryNo: 'DL-A', packages: 3 }] })
if (method === 'GET' && path === F) return ok({ trip, dispatches: fault === 'wrong-binding' ? [{ dispatchNo: 'DP-OTHER' }] : [{ dispatchNo: 'DP-A' }] })
if (method === 'GET' && path === F.replace('TR-A', 'TR-B')) return ok({ trip: { ...trip, tripNo: 'TR-B' }, dispatches: [{ dispatchNo: 'DP-A' }] })
if (method === 'GET' && path === F.replace('TR-A', 'TR-NEXT')) return ok({ trip: nextTrip, dispatches: [{ dispatchNo: 'DP-NEXT' }] })
if (method === 'POST' && [ `${F}/complete`, `${F}/resources/release` ].includes(path)) {
if (saved.has(body.idempotencyKey)) {
assert.deepEqual(body, saved.get(body.idempotencyKey))
if (fault === 'old-release-clears-new-claim' && nextTrip && path.endsWith('/resources/release')) claims.delete(body.resourceKind)
return ok({ trip })
}
assert.equal(body.recordVersion, trip.recordVersion)
remembered.set(key, structuredClone(body)); trip.recordVersion++
if (suffix === '/complete') { trip.status = 'COMPLETED'; trip.completedAt = '2026-10-02T00:00:00Z' }
else {
const vehicle = body.resourceKind === 'VEHICLE'
claims.delete(`${body.resourceKind}:${vehicle ? trip.vehicleId : trip.driverId}`)
trip[vehicle ? 'vehicleRelease' : 'driverRelease'] = { actualEndAt: body.actualEndAt, endLocation: body.endLocation, vehicleStatus: body.vehicleStatus }
if (fault === 'vehicle-also-frees-driver' && vehicle) claims.delete(`DRIVER:${trip.driverId}`)
assert.equal(body.recordVersion, trip.recordVersion); saved.set(body.idempotencyKey, structuredClone(body)); trip.recordVersion++
if (path.endsWith('/complete')) {
trip.status = 'COMPLETED'; trip.completedAt = '2026-10-19T00:00:00Z'
if (fault === 'completion-auto-releases') { claims.clear(); trip.vehicleRelease = { actualEndAt: trip.completedAt } }
} else {
claims.delete(body.resourceKind)
trip[body.resourceKind === 'VEHICLE' ? 'vehicleRelease' : 'driverRelease'] = { actualEndAt: body.actualEndAt, endLocation: body.endLocation }
if (fault === 'vehicle-frees-driver' && body.resourceKind === 'VEHICLE') claims.delete('DRIVER')
}
return ok({ trip })
}
if (suffix === '/assignment') {
if (fault === 'unrelated-conflict') return fail('车次版本已变化')
if (claims.has(`VEHICLE:${body.vehicleId}`)) return fail('车辆已被其他车次占用')
if (claims.has(`DRIVER:${body.driverId}`)) return fail('司机已被其他车次占用')
assert.equal(body.recordVersion, trip.recordVersion)
trip.vehicleId = body.vehicleId; trip.driverId = body.driverId; trip.recordVersion++
claims.set(`VEHICLE:${body.vehicleId}`, trip.tripNo); claims.set(`DRIVER:${body.driverId}`, trip.tripNo)
return ok({ trip })
}
if (suffix === '/cancel') {
assert.equal(body.recordVersion, trip.recordVersion)
for (const [key, owner] of claims) if (owner === trip.tripNo) claims.delete(key)
trip.status = 'CANCELLED'; trip.recordVersion++; return ok({ trip })
}
throw new Error(`unhandled request ${method} ${path}`)
assert.fail(`retired or unexpected request ${method} ${path}`)
},
}
return { proof, trips, claims, calls, assertions, context }
return { context, detail, trip, calls, assertions, claims, pick() { detail.counts.pickedPackageCount = 3; bump() }, failCheckpoint() { checkpointError = true } }
}
test('closure proves independent resource occupancy through real command-shaped probes, then cancels empty plans', async () => {
const server = closureServer()
const result = await finishFleetTrip(server.context, 'test', server.proof)
assert.equal(result.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
assert.equal(server.claims.size, 0)
assert.ok(server.assertions.every(row => row.passed))
assert.equal(result.probes.length, 2)
assert.ok(result.probes.every(row => row.finalStatus === 'CANCELLED'))
assert.equal(server.calls.filter(row => row.path.endsWith('/resources/release')).length, 4)
assert.equal(server.calls.filter(row => row.path.endsWith('/assignment')).length, 5)
assert.ok(server.calls.every(row => !/\/depart|\/items\/.*\/pick/.test(row.path)), 'closure never replays physical departure or inventory work')
await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /已开始/)
const prepare = s => prepareFleetTrip(s.context, 'native', 'DP-A', '92')
function jacksonCountWire(s) {
const names = new Set(['itemCount', 'palletCount', 'packageCount', 'pickedPackageCount', 'shippedPackageCount', 'scheduledPackages', 'pickedPackages'])
const serialize = value => Array.isArray(value) ? value.map(serialize)
: value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([name, nested]) =>
[name, names.has(name) && typeof nested === 'number' ? String(nested) : serialize(nested)])) : value
const original = s.context.request
s.context.request = async (...args) => serialize(await original(...args))
return s
}
test('real string-zero counts permit preparation, PDA loading replay and safe empty-dispatch closure', async () => {
const s = jacksonCountWire(server()), proof = await prepare(s)
assert.equal(proof.preparation, 'VERIFIED')
s.pick()
await confirmFleetLoading(s.context, 'native', proof, 'PDA-IN-MEMORY-ONLY', receipt())
s.trip.status = 'IN_TRANSIT'
const closure = await finishFleetTrip(s.context, 'native', proof)
assert.equal(closure.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
assert.equal(closure.nextDispatch.finalStatus, 'CANCELLED')
assert.ok(s.assertions.every(row => row.passed))
})
test('closure fails if ending only the vehicle silently releases the driver too', async () => {
const server = closureServer('vehicle-also-frees-driver')
await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /司机仍不能/)
assert.equal(server.proof.closure.status, 'CHECKING')
assert.ok(server.assertions.some(row => row.passed === false))
test('missing or malformed zero counts cannot create resources before preparation', async () => {
for (const value of [undefined, null, false, '', '00', '0.0', '0e0', '9007199254740993']) {
const s = server(); s.detail.counts.pickedPackageCount = value
await assert.rejects(prepare(s), /箱数/)
assert.deepEqual(s.calls.map(row => row.method), ['GET'])
}
})
test('closure detects an old release command deleting a subsequent trip claim', async () => {
const server = closureServer('old-release-clears-new-claim')
await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /旧结束重放不能释放/)
assert.notEqual(server.proof.closure.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
test('native preparation edits existing master then submits both reviews, discovers the real sole automatic Fleet record', async () => {
const s = server(), proof = await prepare(s)
assert.equal(proof.tripNo, 'TR-A'); assert.equal(proof.preparation, 'VERIFIED'); assert.equal(proof.vehicleId, VID)
assert.equal(proof.vehicle.plate, proof.marker); assert.equal(proof.driver.phone, '19900000000')
assert.equal(proof.recordVersion, '9007199254741002'); assert.equal(proof.fleetRecordVersion, 4)
const writes = s.calls.filter(c => ['POST', 'PUT'].includes(c.method))
assert.deepEqual(writes.slice(-4).map(c => c.path), [D, `${D}/submit-review`, `${D}/dispatch-review`, `${D}/loading-review`])
assert.ok(writes.every(c => !/\/trips(?:$|\/.*\/(?:assignment|plan|bind|review))/.test(c.path)))
assert.equal(s.detail.dispatchInfo.fbaAppointments[0].appointmentNo, 'FBA-ORIGINAL')
assert.ok(s.assertions.every(a => a.passed))
})
test('an unrelated conflict cannot stand in for proof of occupied resources', async () => {
const server = closureServer('unrelated-conflict')
await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /未证明资源占用/)
assert.equal(server.calls.some(row => row.path.endsWith('/resources/release')), false)
test('zero-scan LOADING must explicitly return to review before editing and pass a new review round', async () => {
const s = server(); s.detail.stage = 'LOADING'; s.detail.reviewState = 'APPROVED'
const proof = await prepare(s)
const returnIndex = s.calls.findIndex(c => c.path.endsWith('/return-to-review'))
assert.ok(returnIndex >= 0 && returnIndex < s.calls.findIndex(c => c.method === 'PUT'))
assert.equal(proof.returnRequest.recordVersion, '9007199254740998'); assert.equal(proof.preparation, 'VERIFIED')
})
for (const fault of ['missing-trip', 'duplicate-trip', 'wrong-binding']) test(`automatic Fleet identity fails closed: ${fault}`, async () => {
const s = server(fault); await assert.rejects(prepare(s), /唯一|绑定/)
assert.equal(s.context.createdResources.fleetTrips[0].preparation, 'CHECKING')
})
test('started picking cannot be silently prepared, reassigned or returned', async () => {
const s = server(); s.detail.stage = 'LOADING'; s.detail.counts.pickedPackageCount = 1
await assert.rejects(prepare(s), /零扫码/)
assert.equal(s.calls.length, 1)
})
test('unknown write result retains the exact command, does not retry or generate another Fleet record', async () => {
const s = server('unknown-edit'); await assert.rejects(prepare(s), /unknown/)
const last = s.context.createdResources.fleetTripCommands.at(-1)
assert.equal(last.status, 'UNKNOWN'); assert.equal(last.path, D)
assert.equal(s.calls.filter(c => c.method === 'PUT').length, 1)
})
test('checkpoint failure sends no business write', async () => {
const s = server(); s.failCheckpoint(); await assert.rejects(prepare(s), /checkpoint/)
assert.deepEqual(s.calls.map(c => c.method), ['GET'])
})
test('actual loading uses authoritative decimal measurements and a proven producer pallet-space measurement', () => {
assert.deepEqual(controlledPalletMeasurements([pallet()], stops, receipt().pallets), [
{ warehousePalletId: PALLET, palletSpaces: 2, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 },
])
})
for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) test(`missing actual ${field} blocks; billing values cannot substitute`, () => {
const row = pallet(), r = receipt(); row[field] = null; delete r.pallets[0][field]
row.grossWeightKg = 999; row.billingWeight = 999
assert.throws(() => controlledPalletMeasurements([row], stops, r.pallets), /实际测量缺少/)
})
test('suggested measurements, duplicate physical pallets and unpicked cargo are rejected', () => {
const row = pallet(); row.evidence[0].actual = false
assert.throws(() => controlledPalletMeasurements([row], stops, receipt().pallets), /真实来源/)
assert.throws(() => controlledPalletMeasurements([pallet(), pallet()], stops, receipt().pallets), /重复/)
assert.throws(() => controlledPalletMeasurements([{ ...pallet(), pickedPackages: 2 }], stops, receipt().pallets), /核平/)
assert.throws(() => controlledPalletMeasurements([pallet()], [{ name: 'Origin' }], receipt().pallets), /卸货站/)
})
test('extra or inconsistent producer measurements cannot alter another pallet or authoritative actual weight', () => {
const r = receipt(); r.pallets[0].weightKg = 100
assert.throws(() => controlledPalletMeasurements([pallet()], stops, r.pallets), /不一致/)
assert.throws(() => controlledPalletMeasurements([pallet()], stops, [...receipt().pallets, { warehousePalletId: 'OTHER' }]), /本车以外/)
})
test('PDA actual loading and platform exact replay share one native version after proving scanned edit refusal', async () => {
const s = server(), proof = await prepare(s); s.pick()
const beforeCount = s.detail.counts.pickedPackageCount
const v = await confirmFleetLoading(s.context, 'native', proof, 'PDA-IN-MEMORY-ONLY', receipt())
assert.equal(v, '9007199254741004'); assert.equal(proof.reassignment.status, 'REJECTED_WITHOUT_MUTATION')
assert.equal(s.detail.counts.pickedPackageCount, beforeCount); assert.equal(s.detail.stage, 'AFTER_LOADING')
const writes = s.calls.filter(c => c.path.endsWith('/loading-complete'))
assert.equal(writes.length, 2); assert.deepEqual(writes[0].body, writes[1].body)
assert.equal(writes[0].actor, 'PDA'); assert.equal(writes[1].actor, 'PLATFORM')
assert.equal(writes[0].body.measurements[0].weightKg, '187.25')
assert.ok(s.calls.every(c => !/\/fleet\/trips\/.*\/(?:assignment|loading-review|loading)$/.test(c.path)))
const count = s.calls.length
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /已开始/)
assert.equal(s.calls.length, count)
})
for (const fault of ['edit-mutates-on-rejection', 'replay-new-version']) test(`loading verification rejects native drift: ${fault}`, async () => {
const s = server(fault), proof = await prepare(s); s.pick()
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /失败/)
if (fault === 'edit-mutates-on-rejection') assert.equal(s.calls.some(c => c.path.endsWith('/loading-complete')), false)
})
test('missing actual producer measurement blocks before any edit rejection or loading write', async () => {
const s = server(), proof = await prepare(s); s.pick(); const count = s.calls.length
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA'), /palletSpaces/)
assert.equal(s.calls.length, count + 1); assert.equal(s.calls.at(-1).method, 'GET')
})
test('wrong-run producer receipt and unreconciled picking block loading without mutation', async () => {
const s = server(), proof = await prepare(s)
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /未就绪/)
s.pick(); await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', { ...receipt(), marker: 'OTHER-RUN' }), /不属于本轮/)
assert.equal(s.calls.some(c => c.path.endsWith('/loading-complete')), false)
})
test('closure keeps cargo completion, vehicle ending and driver ending distinct using real occupancy owners', async () => {
const s = server(), proof = await prepare(s); s.trip.status = 'IN_TRANSIT'
const state = await finishFleetTrip(s.context, 'native', proof)
assert.equal(state.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'); assert.equal(s.claims.size, 0)
assert.equal(state.subsequentTripClaimProtection, 'VERIFIED_NATIVE_EMPTY_DISPATCH')
assert.equal(state.nextDispatch.tripNo, 'TR-NEXT'); assert.equal(state.nextDispatch.finalStatus, 'CANCELLED')
assert.deepEqual(state.occupancy.map(c => [c.kind, c.rows[0].occupiedTripNo]), [
['VEHICLE', 'TR-A'], ['DRIVER', 'TR-A'], ['VEHICLE', null], ['DRIVER', 'TR-A'], ['VEHICLE', null], ['DRIVER', null],
['VEHICLE', 'TR-NEXT'], ['DRIVER', 'TR-NEXT'], ['VEHICLE', 'TR-NEXT'], ['DRIVER', 'TR-NEXT'], ['VEHICLE', null], ['DRIVER', null],
])
assert.equal(s.calls.filter(c => c.path.endsWith('/resources/release')).length, 4)
assert.ok(s.calls.every(c => !(c.method === 'POST' && /\/fleet\/trips$|\/assignment$/.test(c.path))))
await assert.rejects(finishFleetTrip(s.context, 'native', proof), /已开始/)
})
for (const fault of ['incomplete', 'completion-auto-releases', 'vehicle-frees-driver', 'old-release-clears-new-claim', 'new-empty-has-cargo', 'unknown-next-create']) test(`closure does not claim success on independent-resource failure: ${fault}`, async () => {
const s = server(fault), proof = await prepare(s); s.trip.status = 'IN_TRANSIT'
await assert.rejects(finishFleetTrip(s.context, 'native', proof), /失败|unknown/)
assert.equal(proof.closure.status, 'CHECKING')
if (['new-empty-has-cargo', 'unknown-next-create'].includes(fault)) assert.equal(s.calls.some(c => c.path.endsWith('/DP-NEXT/cancel')), false)
})