384 lines
28 KiB
JavaScript
384 lines
28 KiB
JavaScript
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)] },
|
|
] })
|
|
|
|
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: {},
|
|
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
|
|
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 (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) {
|
|
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.deepEqual(body.measurements, [{ warehousePalletId: PALLET, palletSpaces: 2, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }])
|
|
loadingActors.set(body.idempotencyKey, token ? 'PDA' : 'PLATFORM')
|
|
detail.stage = 'DELIVERING'; bump(); return remember('LOADING_COMPLETE', body, { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })
|
|
}
|
|
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')
|
|
|
|
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('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.equal(closure.nextDispatch.vehicle, undefined)
|
|
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(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)
|
|
})
|
|
|
|
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('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)
|
|
})
|