Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-trip-fulfillment-contract.test.mjs
T
2026-10-22 11:56:47 +08:00

504 lines
36 KiB
JavaScript

import { LOADING_PHOTO_SLOTS, ensureFleetLoadingDocuments, reuseFleetResources } from './fleet-trip-fulfillment.mjs'
import test from 'node:test'
import assert from 'node:assert/strict'
import { prepareFleetTrip, confirmFleetLoading, controlledPalletMeasurements, finishFleetTrip } from './fleet-trip-fulfillment.mjs'
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)] },
] })
for (const initialStage of ['LOADING', 'AFTER_LOADING']) test(`loading documents fill missing slots without replacing existing files from ${initialStage}`, async () => {
const proof = { marker: 'TEST-DOCS', dispatchNo: 'DP-A' }, calls = [], replays = new Map()
const state = { dispatchNo: 'DP-A', recordVersion: '9007199254740993', dispatchRecordVersion: '11', stage: initialStage,
complete: false, editable: true, photos: [{ slot: LOADING_PHOTO_SLOTS[0], fileId: 'existing' }] }
let saved = false
const context = { async checkpoint() { saved = true },
assert(_m, label, _r, passed) { assert.ok(passed, label) }, apiSuccess(_m, _l, response) { return response.body.data },
async requestMultipart(method, path, file, token) {
assert.ok(saved); saved = false; assert.equal(token, 'PDA-TEST'); assert.equal(method, 'POST')
const slot = path.split('/').at(-2)
assert.match(path, /idempotencyKey=[a-f0-9]{64}$/); assert.equal(file.type, 'image/png')
assert.equal(file.content.subarray(1, 4).toString(), 'PNG'); calls.push(slot)
assert.equal(proof.loadingDocuments.uploads.at(-1).status, 'PREPARED')
return ok({ slot, fileId: `new-${slot}` })
},
async request(method, path, body, token) {
assert.equal(token, 'PDA-TEST')
if (method === 'GET') return ok(state)
assert.equal(method, 'PUT')
if (replays.has(body.idempotencyKey)) return ok(replays.get(body.idempotencyKey))
assert.equal(body.recordVersion, state.recordVersion)
state.photos.push({ slot: path.split('/').at(-1), fileId: body.fileId })
state.recordVersion = String(BigInt(state.recordVersion) + 1n)
state.complete = state.photos.length === 5
if (state.complete && initialStage === 'AFTER_LOADING') state.stage = 'DELIVERING'
replays.set(body.idempotencyKey, structuredClone(state)); return ok(state)
} }
await ensureFleetLoadingDocuments(context, 'docs', proof, 'PDA-TEST')
assert.deepEqual(calls, LOADING_PHOTO_SLOTS.slice(1))
assert.equal(state.photos[0].fileId, 'existing')
assert.equal(state.stage, initialStage === 'LOADING' ? 'LOADING' : 'DELIVERING')
assert.equal(proof.loadingDocuments.result.complete, true)
})
for (const fault of ['checkpoint', 'upload-unknown', 'wrong-slot', 'replay-drift', 'early-departure']) test(`loading document ${fault} stops before loading confirmation`, async () => {
const proof = { marker: 'TEST-DOCS', dispatchNo: 'DP-A' }, calls = []
const state = { dispatchNo: 'DP-A', stage: 'LOADING', recordVersion: '1', complete: false, editable: true, photos: [] }
const context = { async checkpoint() { if (fault === 'checkpoint') throw Error('checkpoint failed') },
assert(_m, label, _r, passed) { assert.ok(passed, label) }, apiSuccess(_m, _l, response) { return response?.body?.data },
async requestMultipart(_method, path) {
calls.push(path)
if (fault === 'upload-unknown') throw Error('unknown upload')
return ok({ slot: fault === 'wrong-slot' ? 'RECEIPT' : 'LOADING_SCENE', fileId: 'new-file' })
},
async request(method, path) {
calls.push(path)
if (method === 'GET') return ok(state)
if (state.photos.length && fault === 'replay-drift') return ok({ ...state, recordVersion: '3' })
state.photos = [{ slot: 'LOADING_SCENE', fileId: 'new-file' }]
if (fault === 'early-departure') state.stage = 'DELIVERING'
return ok(state)
} }
await assert.rejects(ensureFleetLoadingDocuments(context, 'docs', proof, 'PDA-TEST'))
assert.equal(calls.some(path => path.endsWith('/loading-complete')), false)
if (fault === 'checkpoint') assert.equal(calls.length, 1)
if (fault === 'upload-unknown') {
assert.equal(proof.loadingDocuments.uploads[0].status, 'UNKNOWN')
const sent = calls.filter(path => path.includes('/uploads?')).length
await assert.rejects(ensureFleetLoadingDocuments(context, 'docs', proof, 'PDA-TEST'), /不自动重发/)
assert.equal(calls.filter(path => path.includes('/uploads?')).length, sent)
}
})
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)
}
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), /核平/)
})
function server(fault = '') {
const calls = [], assertions = [], saved = new Map(), loadingActors = new Map()
const detail = { dispatchNo: 'DP-A', status: 'ACTIVE', stage: 'BEFORE_LOADING', reviewState: 'DRAFT', recordVersion: '9007199254740998',
counts: { packageCount: 3, pickedPackageCount: 0, shippedPackageCount: 0 }, reviews: {}, publishLooseboardDemand: false,
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 = fault === 'fixed-master', nextPlate = fault === 'fixed-master' ? 'TEST-EXISTING' : '', checkpointError = false, nextTrip, nextDetail
let mutatedItems = false, mutatedLoading = false
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-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 },
async requestMultipart(method, path, file) {
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, token) {
calls.push({ method, path, body: structuredClone(body), actor: token ? 'PDA' : 'PLATFORM' })
if (method === 'GET' && path.endsWith('/loading-documents')) return ok({ dispatchNo: 'DP-A', stage: 'LOADING', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
if (method === 'GET' && path.endsWith(`/master-data/vehicles/${VID}`)) return ok({ id: VID, status: 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
if (method === 'GET' && path.includes(`/fleet/drivers/${DRIVER}?`)) return ok({ id: DRIVER, active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: true, reason: 'ELIGIBLE' } })
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.toUpperCase()
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' })
const wrong = { 'wrong-plate': { vehiclePlate: `${trip.vehiclePlate}-OTHER` },
'wrong-vehicle': { vehicleId: 'OTHER-VEHICLE' }, 'wrong-driver': { driverId: 'OTHER-DRIVER' },
'wrong-warehouse': { warehouseId: 'OTHER-WAREHOUSE' } }[fault]
if (wrong) rows[0] = { ...rows[0], ...wrong }
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: mutatedItems ? 'OTHER-CUSTOMER' : '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: mutatedLoading ? 'CHANGED-MANIFEST' : 'REAL-MANIFEST', loadingComplete: { allowed: detail.counts.pickedPackageCount === 3, blockReason: '拣货未核平' },
pallets: [pallet()], missingFields: ['PLT-A:palletSpaces'] })
}
if (method === 'PUT' && path === D) {
assert.equal(body.publishLooseboardDemand, detail.publishLooseboardDemand)
if (detail.stage !== 'BEFORE_LOADING') {
if (fault === 'edit-mutates-on-rejection') detail.counts.pickedPackageCount--
return conflict('仅装车前可编辑,请显式退回;已经扫码不得改派')
}
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 (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 (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 (method === 'POST' && [D, P].some(prefix => path === `${prefix}/loading-complete`)) {
if (saved.has(body.idempotencyKey)) {
assert.deepEqual(body, saved.get(body.idempotencyKey).body)
if (loadingActors.get(body.idempotencyKey) !== (token ? 'PDA' : 'PLATFORM')) {
if (fault === 'cross-actor-unknown') throw new Error('cross actor result unknown')
if (fault === 'cross-actor-mutates-main') bump()
if (fault === 'cross-actor-mutates-items') mutatedItems = true
if (fault === 'cross-actor-mutates-loading') mutatedLoading = true
if (fault !== 'cross-actor-accepted') return { http: 200, body: { code: 'A0403',
message: fault === 'cross-actor-generic-forbidden' ? '没有装车权限' : '该请求标识不属于当前操作员' } }
}
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.equal('measurements' in body, false)
loadingActors.set(body.idempotencyKey, token ? 'PDA' : 'PLATFORM')
detail.stage = 'DELIVERING'; bump(); return remember('LOADING_COMPLETE', body, { confirmedLoad: { manifestHash: body.manifestHash, pallets: [{ warehousePalletId: PALLET, palletSpaces: 1, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }] } })
}
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 (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); 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 })
}
assert.fail(`retired or unexpected request ${method} ${path}`)
},
}
return { context, detail, trip, calls, assertions, claims, pick() { detail.counts.pickedPackageCount = 3; bump() }, failCheckpoint() { checkpointError = true } }
}
const prepare = s => prepareFleetTrip(s.context, 'native', 'DP-A', '92')
test('review preparation uses the current confirmed appointment when arrival is unset', async () => {
const s = server(), at = new Date(Date.now() + 72 * 3600000).toISOString()
s.detail.dispatchInfo.estimatedArrivalAt = null
s.detail.dispatchInfo.fbaAppointments = [{ status: 'PENDING', confirmedAt: null }, { status: 'SUCCESS', confirmedAt: at }]
const proof = await prepare(s)
assert.equal(proof.updateRequest.estimatedArrivalAt, at)
assert.ok(Date.parse(proof.expectedDriverEndAt) > Date.parse(at))
})
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('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('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.toUpperCase()); 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('fixed customer prepares a real trip without creating vehicles, drivers or uploading licenses', async () => {
const s = server('fixed-master')
s.context.options = { fixedCustomerId: '123', fixedFleetResources: [{ vehicleId: VID, driverId: DRIVER }] }
const proof = await prepare(s)
assert.equal(proof.reusedMasterData, true)
assert.equal(proof.preparation, 'VERIFIED')
assert.equal(proof.vehicleId, VID)
assert.equal(proof.driverId, DRIVER)
assert.equal(s.calls.some(call => call.file), false)
assert.ok(s.calls.filter(call => call.method !== 'GET').every(call => call.path.startsWith(D)))
})
test('fixed fleet resources block missing, locally occupied and ineligible pairs without master writes', async () => {
const pair = { vehicleId: VID, driverId: DRIVER }
for (const fault of ['missing', 'occupied', 'vehicle-status', 'driver-qualification']) {
const proof = {}, calls = []
const context = { options: { fixedFleetResources: fault === 'missing' ? [] : [pair] },
createdResources: { fleetTrips: fault === 'occupied' ? [{ ...pair }] : [proof] },
async request(method, path) {
calls.push(path); assert.equal(method, 'GET')
return path.includes('/vehicles/') ? ok({ id: VID, status: fault === 'vehicle-status' ? 'DISPATCHED' : 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
: ok({ id: DRIVER, active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: fault !== 'driver-qualification', reason: 'ELIGIBLE' } })
}, apiSuccess(_m, _l, response) { return response.body.data } }
await assert.rejects(reuseFleetResources(context, 'test', proof), /固定车辆司机/)
if (['missing', 'occupied'].includes(fault)) assert.equal(calls.length, 0)
}
})
test('verified resource closure makes a fixed pair available again', async () => {
const proof = {}
const context = { options: { fixedFleetResources: [{ vehicleId: VID, driverId: DRIVER }] },
createdResources: { fleetTrips: [{ vehicleId: VID, driverId: DRIVER, closure: { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' } }, proof] },
async request(method, path) {
assert.equal(method, 'GET')
return path.includes('/vehicles/') ? ok({ id: VID, status: 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
: ok({ id: DRIVER, active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: true, reason: 'ELIGIBLE' } })
}, apiSuccess(_m, _l, response) { return response.body.data } }
await reuseFleetResources(context, 'test', proof)
assert.equal(proof.vehicleId, VID)
assert.equal(proof.driverId, DRIVER)
})
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')
})
test('authoritative uppercase vehicle plate discovers both the prepared and subsequent native dispatch without changing marker', async () => {
const s = server(), request = s.context.request, marker = 'FT-1d529b3e-d15'
s.context.request = async (...args) => {
// 固定本轮随机标识以精确复现真实HTTP大小写回执;首次GET仍在任何车辆写入之前。
if (args[0] === 'GET' && args[1] === D && !s.context.createdResources.fleetTrips[0].vehicleId)
s.context.createdResources.fleetTrips[0].marker = marker
return request(...args)
}
const proof = await prepare(s)
assert.equal(proof.marker, marker); assert.equal(proof.vehicle.plate, 'FT-1D529B3E-D15')
assert.equal(proof.tripNo, 'TR-A')
s.trip.status = 'IN_TRANSIT'
const closure = await finishFleetTrip(s.context, 'native', proof)
assert.equal(closure.nextDispatch.marker, marker); assert.deepEqual(closure.nextDispatch.vehicle, proof.vehicle)
assert.equal(closure.nextDispatch.tripNo, 'TR-NEXT'); assert.equal(closure.nextDispatch.finalStatus, 'CANCELLED')
const queries = s.calls.filter(c => c.path.startsWith('/api/platform/v1/last-leg/fleet/trips?'))
assert.equal(queries.length, 2)
for (const call of queries) assert.equal(new URL(`https://unused.test${call.path}`).searchParams.get('keyword'), 'FT-1D529B3E-D15')
})
for (const fault of ['missing-trip', 'duplicate-trip', 'wrong-binding', 'wrong-plate', 'wrong-vehicle', 'wrong-driver', 'wrong-warehouse']) 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 original-actor replay restores loading while another platform actor is rejected without mutation', 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, 'DELIVERING')
const writes = s.calls.filter(c => c.path.endsWith('/loading-complete'))
assert.equal(writes.length, 3)
assert.deepEqual(writes.map(c => c.body), [writes[0].body, writes[0].body, writes[0].body])
assert.deepEqual(writes.map(c => [c.path, c.actor]), [[`${P}/loading-complete`, 'PDA'], [`${P}/loading-complete`, 'PDA'], [`${D}/loading-complete`, 'PLATFORM']])
assert.equal(proof.loadingCommand.replay.replayed, true); assert.equal(proof.loadingCommand.platformRejection.code, 'A0403')
assert.equal(proof.loadingCommand.platformPositiveLoading, 'PENDING_SEPARATE_SCENARIO')
assert.deepEqual(proof.loadingCommand.beforeReplay, proof.loadingCommand.afterReplay)
assert.equal(proof.loadingCommand.platformReplay, undefined)
assert.equal('measurements' in writes[0].body, false)
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)
})
test('platform positive loading remains executable in its own scenario with original-actor replay', async () => {
const s = server(), proof = await prepare(s); s.pick()
await confirmFleetLoading(s.context, 'native', proof, undefined, receipt())
const writes = s.calls.filter(c => c.path.endsWith('/loading-complete'))
assert.deepEqual(writes.map(c => [c.path, c.actor]), [[`${D}/loading-complete`, 'PLATFORM'], [`${D}/loading-complete`, 'PLATFORM']])
assert.equal(proof.loadingCommand.replay.replayed, true)
assert.equal(proof.loadingCommand.platformPositiveLoading, 'VERIFIED'); assert.equal(proof.loadingCommand.platformRejection, undefined)
assert.equal(s.detail.stage, 'DELIVERING')
})
for (const fault of ['cross-actor-accepted', 'cross-actor-generic-forbidden', 'cross-actor-mutates-main', 'cross-actor-mutates-items', 'cross-actor-mutates-loading', 'cross-actor-unknown']) test(`cross-actor loading cannot claim isolation: ${fault}`, async () => {
const s = server(fault), proof = await prepare(s); s.pick()
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /失败|unknown/)
assert.equal(proof.loadingCommand.platformPositiveLoading, undefined)
assert.equal(s.calls.filter(c => c.path.endsWith('/loading-complete')).length, 3)
if (fault === 'cross-actor-unknown') assert.equal(s.context.createdResources.fleetTripCommands.at(-1).status, 'UNKNOWN')
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('scanned loading no longer requires the operator to supply a measurement receipt', async () => {
const s = server(), proof = await prepare(s); s.pick()
await confirmFleetLoading(s.context, 'native', proof, 'PDA')
assert.equal(proof.loadingCommand.result.stage, 'DELIVERING')
assert.equal('measurements' in proof.loadingCommand.body, false)
})
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)
})