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) })