import test from 'node:test' import assert from 'node:assert/strict' import { verifyFleetBookingReservation } from './fleet-booking-reservation-acceptance.mjs' function fixture(fault = '') { const baseline = [ { fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 2, weightKg: 200, volumeCbm: 2 }, remaining: { palletSpaces: 28, weightKg: 17800, volumeCbm: 88 } }, { fromIndex: 1, toIndex: 2, occupied: { palletSpaces: 1, weightKg: 100, volumeCbm: 1 }, remaining: { palletSpaces: 29, weightKg: 17900, volumeCbm: 89 } }, ] const cargo = [ { dispatchNo: 'D-TEST', customerId: '900719925474099301', warehousePalletId: '900719925474099311', palletNo: 'P-A', destinationKey: 'FBA:A' }, { dispatchNo: 'D-TEST', customerId: '900719925474099301', warehousePalletId: '900719925474099312', palletNo: 'P-B', destinationKey: 'FBA:B' }, ] const tripProof = { tripNo: 'T-TEST', dispatchNo: 'D-TEST', vehicleId: 'V-TEST', driverId: 'DR-TEST', stops: [{ name: '起点' }, { name: 'A', destinationKey: 'FBA:A' }, { name: 'B', destinationKey: 'FBA:B' }] } let tripVersion = 10, planned = false, segments = structuredClone(baseline) const bookings = [], bodies = new Map(), calls = [], assertions = [] const response = (data, rejected = false) => ({ http: 200, body: { code: rejected ? 'A0409' : '200', data, message: fault === 'wrong-conflict' ? '版本已变化' : '第1段路线超过车辆托位、重量或容积限制' }, requestId: 'test-request', traceId: fault === 'missing-trace' ? null : 'test-trace' }) const result = (booking, replayed = false) => response({ tripRecordVersion: tripVersion, booking: structuredClone(booking), replayed }) const context = { marker: 'OFFLINE-CONTRACT', createdResources: { fleetTrips: [tripProof] }, assert(module, name, res, passed, evidence, options) { assertions.push({ name, passed, options }) }, async request(method, path, body) { const suffix = path.replace('/api/platform/v1/last-leg/fleet/trips/T-TEST', '') calls.push({ method, suffix, body }) if (method === 'GET' && suffix === '') return response({ trip: { status: 'PLANNED', vehicleId: tripProof.vehicleId, driverId: tripProof.driverId }, dispatches: [{ dispatchNo: 'D-TEST' }] }) if (method === 'GET' && suffix === '/loading') return response({ confirmedLoad: fault === 'fake-loaded' ? {} : null }) if (suffix === '/capacity-plan') { if (method === 'POST') { planned = true; tripVersion++ } return response({ recordVersion: tripVersion, cargo, manifestHash: 'a'.repeat(64), blockReason: null, capacityPlan: planned ? { manifestHash: 'a'.repeat(64) } : null, planValid: planned, availableSegments: structuredClone(segments) }) } if (method === 'GET' && suffix === '/bookings') return response({ tripRecordVersion: tripVersion, records: structuredClone(bookings) }) if (method === 'POST' && suffix === '/bookings') { if (bodies.has(body.idempotencyKey)) { const old = bodies.get(body.idempotencyKey) if (JSON.stringify(old.body) !== JSON.stringify(body)) return response(null, true) return result(old.booking, true) } assert.equal(body.tripRecordVersion, tripVersion) const booking = { bookingNo: `FB-${bookings.length + 1}`, tripNo: tripProof.tripNo, customerId: body.customerId, cargo: body.cargo, status: 'DRAFT', recordVersion: 0 } bookings.push(booking); bodies.set(body.idempotencyKey, { body, booking }); tripVersion++ return result(booking) } const match = suffix.match(/^\/bookings\/(FB-\d+)\/(confirm|cancel)$/) assert.ok(match, `unexpected ${method} ${suffix}`) const booking = bookings.find(row => row.bookingNo === match[1]) if (bodies.has(body.idempotencyKey)) { if (fault === 'replay-reoccupies' && match[2] === 'confirm') segments[0].remaining.palletSpaces-- return result(booking, true) } assert.equal(body.tripRecordVersion, tripVersion) assert.equal(body.recordVersion, booking.recordVersion) if (match[2] === 'confirm') { if (booking.cargo.palletSpaces > segments[0].remaining.palletSpaces) return response(null, true) booking.status = 'CONFIRMED'; booking.agreedAmount = 25 for (const index of (fault === 'wrong-segment' ? [0, 1] : [0])) { segments[index].occupied.palletSpaces++; segments[index].occupied.weightKg++ segments[index].occupied.volumeCbm += 0.01 segments[index].remaining.palletSpaces--; segments[index].remaining.weightKg-- segments[index].remaining.volumeCbm -= 0.01 } } else { if (booking.status === 'CONFIRMED' && fault !== 'cancel-does-not-release') segments = structuredClone(baseline) booking.status = 'CANCELLED' } tripVersion++; booking.recordVersion++; bodies.set(body.idempotencyKey, { body, booking }) return result(booking) }, } const measurements = (actual, stops) => { assert.deepEqual(actual, cargo); assert.deepEqual(stops, tripProof.stops) return cargo.map((row, index) => ({ warehousePalletId: row.warehousePalletId, palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex: index + 1 })) } return { context, tripProof, measurements, calls, bookings, assertions } } test('reservation HTTP helper verifies actual segments, original requests and cancellation without receiving or charging', async () => { const f = fixture() const proof = await verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements) assert.equal(proof.status, 'VERIFIED') assert.equal(proof.bookings.length, 2) assert.ok(f.bookings.every(row => row.status === 'CANCELLED')) assert.deepEqual(proof.finalSegments, proof.initialSegments) assert.ok(proof.requests.every(row => row.requestId && row.traceId)) assert.ok(f.assertions.every(row => row.passed && row.options.deploymentBlocker)) assert.ok(f.calls.every(row => !/incoming|receipt-confirmation|transfer-delivery|departure/.test(row.suffix))) assert.equal(proof.createRequest.customerId, '900719925474099301') await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements), /本轮新建/) }) for (const fault of ['wrong-segment', 'replay-reoccupies', 'cancel-does-not-release', 'fake-loaded', 'missing-trace', 'wrong-conflict']) { test(`reservation helper fails closed on ${fault} even when HTTP reports success`, async () => { const f = fixture(fault) await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements)) assert.equal(f.tripProof.reservation.status, 'CHECKING') assert.ok(f.assertions.some(row => !row.passed && row.options.deploymentBlocker)) if (['wrong-segment', 'replay-reoccupies'].includes(fault)) assert.equal(f.calls.some(row => row.suffix.endsWith('/cancel')), false) }) } test('reservation helper rejects foreign fixture before making any request', async () => { const f = fixture() await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', { ...f.tripProof }, f.measurements), /本轮新建/) assert.equal(f.calls.length, 0) })