Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-trip-fulfillment-contract.test.mjs
T

274 lines
18 KiB
JavaScript

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' })
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' } } }
},
}
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')))
})
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 })
const context = {
marker: 'TEST-REASSIGN',
assert(_module, label, _response, passed) { assertions.push({ label, passed }) },
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 })
},
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' ? '权限未配置' : '车次已被修改,请刷新后重试' } }
}
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())
}
if (path.endsWith('/master-data/vehicles')) {
assert.equal(body.status, 'IN_SERVICE'); assert.equal(body.maxLoadKg, trip.maxLoadKg)
return ok({ id: '102' })
}
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 (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 (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 (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}`)
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}`)
}
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}`)
},
}
return { proof, trips, claims, calls, assertions, context }
}
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), /已开始/)
})
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('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('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)
})