import test from 'node:test' import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { verifyFleetHandover, cleanupFleetHandoverResources } from './fleet-handover-acceptance.mjs' const copy = value => value == null ? value : structuredClone(value) function fixture(fault = '') { const marker = 'TEST-HANDOVER-RUN', now = new Date(Date.now() - 60000).toISOString() const proof = { marker: 'FT-CONTROLLED', tripNo: 'TR-ONE', vehicleId: '11', driverId: '21', warehouseId: '92', stops: [{ name: 'Origin' }, { name: 'Destination', destinationKey: 'FBA:ONE' }] } const originalTransport = { vehiclePlate: 'ORIGINAL', driverName: '原司机', driverPhone: '111' } const trip = { tripNo: proof.tripNo, vehicleId: '11', driverId: '21', warehouseId: '92', ...originalTransport, status: 'IN_TRANSIT', recordVersion: '7', departedAt: now, expectedDriverEndAt: new Date(Date.now() + 86400000).toISOString(), maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 } const dispatchDetails = ['A', 'B'].map(customer => ({ dispatchNo: `PC-${customer}`, customerId: customer, overseasWarehouseId: '92', status: 'READY_TO_DEPART', packageCount: 10, pickedPackageCount: 10, shippedPackageCount: 0, ...originalTransport, items: [{ id: `ITEM-${customer}`, warehousePalletId: 'PALLET-INTERNAL', inboundCode: `IN-${customer}`, packageCount: 10 }] })) const liveDispatches = dispatchDetails.map(({ customerId, ...row }) => ({ ...row, status: 'DEPARTED', shippedPackageCount: 10 })) const cargo = ['A', 'B'].map(customer => ({ customerId: customer, dispatchNo: `PC-${customer}`, dispatchItemId: `ITEM-${customer}`, warehousePalletId: 'PALLET-INTERNAL', palletNo: 'PAL-REAL', packages: 10, destinationKey: 'FBA:ONE' })) const originalHistory = { action: 'DEPART', recordVersion: '7', snapshot: { trip: copy(trip), confirmedLoad: { cargo, pallets: [{ warehousePalletId: 'PALLET-INTERNAL', palletSpaces: 1, weightKg: 100, volumeCbm: 1 }] } } } const resources = new Map([['VEHICLE:11', 'TR-ONE'], ['DRIVER:21', 'TR-ONE']]) const vehicles = new Map(), drivers = new Map(), probes = new Map(), calls = [] let createdVehicles = 0, handover = null, preview = null const ok = data => ({ http: 200, body: { code: '200', data: copy(data) }, requestId: 'request', traceId: 'trace' }) const conflict = message => ({ http: 200, body: { code: 'A0409', message }, requestId: 'request', traceId: 'trace' }) const bump = () => { trip.recordVersion = String(BigInt(trip.recordVersion) + 1n) } const bindings = dispatchDetails.map(row => ({ dispatchNo: row.dispatchNo, customerId: row.customerId })) if (fault === 'binding-customer') bindings[0].customerId = 'OTHER' if (fault === 'dispatch-cargo') liveDispatches[0].items = [{ ...liveDispatches[0].items[0], id: 'OTHER-ITEM' }] const context = { context: { async checkpointFleetHandover() {} }, marker, createdResources: { fleetTrips: [proof], fleetAcceptance: { marker, tripNo: proof.tripNo, departure: { tripNo: proof.tripNo, status: 'IN_TRANSIT', departedAt: now, shippedPackages: 20, dispatches: dispatchDetails.map(row => ({ dispatchNo: row.dispatchNo, shippedPackages: 10 })) } } }, assert(_module, label, _response, passed) { calls.push({ assertion: label, passed }) }, async requestMultipart(method, path, file) { calls.push({ method, path, certificate: file.name }); assert.ok(file.name.includes('TEST-NOT-REAL')); assert.ok(file.content.byteLength > 0) drivers.get('22').recordVersion = '1'; return ok({ id: '22', recordVersion: '1', licenseFileId: 'TEST-LICENSE-FILE' }) }, async request(method, path, body, token) { calls.push({ method, path, body: copy(body), token }) if (path.includes('/finance-fee-sources?')) { const customer = new URLSearchParams(path.split('?')[1]).get('customerId') return ok([{ sourceFeeId: `FEE-${customer}`, sourceVersion: 1, sourceStatus: 'ACTIVE', feeItemCode: 'DELIVERY_FEE', direction: 'RECEIVABLE', originalCurrency: 'CNY', originalAmount: fault === 'fee-changed' && handover ? 99 : 10 }]) } if (path.includes('/handover-status')) { assert.equal(token, 'picker-secret') const dispatchNo = path.split('/').at(-2), customer = dispatchNo.slice(-1) const current = handover && fault !== 'pda-keeps-original' ? handover.snapshot.replacement : originalTransport return ok({ dispatchNo, tripNo: trip.tripNo, originalDeparture: fault === 'rewritten-pda-departure' && handover ? current : originalTransport, deliveries: [{ deliveryNo: `DL-${customer}`, status: 'WAIT_DELIVERY', packages: 10, terminalAt: null, transport: { vehiclePlate: current.vehiclePlate, driverName: current.driverName, driverPhone: current.driverPhone } }], latestHandover: handover ? { handoverNo: handover.handoverNo } : null }) } if (path.startsWith('/api/platform/v1/last-leg/dispatches/')) return ok(liveDispatches.find(row => row.dispatchNo === path.split('/').at(-1))) if (path.endsWith('/master-data/vehicles') && method === 'POST') { const vehicle = { ...body, id: String(12 + createdVehicles++), recordVersion: '0' }; vehicles.set(vehicle.id, vehicle); return ok(vehicle) } if (path.includes('/master-data/vehicles/')) { const id = path.match(/\/vehicles\/([^/]+)/)[1], vehicle = vehicles.get(id) if (method === 'PATCH') { vehicle.status = body.status; vehicle.recordVersion = '1' } return ok(vehicle) } if (path.endsWith('/fleet/drivers') && method === 'POST') { const driver = { id: '22', recordVersion: '0', details: body, active: true }; drivers.set(driver.id, driver); return ok(driver) } if (path.includes('/fleet/drivers/')) { const driver = drivers.get('22') if (path.endsWith('/review')) { driver.reviewStatus = 'APPROVED'; driver.recordVersion = '2' } if (path.endsWith('/status')) { driver.active = body.active; driver.recordVersion = '3' } return ok(driver) } if (path.endsWith('/fleet/trips') && method === 'POST') { const next = { ...body, tripNo: `PROBE-${probes.size + 1}`, status: 'PLANNED', recordVersion: '0' }; probes.set(next.tripNo, next); return ok({ trip: next, dispatches: [] }) } if (path.includes('/trips/PROBE-')) { const number = path.match(/\/trips\/([^/]+)/)[1], probe = probes.get(number) if (path.endsWith('/assignment')) { if (resources.has(`VEHICLE:${body.vehicleId}`)) return conflict('车辆已被车次占用') if (resources.has(`DRIVER:${body.driverId}`)) return conflict('司机已被车次占用') resources.set(`VEHICLE:${body.vehicleId}`, number); resources.set(`DRIVER:${body.driverId}`, number) Object.assign(probe, { vehicleId: body.vehicleId, driverId: body.driverId, recordVersion: '1' }) } if (path.endsWith('/cancel')) { for (const [resource, owner] of resources) if (owner === number) resources.delete(resource) probe.status = 'CANCELLED' } return ok({ trip: probe, dispatches: [] }) } if (path.includes('/history?')) { const departure = copy(originalHistory) if (handover && fault === 'rewritten-departure') departure.snapshot.trip.vehiclePlate = 'WRONG' const records = handover ? [departure, { action: 'HANDOVER', recordVersion: '8' }] : [departure] return ok({ records, total: records.length }) } if (path.endsWith('/handovers/preview')) { preview = { cargoHash: 'a'.repeat(64), driverQualification: { canDispatch: true }, cargo: { remainingCargo: cargo.map(row => ({ customerId: row.customerId, dispatchNo: row.dispatchNo, itemId: row.dispatchItemId, palletId: row.warehousePalletId, destinationKey: row.destinationKey, packages: row.packages })), completedDeliveries: [], remainingPalletIds: ['PALLET-INTERNAL'], occupied: { palletSpaces: 1, weightKg: 100, volumeCbm: 1 } } } if (fault === 'wrong-customer') preview.cargo.remainingCargo[0].customerId = 'OTHER' if (fault === 'double-count-pallet') preview.cargo.occupied.palletSpaces = 2 return ok(preview) } if (path.endsWith('/handovers') && method === 'POST') { if (handover && body.idempotencyKey === proof.handover.confirmRequest.idempotencyKey) return ok(handover) if (String(body.selection.recordVersion) !== trip.recordVersion) { if (fault === 'stale-accepted') return ok(handover) return conflict(fault === 'unrelated-conflict' ? '费用正在处理中' : '车次已变化,请刷新后重新核对') } handover = { handoverNo: 'HO-REAL', tripNo: trip.tripNo, recordVersion: '0', snapshot: { previous: { vehicleId: '11', driverId: '21', ...originalTransport }, replacement: { vehicleId: '12', driverId: '22', vehiclePlate: vehicles.get('12').plateNumber, driverName: '接替司机', driverPhone: '222' }, cargo: preview.cargo }, vehicleRelease: null, driverRelease: null } Object.assign(trip, handover.snapshot.replacement); bump() resources.set('VEHICLE:12', 'TR-ONE'); resources.set('DRIVER:22', 'TR-ONE') if (fault === 'unknown-confirmation') throw new Error('connection lost after commit') return ok(handover) } if (path.includes('/handovers?')) return ok({ records: handover ? [handover] : [], total: handover ? 1 : 0 }) if (path.endsWith('/resources/release')) { const vehicle = body.resourceKind === 'VEHICLE', field = vehicle ? 'vehicleRelease' : 'driverRelease', resource = vehicle ? 'VEHICLE:11' : 'DRIVER:21' if (handover[field]) { if (fault === 'replay-releases-next') resources.delete(resource) return ok(handover) } assert.equal(String(body.recordVersion), handover.recordVersion) resources.delete(resource); handover[field] = { actualEndAt: body.actualEndAt, location: body.location } handover.recordVersion = String(BigInt(handover.recordVersion) + 1n); bump(); return ok(handover) } if (path.endsWith('/trips/TR-ONE') && method === 'GET') return ok({ trip, dispatches: bindings }) throw new Error(`Unexpected test route ${method} ${path}`) }, } return { context, proof, dispatchDetails, calls, trip, resources, probes, vehicles, drivers } } test('fresh real departure carries all customer cargo, preserves DEPART and fees, and verifies both old-resource releases', async () => { const f = fixture(), result = await verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails) assert.equal(result.status, 'VERIFIED'); assert.equal(result.pdaReadCount, 4) assert.equal(result.browser.originalVehiclePlate, 'ORIGINAL'); assert.match(result.browser.replacementVehiclePlate, /^HO-/) assert.equal(f.proof.vehicleId, '12'); assert.equal(f.proof.driverId, '22'); assert.equal(f.proof.licenseFileId, 'TEST-LICENSE-FILE') assert.equal(result.replacement.certificate.synthetic, true); assert.ok(result.replacement.certificate.contentBase64) assert.doesNotMatch(JSON.stringify(result), /picker-secret/) assert.ok(f.calls.filter(row => row.assertion).every(row => row.passed)) assert.ok(!f.calls.some(row => row.path?.endsWith('/depart') || row.path?.endsWith('/complete') || row.path?.endsWith('/cancel'))) assert.equal(f.resources.get('VEHICLE:11'), 'PROBE-1'); assert.equal(f.resources.get('DRIVER:21'), 'PROBE-1') }) test('marker mismatch, unregistered copied proof and missing actual departure fail without HTTP', async () => { for (const fault of ['marker', 'unregistered', 'departure']) { const f = fixture(); let proof = f.proof if (fault === 'marker') f.context.createdResources.fleetAcceptance.marker = 'ANOTHER-RUN' if (fault === 'unregistered') proof = structuredClone(proof) if (fault === 'departure') delete f.context.createdResources.fleetAcceptance.departure await assert.rejects(verifyFleetHandover(f.context, 'test', proof, 'picker-secret', f.dispatchDetails), /本轮|当前marker/) assert.equal(f.calls.length, 0) } }) test('unknown successful write preserves the original command and blocks automatic rerun and cleanup', async () => { const f = fixture('unknown-confirmation') const saved = [] f.context.context.checkpointFleetHandover = async proof => saved.push(copy(proof)) await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), /connection lost/) assert.equal(f.proof.handover.status, 'FAILED'); assert.ok(f.proof.handover.confirmRequest.idempotencyKey) assert.equal(f.proof.handover.requests.at(-1).status, 'UNKNOWN_RESULT') const sending = saved.find(row => row.requests.at(-1).path.endsWith('/handovers') && row.requests.at(-1).status === 'SENDING') assert.deepEqual(sending.requests.at(-1).body, f.proof.handover.confirmRequest) assert.equal(saved.at(-1).requests.at(-1).status, 'UNKNOWN_RESULT') assert.ok(!JSON.stringify(saved).includes('picker-secret')) const calls = f.calls.length await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), /不自动续跑/) await assert.rejects(cleanupFleetHandoverResources(f.context, 'test', f.proof), /仅允许/) assert.equal(f.calls.length, calls); assert.equal(f.proof.vehicleId, '11') }) test('missing persistence and failed pre-send checkpoints cannot send HTTP', async () => { const missing = fixture(); delete missing.context.context.checkpointFleetHandover await assert.rejects(verifyFleetHandover(missing.context, 'test', missing.proof, 'picker-secret', missing.dispatchDetails), /持久化/) assert.equal(missing.calls.length, 0) for (const target of ['vehicle', 'multipart']) { const f = fixture() let blocked f.context.context.checkpointFleetHandover = async proof => { const entry = proof.requests.at(-1) if (entry.status === 'SENDING' && (target === 'multipart' ? entry.file : entry.method === 'POST')) { blocked = copy(entry); throw Error('disk full') } } await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), /disk full/) assert.ok(blocked) assert.ok(!f.calls.some(row => row.method === blocked.method && row.path === blocked.path)) if (target === 'multipart') { assert.match(blocked.file.sha256, /^[a-f0-9]{64}$/); assert.ok(blocked.file.bytes > 0) assert.equal(blocked.file.content, undefined) } } }) test('every HTTP call observes a completed pre-send checkpoint; post-response disk failure stops the chain', async () => { const f = fixture(), original = f.context.request.bind(f.context), saved = [] f.context.context.checkpointFleetHandover = async proof => { const entry = proof.requests.at(-1) if (entry.status === 'RESPONDED' && entry.method === 'POST') throw Error('response disk failure') saved.push(copy(proof)) } f.context.request = async (...args) => { const entry = saved.at(-1).requests.at(-1) assert.equal(entry.status, 'SENDING'); assert.equal(entry.path, args[1]); assert.deepEqual(entry.body, args[2]) return original(...args) } await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), /response disk failure/) assert.equal(f.calls.filter(row => row.method === 'POST').length, 1) assert.equal(saved.at(-1).requests.at(-1).status, 'SENDING') }) for (const [fault, message] of Object.entries({ 'binding-customer': '每张排车已发车', 'dispatch-cargo': '每张排车已发车', 'wrong-customer': '预览精确包含', 'double-count-pallet': '共享实托只计一次', 'stale-accepted': '陈旧版本交接必须拒绝', 'unrelated-conflict': '陈旧版本交接必须拒绝', 'pda-keeps-original': 'PDA保持原发车资源', 'rewritten-pda-departure': 'PDA保持原发车资源', 'fee-changed': '交接不新增或改写派送费用', 'rewritten-departure': '只新增一次交接且原DEPART内容保持不变', 'replay-releases-next': '旧请求重放不能释放下一计划车辆' })) { test(`business counterexample ${fault} cannot be reported as successful handover`, async () => { const f = fixture(fault) await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), new RegExp(message)) assert.equal(f.proof.handover.status, 'FAILED'); assert.ok(f.proof.handover.requests.length > 0) assert.ok(!f.calls.some(row => row.path?.endsWith('/cancel'))) }) } test('cleanup cannot run before genuine cargo completion and current-resource release', async () => { const f = fixture(); await verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails) const before = f.calls.length await assert.rejects(cleanupFleetHandoverResources(f.context, 'test', f.proof), /整车结案/) assert.ok(f.calls.slice(before).filter(row => row.method).every(row => row.method === 'GET')) assert.equal(f.proof.handover.cleanup.status, 'FAILED') }) test('missing diagnostic correlation blocks the run before creating replacement resources', async () => { const f = fixture(), original = f.context.request.bind(f.context) f.context.request = async (...args) => { const response = await original(...args); delete response.traceId; return response } await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), /读取真实在途车次/) assert.equal(f.vehicles.size, 0); assert.equal(f.proof.handover.requests.length, 1) assert.equal(f.proof.handover.requests[0].status, 'RESPONDED') }) test('explicit closure cleanup cancels only test probes and disables newly created resources without deletion', async () => { const f = fixture(); await verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails) f.trip.status = 'COMPLETED'; f.trip.vehicleRelease = { actualEndAt: new Date().toISOString() }; f.trip.driverRelease = { actualEndAt: new Date().toISOString() } f.resources.delete('VEHICLE:12'); f.resources.delete('DRIVER:22') const result = await cleanupFleetHandoverResources(f.context, 'test', f.proof) assert.equal(result.status, 'COMPLETED'); assert.ok([...f.probes.values()].every(row => row.status === 'CANCELLED')) assert.ok([...f.vehicles.values()].every(row => row.status === 'INACTIVE')); assert.equal(f.drivers.get('22').active, false) assert.ok(!f.calls.some(row => row.method === 'DELETE')) }) test('internal helper has no environment loader, SQL or standalone execution path', async () => { const source = await readFile(new URL('./fleet-handover-acceptance.mjs', import.meta.url), 'utf8') assert.doesNotMatch(source, /process\.env|process\.argv|fetch\(|execFile|INSERT INTO|UPDATE ll_|DELETE FROM/) assert.match(source, /export async function verifyFleetHandover\(context, module, tripProof, pickerToken, dispatchDetails\)/) })