Files
baiyue-firstleg/scripts/prod-release-overseas-stage.test.mjs

534 lines
38 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import { loadingVerificationResult } from './prod-release-full-flow.mjs'
import assert from 'node:assert/strict'
import { mkdtemp, writeFile, rm, readFile, rmdir } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import { overseasCommands, runOverseasStage, validateOverseasInput, finishSingleOrderTrip, validateSyntheticLoadingInput } from './prod-release-overseas-stage.mjs'
const input = { environment: { mode: 'test', apiBase: 'https://test.example.invalid/api' }, batch: 'release-test-001', customerId: '1', warehouseId: '2', packageCount: 1,
resources: { vehicleId: '3', driverId: '4', carrierName: '验收既有车队' } }
const domestic = { containerBookingId: '5', sourceInboundId: '6', orderId: '7', containerNo: 'TEST0000001',
inboundCode: 'IN-TEST', orderNo: 'ORDER-TEST', cartonMarks: ['CARTON-TEST'] }
const ok = data => ({ http: 200, body: { code: '200', data } })
const journal = () => { const state = {}; return { state, save: async patch => Object.assign(state, patch) } }
test('reject multi-ticket quantities and duplicate frozen cartons before HTTP', () => {
assert.throws(() => validateOverseasInput({ ...input, packageCount: 3 }, domestic), /1~2/)
assert.throws(() => validateOverseasInput({ ...input, packageCount: 2 }, { ...domestic, cartonMarks: ['a', 'a'] }), /箱码/)
})
test('command records intent before HTTP; uncertain result blocks resend', async () => {
const checkpoint = journal(); let calls = 0
const { command } = overseasCommands({ request: async () => {
calls++; assert.equal(checkpoint.state.commands.create.status, 'INTENT'); throw new Error('timeout')
} }, checkpoint)
await assert.rejects(command('create', 'POST', '/example', { x: 1 }), /timeout/)
await assert.rejects(command('create', 'POST', '/example', { x: 1 }), /结果未知/)
assert.equal(calls, 1)
})
test('completed command restores result without HTTP and rejects changed request', async () => {
const checkpoint = journal(); let calls = 0
const { command } = overseasCommands({ request: async () => { calls++; return ok({ id: '99' }) } }, checkpoint)
await command('create', 'POST', '/example', { x: 1 })
assert.deepEqual(await command('create', 'POST', '/example', { x: 1 }), { id: '99' })
await assert.rejects(command('create', 'POST', '/example', { x: 2 }), /不符/)
assert.equal(calls, 1)
})
test('unknown prior command blocks even read/other writes on continuation', async () => {
const checkpoint = journal(); checkpoint.state.commands = { lost: { status: 'INTENT' } }
await assert.rejects(runOverseasStage({ input, domestic, checkpoint,
context: { overseasPdaToken: 'pda', merchantToken: 'merchant', request: () => assert.fail('must not call HTTP') } }), /结果未知/)
})
test('only initial current receiving probe accepts HTTP 200 A0404 as no session', async () => {
const calls = []
const helpers = overseasCommands({ request: async (method, path, body, token) => {
calls.push({ method, path, token }); return { http: 200, body: { code: 'A0404' } }
} }, journal())
assert.equal(await helpers.readCurrentReceivingSession('pda'), null)
assert.deepEqual(calls[0], { method: 'GET', path: '/api/pda/v1/last-leg/receiving-containers/current', token: 'pda' })
await assert.rejects(helpers.read('/api/pda/v1/last-leg/receiving-containers/current', 'pda'), /A0404/)
await assert.rejects(helpers.read('/api/platform/v1/last-leg/orders/1'), /A0404/)
})
test('current receiving probe preserves auth, transport and other business errors', async () => {
for (const [http, code] of [[200, 'A0403'], [200, 'A0500'], [401, 'A0404'], [404, 'A0404'], [503, 'A0404']]) {
const helpers = overseasCommands({ request: async () => ({ http, body: { code, requestId: 'request-test' } }) }, journal())
await assert.rejects(helpers.readCurrentReceivingSession('pda'), /request-test/)
}
})
function fixture({ foreignSource = false, destinationType = 'OVERSEAS', missingActual = false, foreignCargo = false,
foreignTrip = false, candidateComplete = true, loseCommand = null, missingAppointment = false } = {}) {
const checkpoint = journal(), calls = []
const session = { containerNo: domestic.containerNo, overseasWarehouseId: '2', totalPackages: 1, pendingPackages: 0,
inbounds: [{ inboundCode: domestic.inboundCode, destinationType }] }
const dispatchNo = 'DISPATCH-TEST', dispatchPath = `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, pdaPath = `/api/pda/v1/last-leg/dispatches/${dispatchNo}`
let stage = 'BEFORE_LOADING', recordVersion = 0, picked = 0, created = false, receivingRead = 0, documentVersion = 0
const loadingPhotos = []
let savedMaintenance = { fleetEmail: 'synthetic@example.invalid', destinationAppointments: [] }, savedVehicle = { mode: 'MASTER', vehicleId: '3' }, savedDriver = { mode: 'MASTER', driverId: '4' }
let plannedDepartureAt = null, estimatedArrivalAt = null
const appointments = () => missingAppointment ? [{ destinationKey: 'FBA:DEST', destinationWarehouseCode: 'DEST', appointmentNo: savedMaintenance.destinationAppointments[0]?.appointmentNo ?? null, status: savedMaintenance.destinationAppointments.length ? 'REGISTERED' : 'PENDING' }] : []
const stops = [{ name: 'Origin' }, { name: 'Destination', destinationKey: 'FBA:DEST' }]
const counts = () => ({ packageCount: 1, pickedPackageCount: picked, shippedPackageCount: stage === 'DELIVERING' ? 1 : 0 })
const nativeResult = result => ({ dispatchNo, status: 'ACTIVE', stage, recordVersion, counts: counts(), result })
const ownItem = () => ({ dispatchItemId: '12', customerId: foreignCargo ? '99' : '1', inboundCode: domestic.inboundCode,
warehousePalletId: '9', palletNo: 'PLT-TEST', scheduledPackages: 1, pickedPackages: picked,
positions: [{ positionId: '13', availablePackages: 1 - picked }] })
const delivery = { customerId: '1', outboundNo: 'OUT-TEST', deliveryNo: 'DELIVERY-TEST', packageCount: 1, destination: { key: 'FBA:DEST' } }
const group = { groupKey: 'GROUP-1', groupCargoHash: 'group-hash', customerId: '1', destination: { key: 'FBA:DEST' },
cargo: [{ customerId: '1', inboundCode: domestic.inboundCode }], requiredServices: ['DELIVERY_FEE'], blockReasons: [],
externalQuotes: [], externalFreightHash: 'freight-hash', feeCoverageCargoHash: 'coverage-hash' }
const trip = () => ({ trip: { tripNo: 'TRIP-TEST', vehicleId: '3', driverId: '4', warehouseId: '2', vehiclePlate: 'TEST',
tripPurpose: 'DELIVERY', recordVersion: 1, stops }, dispatches: [{ dispatchNo: foreignTrip ? 'OTHER-DISPATCH' : dispatchNo }] })
const measured = () => ({ warehousePalletId: '9', palletNo: 'PLT-TEST', scheduledPackages: 1, pickedPackages: picked,
palletSpaces: missingActual ? null : 2, weightKg: 17.75, volumeCbm: '0.072', deliveryIndex: 1,
evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].filter(field => !missingActual || field !== 'palletSpaces').map(field => ({ field, actual: true,
sourceType: 'WAREHOUSE_MEASUREMENT', sourceBusinessNo: 'RECEIPT-TEST', observedAt: '2026-10-19T10:00:00Z',
value: { palletSpaces: 2, weightKg: 17.75, volumeCbm: '0.072' }[field] })) })
const context = { token: 'admin', overseasPdaToken: 'pda', merchantToken: 'merchant', wait: async () => {},
requestWithBody: async (method, path, form, token) => {
calls.push({ method, path, token }); assert.ok(form instanceof FormData)
if (path.includes('/evidence/')) { assert.equal(token, 'admin'); return ok({ fileId: '10' }) }
assert.equal(token, 'pda')
if (path.includes('/loading-documents/')) return ok({ slot: path.split('/loading-documents/')[1].split('/')[0], fileId: '10' })
return ok({ fileId: '10' })
}, request: async (method, path, body, token) => {
calls.push({ method, path, body: body == null ? body : structuredClone(body), token })
if (path.includes('/master-data/vehicles/')) return ok({ id: '3', region: 'OVERSEAS', status: 'IN_SERVICE', plateNumber: 'TEST' })
if (path.includes('/fleet/drivers/')) return ok({ id: '4', qualification: { canDispatch: !created }, details: { name: 'Driver', phone: '10000000000' } })
if (path.includes('/orders?containerNo=')) return ok({ records: [{ sourceContainerBookingId: '5', customerId: foreignSource ? '99' : '1', sourceInboundId: '6',
inboundCode: domestic.inboundCode, overseasWarehouseId: '2', id: '8' }] })
if (path.endsWith('/container-arrival-times/query')) return ok([{ sourceContainerBookingId: '5', version: 0 }])
if (path.includes('/container-preview?')) return ok({ ready: true, totalOrderCount: 1, pendingOrderCount: 1, snapshotVersion: 1, snapshotHash: 'hash' })
if (path.endsWith('/convert-container')) return ok([{ lines: [{ inboundCode: domestic.inboundCode }], pallets: [{ palletNo: 'PLT-TEST', warehousePalletId: '9' }] }])
if (path.endsWith('/current') && method === 'GET') return ++receivingRead === 1 ? { http: 200, body: { code: 'A0404' } } : ok(session)
if (path.includes('/receiving-containers/TEST0000001?')) return ok(session)
if (path.endsWith('/pallets/resolve')) return ok({ compatible: true })
if (path.endsWith('/cartons/resolve')) return ok({ inboundCode: domestic.inboundCode })
if (path.endsWith('/terminal-drayages/5')) return ok({ containerBookingId: '5', status: 'UNLOADED',
version: 1, unloadingCompletedAt: '2026-10-12T12:00:00', allowedActions: ['RECORD_EMPTY_RETURN'] })
if (path.endsWith('/empty-return')) return ok({ status: 'EMPTY_RETURNED' })
if (path.includes('/dispatches/candidates?')) return ok({ complete: candidateComplete, candidates: [{ inboundCode: domestic.inboundCode,
customerId: '1', warehouseId: '2', palletNo: 'PLT-TEST', availablePackages: 1, sourceVersion: 'real-source', blockReasons: [],
selection: { sourceType: 'INBOUND_PLAN', planPalletId: '11', sourceVersion: 'real-source' } }] })
if (path === '/api/platform/v1/last-leg/dispatches' && method === 'POST') { created = true; return ok(nativeResult(null)) }
if (path.startsWith('/api/platform/v1/last-leg/fleet/trips?')) return ok({ records: [trip().trip], total: 1, page: 1, size: 200 })
if (path.endsWith('/fleet/trips/TRIP-TEST')) return ok(trip())
if (path === dispatchPath && method === 'PUT') {
assert.equal(body.recordVersion, recordVersion); recordVersion++
savedVehicle = structuredClone(body.vehicle); savedDriver = structuredClone(body.driver)
plannedDepartureAt = body.plannedDepartureAt; estimatedArrivalAt = body.estimatedArrivalAt
if (body.maintenance) savedMaintenance = structuredClone(body.maintenance)
if (body.idempotencyKey.endsWith('-destination-appointment') && loseCommand === 'appointment') throw Error('appointment response lost')
return ok(nativeResult(null))
}
if (path === dispatchPath && method === 'GET') return ok({ ...nativeResult(null), pickingReconciled: picked === 1,
documents: { dispatchNo, outbounds: stage === 'DELIVERING' ? [{ customerId: '1', outboundNo: 'OUT-TEST' }] : [], deliveries: stage === 'DELIVERING' ? [delivery] : [] },
orderDetails: [{ inboundCode: domestic.inboundCode, customerId: '1' }],
vehicleInfo: { vehicle: savedVehicle, driver: savedDriver, maintenance: savedMaintenance },
dispatchInfo: { fbaAppointments: appointments(), plannedDepartureAt, estimatedArrivalAt }, actions: { DEPART: { allowed: stage === 'AFTER_LOADING' } } })
if (path === `${dispatchPath}/items`) return ok({ dispatchNo, recordVersion, items: [ownItem()] })
if (path === pdaPath && method === 'GET') return ok({ ...nativeResult(null), warehouseId: '2', pickingReconciled: picked === 1,
items: [ownItem()], actions: { DEPART: { allowed: stage === 'AFTER_LOADING' } } })
if (path.startsWith(`${pdaPath}/loading-documents`)) {
if (method === 'PUT') { assert.equal(body.recordVersion, String(documentVersion)); documentVersion++; loadingPhotos.push({ slot: path.split('/').at(-1), fileId: body.fileId }) }
return ok({ dispatchNo, stage, recordVersion: String(documentVersion), dispatchRecordVersion: String(recordVersion), editable: true, complete: loadingPhotos.length === 5, photos: [...loadingPhotos] })
}
if (path === `${pdaPath}/loading` && method === 'GET') return ok({ ...nativeResult(null), pickingReconciled: picked === 1,
loadingComplete: { allowed: picked === 1 }, manifestHash: 'real-manifest', pallets: [measured()] })
if (path === `${dispatchPath}/delivery-arrangement`) return ok({ ...nativeResult(null), arrange: { allowed: true },
cargoHash: 'real-cargo', loadingCompletionHash: 'real-load', groups: [group] })
if (method === 'POST' && (path.startsWith(`${dispatchPath}/`) || path.startsWith(`${pdaPath}/`))) {
assert.equal(body.recordVersion, recordVersion, `must use current main version: ${path}`)
recordVersion++
if (path.endsWith('/loading-review')) stage = 'LOADING'
if (path.endsWith('/pick')) { assert.equal(stage, 'LOADING'); picked = 1 }
if (path.endsWith('/loading-complete')) {
assert.deepEqual(Object.keys(body).sort(), ['idempotencyKey','manifestHash','recordVersion','remark'])
assert.equal(loadingPhotos.length, 5); stage = 'DELIVERING'; return ok(nativeResult({ confirmedLoad: { pallets: [{ ...measured(), palletSpaces: 2 }] } }))
}
if (path.endsWith('/transfer-delivery')) {
assert.equal(token, 'admin'); assert.equal(stage, 'AFTER_LOADING'); assert.equal(body.groups.length, 1)
assert.equal(body.groups[0].fees[0].currency, 'USD'); assert.equal(body.groups[0].fees[0].amount, 10)
return ok(nativeResult({ deliveries: [delivery], outbounds: [{ customerId: '1', outboundNo: 'OUT-TEST' }] }))
}
if (path.endsWith('/depart')) { stage = 'DELIVERING'; return ok(nativeResult({ shippedPackages: 1, deliveries: [delivery] })) }
if (path.endsWith(loseCommand || 'NOT_A_COMMAND')) throw Error('simulated response loss')
return ok(nativeResult(null))
}
if (path.includes('/outbound-orders/')) return ok({ outbound: { outboundNo: 'OUT-TEST', overseasWarehouseId: '2', dispatchNo, status: 'OUTBOUNDED', packageCount: 1 }, deliveries: [delivery], pallets: [{ inboundCode: domestic.inboundCode }] })
if (method === 'GET') assert.fail(`unexpected GET ${path}`)
return ok({})
} }
return { input, domestic, context, checkpoint, calls }
}
test('normal single-ticket HTTP chain reaches outbound with existing master resources and PDA identity', async () => {
const f = fixture(), result = await runOverseasStage(f)
assert.equal(result.status, 'OVERSEAS_OUTBOUNDED')
assert.equal(result.fullFlowVerified, false)
assert.equal(result.deliveryNo, 'DELIVERY-TEST')
assert.ok(f.calls.some(row => row.path.endsWith('/receipts/carton') && row.token === 'pda'))
assert.ok(f.calls.some(row => row.path.endsWith('/loading-complete') && row.token === 'pda'))
assert.equal(f.calls.filter(row => /delivery-arrangement|transfer-delivery|\/depart$/.test(row.path)).length, 0)
assert.equal(f.calls.filter(row => row.path.endsWith('/terminal-drayages/5')).length, 1)
assert.equal(f.calls.filter(row => row.path.endsWith('/empty-return')).length, 1)
assert.equal(f.calls.filter(row => row.method !== 'GET' && /\/master-data\/|\/fleet\/drivers|\/iam\//.test(row.path)).length, 0)
assert.ok(Object.values(f.checkpoint.state.commands).every(row => row.status === 'DONE'))
})
test('foreign customer source stops before plan conversion or receiving', async () => {
const f = fixture({ foreignSource: true })
await assert.rejects(runOverseasStage(f), /来源不属于/)
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0)
})
test('FBA receives cumulative package count without fabricated or resolved carton marks', async () => {
const f = fixture({ destinationType: 'FBA' })
f.domestic = { ...f.domestic }; delete f.domestic.cartonMarks
const result = await runOverseasStage(f)
assert.equal(result.status, 'OVERSEAS_OUTBOUNDED')
const received = f.calls.filter(row => row.path.endsWith('/receipts/bulk'))
assert.equal(received.length, 1)
assert.equal(received[0].body.packageCount, 1)
assert.equal(f.calls.filter(row => /\/cartons\/resolve|\/receipts\/carton/.test(row.path)).length, 0)
})
test('OVERSEAS missing frozen carton marks fails before opening receiving session', async () => {
const f = fixture(); f.domestic = { ...f.domestic }; delete f.domestic.cartonMarks
await assert.rejects(runOverseasStage(f), /正式冻结箱码/)
assert.equal(f.calls.filter(row => /\/prepare\?|\/current\/sign|\/receipts\//.test(row.path)).length, 0)
})
test('signed single ticket closes its trip and releases only its existing resources', async () => {
const checkpoint = journal(), writes = []
const overseas = { tripNo: 'TRIP-TEST', dispatchNo: 'DISPATCH-TEST', deliveryNo: 'DELIVERY-TEST' }
let trip = { tripNo: overseas.tripNo, vehicleId: '3', driverId: '4', recordVersion: 1,
stops: [{ name: 'Origin' }, { name: 'Destination' }] }
const context = { request: async (method, path, body) => {
if (path.includes('/delivery-orders/')) return ok({ order: { customerId: '1', status: 'DELIVERED', dispatchNo: overseas.dispatchNo } })
if (path.endsWith('/completion')) return ok({ canComplete: true, recordVersion: 1, outcomes: [{}] })
if (method === 'POST') {
writes.push({ path, body }); trip = { ...trip, recordVersion: trip.recordVersion + 1 }
if (path.endsWith('/complete')) trip = { ...trip, status: 'COMPLETED', completedAt: '2026-10-12T12:00:00Z' }
else if (body.resourceKind === 'VEHICLE') trip.vehicleRelease = { ended: true }
else trip.driverRelease = { ended: true }
}
return ok({ trip, dispatches: [{ dispatchNo: overseas.dispatchNo }] })
} }
assert.equal((await finishSingleOrderTrip({ input, overseas, context, checkpoint })).status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
assert.equal(writes.length, 3)
assert.deepEqual(writes.map(row => row.body.recordVersion), [1, 2, 3])
})
test('native successful continuation restores original dynamic requests without sending any business write again', async () => {
const f = fixture()
const result = await runOverseasStage(f)
const writes = f.calls.filter(row => row.method !== 'GET').length
const firstSubmit = structuredClone(f.checkpoint.state.commands['submit-review'].request)
assert.equal((await runOverseasStage(f)).dispatchNo, result.dispatchNo)
assert.equal(f.calls.filter(row => row.method !== 'GET' && !row.path.endsWith('/container-arrival-times/query')).length, writes - 1)
assert.deepEqual(f.checkpoint.state.commands['submit-review'].request, firstSubmit)
assert.equal(firstSubmit.body.recordVersion, 1)
assert.equal(f.calls.filter(row => row.path.endsWith('/loading-complete')).length, 1)
})
test('completed recorded commands retain original payload instead of reading a new current version', async () => {
const checkpoint = journal(); let calls = 0
const helper = overseasCommands({ request: async (_method, _path, body) => { calls++; return ok({ recordVersion: body.recordVersion + 1 }) } }, checkpoint)
await helper.recorded('review', 'POST', '/native/review', async () => ({ recordVersion: 0, idempotencyKey: 'original' }))
const saved = checkpoint.state.commands.review
assert.equal(saved.request.body.recordVersion, 0)
assert.deepEqual(await helper.recorded('review', 'POST', '/native/review', () => assert.fail('must not rebuild with current version')), { recordVersion: 1 })
assert.equal(calls, 1)
saved.request.body.recordVersion = 2
await assert.rejects(helper.recorded('review', 'POST', '/native/review', () => assert.fail('must stop')), /检查点不符/)
})
test('legacy flat dispatch checkpoint is explicitly rejected before network without converting old replies', async () => {
for (const name of ['dispatch', 'trip-bind', 'pickComplete', 'loading-review', 'depart']) {
const f = fixture(); f.checkpoint.state.commands = { [name]: { status: 'DONE', result: { dispatchNo: 'OLD' } } }
await assert.rejects(runOverseasStage(f), /旧排车协议/)
assert.equal(f.calls.length, 0)
}
})
test('real missing pallet spaces blocks loading before any arrangement or departure rather than fabricating one', async () => {
const f = fixture({ missingActual: true })
await assert.rejects(runOverseasStage(f), /palletSpaces/)
assert.ok(f.calls.some(row => row.path.endsWith('/pick')))
assert.equal(f.calls.filter(row => /loading-complete|transfer-delivery|\/depart$/.test(row.path)).length, 0)
assert.equal(f.checkpoint.state.commands['loading-complete'], undefined)
})
test('incomplete candidates, foreign cargo and wrong auto-trip binding cannot enter actual picking', async () => {
for (const options of [{ candidateComplete: false }, { foreignCargo: true }, { foreignTrip: true }]) {
const f = fixture(options)
await assert.rejects(runOverseasStage(f), /候选不完整|主单含其他票|自动Fleet资源记录不唯一/)
assert.equal(f.calls.filter(row => row.path.endsWith('/pick')).length, 0)
assert.equal(f.calls.filter(row => row.method === 'POST' && /\/fleet\/trips$|\/dispatches\/bind$/.test(row.path)).length, 0)
}
})
test('actual native loading keeps measured 2 spaces, 17.75kg and 0.072m3 and distinct explicit stages', async () => {
const f = fixture(); await runOverseasStage(f)
const load = f.calls.find(row => row.path.endsWith('/loading-complete'))
assert.equal(Object.hasOwn(load.body, 'measurements'), false)
assert.deepEqual(f.checkpoint.state.loadingMeasurementEvidence.measurements, [{ warehousePalletId: '9', palletSpaces: 2, weightKg: 17.75, volumeCbm: '0.072', deliveryIndex: 1 }])
const create = f.calls.find(row => row.method === 'POST' && row.path === '/api/platform/v1/last-leg/dispatches')
assert.deepEqual(create.body.vehicle, { mode: 'MASTER', vehicleId: '3' })
assert.equal(create.body.driver.mode, 'MASTER'); assert.equal(create.body.driver.driverId, '4')
assert.equal(create.body.driver.evidence.expectedTaskEndAt, f.checkpoint.state.schedule.expectedDriverEndAt)
assert.equal(Object.hasOwn(create.body, 'customerId'), false)
const pick = f.calls.findIndex(row => row.path.endsWith('/pick')), loading = f.calls.indexOf(load)
assert.ok(pick < loading)
assert.equal(f.calls.filter(row => /delivery-arrangement|transfer-delivery|\/depart$/.test(row.path)).length, 0)
assert.equal(f.calls.filter(row => row.path === '/api/pda/v1/last-leg/dispatches/DISPATCH-TEST/complete').length, 0)
assert.equal(f.calls.filter(row => row.path.includes('/merchant/v1/last-leg/delivery-orders')).length, 0)
})
test('lost native review result preserves exact INTENT request and prevents all continuation HTTP', async () => {
const f = fixture({ loseCommand: '/dispatch-review' })
await assert.rejects(runOverseasStage(f), /response loss/)
const intent = f.checkpoint.state.commands['dispatch-review']
assert.equal(intent.status, 'INTENT')
assert.equal(intent.request.body.recordVersion, 2)
assert.equal(intent.request.body.idempotencyKey, `${input.batch}-dispatch-review`)
const calls = f.calls.length
await assert.rejects(runOverseasStage(f), /结果未知/)
assert.equal(f.calls.length, calls)
})
test('native successful checkpoint without its original request cannot rebuild a versioned command', async () => {
const checkpoint = journal(); checkpoint.state.commands = { review: { status: 'DONE', binding: 'old-hash', result: { recordVersion: 9 } } }
const helper = overseasCommands({ request: () => assert.fail('no HTTP') }, checkpoint)
await assert.rejects(helper.recorded('review', 'POST', '/native/review', () => assert.fail('no rebuild')), /原请求或检查点不符/)
})
function measurementReceipt(patch = {}) {
return { environment: 'TEST', batch: input.batch, dispatchNo: 'DISPATCH-TEST', pallets: [{ warehousePalletId: '9', palletSpaces: 2,
weightKg: 17.75, volumeCbm: '0.072', deliveryIndex: 1,
evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].map(field => ({ field, actual: true, sourceType: 'WAREHOUSE_MEASUREMENT',
sourceBusinessNo: 'MEASURE-SAME-RUN', observedAt: '2026-10-19T10:00:00Z', value: { palletSpaces: 2, weightKg: 17.75, volumeCbm: '0.072' }[field] })) }], ...patch }
}
test('gcp-test loading accepts only GCP-TEST receipt and preserves actual field evidence', async () => {
for (const environment of ['TEST', 'GRAY', 'gcp-test', 'GCP-TEST']) {
const f = fixture({ missingActual: true })
f.input = { ...f.input, environment: { mode: 'gcp-test', apiBase: 'https://gcp-fixture.invalid/api' },
loadingMeasurements: measurementReceipt({ environment }) }
if (environment === 'GCP-TEST') {
assert.equal((await runOverseasStage(f)).status, 'OVERSEAS_OUTBOUNDED')
assert.equal(f.checkpoint.state.loadingMeasurementEvidence.environment, 'GCP-TEST')
assert.equal(f.checkpoint.state.loadingMeasurementEvidence.receipt.environment, 'GCP-TEST')
} else {
await assert.rejects(runOverseasStage(f), /绑定不符/)
assert.equal(f.checkpoint.state.commands['loading-complete'], undefined)
}
}
const f = fixture({ missingActual: true })
const receipt = measurementReceipt({ environment: 'GCP-TEST' })
receipt.pallets[0].evidence[0].actual = false
f.input = { ...f.input, environment: { mode: 'gcp-test', apiBase: 'https://gcp-fixture.invalid/api' }, loadingMeasurements: receipt }
await assert.rejects(runOverseasStage(f), /actual来源证据/)
assert.equal(f.checkpoint.state.commands['loading-complete'], undefined)
})
test('explicit fixed measurement file can be supplied after picking and continued without changing the original input', async () => {
const directory = await mkdtemp(join(tmpdir(), 'release-native-measurement-'))
try {
const file = join(directory, 'actual.json'), f = fixture({ missingActual: true })
f.input = { ...f.input, loadingMeasurementFile: file }
await assert.rejects(runOverseasStage(f), /等待真实测量/)
assert.equal(f.checkpoint.state.pickingVerified, true)
assert.equal(f.checkpoint.state.commands['loading-complete'], undefined)
const picks = f.calls.filter(row => row.path.endsWith('/pick')).length
await writeFile(file, JSON.stringify(measurementReceipt()), 'utf8')
assert.equal((await runOverseasStage(f)).status, 'OVERSEAS_OUTBOUNDED')
assert.equal(f.calls.filter(row => row.path.endsWith('/pick')).length, picks)
const proof = f.checkpoint.state.loadingMeasurementEvidence
assert.equal(proof.source, 'EXPLICIT_FILE'); assert.equal(proof.file, file)
assert.equal(proof.sourceSha256, createHash('sha256').update(await readFile(file)).digest('hex'))
assert.deepEqual(proof.receipt, measurementReceipt())
assert.equal(proof.measurements[0].palletSpaces, 2)
const original = structuredClone(f.checkpoint.state.commands['loading-complete'].request), writes = f.calls.filter(row => row.method !== 'GET').length
await writeFile(file, JSON.stringify(measurementReceipt({ batch: 'OTHER-BATCH', pallets: [] })), 'utf8')
await runOverseasStage(f)
assert.deepEqual(f.checkpoint.state.commands['loading-complete'].request, original)
assert.deepEqual(f.checkpoint.state.loadingMeasurementEvidence, proof)
assert.equal(f.calls.filter(row => row.method !== 'GET').length, writes + 1) // only the existing read-only arrival lookup
} finally { await rm(join(directory, 'actual.json'), { force: true }); await rmdir(directory) }
})
test('actual file environment, batch, dispatch, pallet and field evidence are checked before any loading command', async () => {
const directory = await mkdtemp(join(tmpdir(), 'release-native-measurement-scope-'))
try {
const file = join(directory, 'actual.json')
for (const alter of [receipt => { receipt.environment = 'GRAY' }, receipt => { receipt.batch = 'OTHER-BATCH' },
receipt => { receipt.dispatchNo = 'OTHER-DISPATCH' }, receipt => { receipt.pallets[0].warehousePalletId = '999' },
receipt => { receipt.pallets[0].evidence[0].actual = false }]) {
const receipt = measurementReceipt(); alter(receipt)
await writeFile(file, JSON.stringify(receipt), 'utf8')
const f = fixture({ missingActual: true }); f.input = { ...f.input, loadingMeasurementFile: file }
await assert.rejects(runOverseasStage(f), /绑定不符|其他托盘|actual来源证据/)
assert.equal(f.calls.filter(row => /loading-complete|transfer-delivery|\/depart$/.test(row.path)).length, 0)
assert.equal(f.checkpoint.state.commands['loading-complete'], undefined)
}
} finally { await rm(join(directory, 'actual.json'), { force: true }); await rmdir(directory) }
})
test('measurement input rejects ambiguous or relative paths before HTTP', () => {
assert.throws(() => validateOverseasInput({ ...input, loadingMeasurementFile: 'relative.json' }, domestic), /绝对路径/)
assert.throws(() => validateOverseasInput({ ...input, loadingMeasurementFile: join(tmpdir(), 'actual.json'), loadingMeasurements: measurementReceipt() }, domestic), /不能同时提供/)
})
test('changed file after an unknown loading write cannot replace frozen original evidence or trigger resend', async () => {
const directory = await mkdtemp(join(tmpdir(), 'release-native-measurement-unknown-'))
try {
const file = join(directory, 'actual.json'), f = fixture({ missingActual: true })
f.input = { ...f.input, loadingMeasurementFile: file }
await writeFile(file, JSON.stringify(measurementReceipt()), 'utf8')
const originalRequest = f.context.request
f.context.request = async (...args) => {
const result = await originalRequest(...args)
if (args[0] === 'POST' && args[1].endsWith('/loading-complete')) throw Error('loading response lost')
return result
}
await assert.rejects(runOverseasStage(f), /loading response lost/)
assert.equal(f.checkpoint.state.commands['loading-complete'].status, 'INTENT')
const proof = structuredClone(f.checkpoint.state.loadingMeasurementEvidence), original = structuredClone(f.checkpoint.state.commands['loading-complete'].request)
const edited = measurementReceipt(); edited.pallets[0].palletSpaces = 3; edited.pallets[0].evidence[0].value = 3
await writeFile(file, JSON.stringify(edited), 'utf8')
const calls = f.calls.length
await assert.rejects(runOverseasStage(f), /结果未知/)
assert.equal(f.calls.length, calls)
assert.deepEqual(f.checkpoint.state.loadingMeasurementEvidence, proof)
assert.deepEqual(f.checkpoint.state.commands['loading-complete'].request, original)
} finally { await rm(join(directory, 'actual.json'), { force: true }); await rmdir(directory) }
})
test('synthetic loading is explicit GCP-only and rejects ambiguous actual inputs before HTTP', () => {
const base = { ...input, environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true }
validateSyntheticLoadingInput(base)
for (const patch of [{ environment: { mode: 'test' } }, { environment: { mode: 'gray' } }, { environment: { mode: 'prod' } },
{ syntheticOnly: false }, { syntheticLoading: 'true' }, { loadingMeasurements: {} }, { loadingMeasurementFile: 'anything' },
{ syntheticLoadingMeasurements: { values: { palletSpaces: 1, weightKg: 1, volumeCbm: 1 } } }]) {
assert.throws(() => validateSyntheticLoadingInput({ ...base, ...patch }))
}
})
test('explicit GCP synthetic path never manufactures actual evidence or sends numeric measurements', async () => {
const f = fixture({ missingActual: true })
f.input = { ...input, environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true }
const result = await runOverseasStage(f)
assert.equal(result.realMeasurementVerified, false)
assert.equal(result.loadingMeasurementMode, 'SYNTHETIC_TEST')
assert.deepEqual(loadingVerificationResult(result), { loadingMeasurementMode: 'SYNTHETIC_TEST', realMeasurementVerified: false, scope: 'ONE_ORDER_SYNTHETIC_HTTP_PATH' })
const proof = f.checkpoint.state.loadingMeasurementEvidence
assert.equal(proof.actual, false); assert.equal(proof.source, 'SYNTHETIC_TEST'); assert.equal(proof.measurements, null)
assert.equal(proof.dispatchNo, 'DISPATCH-TEST'); assert.equal(proof.tripNo, 'TRIP-TEST')
assert.equal(JSON.stringify(proof).includes('"actual":true'), false)
const load = f.checkpoint.state.commands['loading-complete'].request
assert.equal(Object.hasOwn(load.body, 'measurements'), false)
assert.match(load.body.remark, /SYNTHETIC_TEST:actual=false/)
const writes = f.calls.filter(row => row.method !== 'GET' && !row.path.endsWith('/container-arrival-times/query')).length
await runOverseasStage(f)
assert.equal(f.calls.filter(row => row.method !== 'GET' && !row.path.endsWith('/container-arrival-times/query')).length, writes)
const calls = f.calls.length
f.input = { ...f.input, syntheticLoading: false }
await assert.rejects(runOverseasStage(f), /冻结检查点/)
assert.equal(f.calls.length, calls)
})
test('synthetic unknown loading result blocks all continuation, including reads', async () => {
const f = fixture({ missingActual: true })
f.input = { ...input, environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true }
const request = f.context.request
f.context.request = async (...args) => { const result = await request(...args); if (args[1].endsWith('/loading-complete')) throw Error('response lost'); return result }
await assert.rejects(runOverseasStage(f), /response lost/)
const calls = f.calls.length
assert.equal(f.checkpoint.state.commands['loading-complete'].status, 'INTENT')
await assert.rejects(runOverseasStage(f), /结果未知/)
assert.equal(f.calls.length, calls)
})
test('synthetic mode preserves authoritative preview pallet, dispatch, pick and stop boundaries', async () => {
for (const mutate of [p => { p.dispatchNo = 'FOREIGN' }, p => { p.pallets.push({ ...p.pallets[0] }) },
p => { p.pallets[0].warehousePalletId = '999' }, p => { p.pallets[0].pickedPackages = 0 },
p => { p.pallets[0].deliveryIndex = 99 }]) {
const f = fixture({ missingActual: true }), request = f.context.request
f.input = { ...input, environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true }
f.context.request = async (...args) => { const result = await request(...args); if (args[0] === 'GET' && args[1].endsWith('/loading')) mutate(result.body.data); return result }
await assert.rejects(runOverseasStage(f), /预览|路线/)
assert.equal(f.checkpoint.state.commands['loading-complete'], undefined)
}
})
test('uncertain synthetic photo upload is not resent and incomplete slots cannot trigger auto departure', async () => {
const f = fixture(), upload = f.context.requestWithBody
f.context.requestWithBody = async (...args) => { const result = await upload(...args); if (args[1].includes('/loading-documents/')) throw Error('photo response lost'); return result }
await assert.rejects(runOverseasStage(f), /photo response lost/)
assert.equal(f.checkpoint.state.commands['loading-photo-LOADING_SCENE'].status, 'INTENT')
const calls = f.calls.length
await assert.rejects(runOverseasStage(f), /结果未知/)
assert.equal(f.calls.length, calls)
const other = fixture(), request = other.context.request
other.context.request = async (...args) => { const result = await request(...args); if (args[0] === 'GET' && args[1].endsWith('/loading-documents')) result.body.data.complete = false; return result }
await assert.rejects(runOverseasStage(other), /五槽装车资料未核齐/)
assert.equal(other.checkpoint.state.commands['loading-complete'], undefined)
})
test('GCP synthetic FBA appointment binds only this destination and preserves photos, maintenance and schedule', async () => {
const f = fixture({ missingAppointment: true })
f.input = { ...input, environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true, destination: { type: 'FBA', code: 'DEST' } }
await runOverseasStage(f)
const resource = f.checkpoint.state.commands['resource-evidence'].request.body
const appointment = f.checkpoint.state.commands['destination-appointment'].request.body
assert.deepEqual(appointment.vehicle.evidenceFileIds, resource.vehicle.evidenceFileIds)
assert.deepEqual(appointment.driver.evidence, resource.driver.evidence)
assert.equal(appointment.plannedDepartureAt, resource.plannedDepartureAt)
assert.equal(appointment.estimatedArrivalAt, resource.estimatedArrivalAt)
assert.equal(appointment.maintenance.fleetEmail, 'synthetic@example.invalid')
assert.deepEqual(appointment.maintenance.destinationAppointments, [{ destinationKey: 'FBA:DEST', appointmentNo: `SYNTHETIC_TEST-${input.batch}-DEST` }])
assert.equal(f.checkpoint.state.destinationAppointmentEvidence.actual, false)
assert.match(appointment.remark, /不代表第三方确认/)
const submit = f.checkpoint.state.commands['submit-review'].request.body
assert.equal(submit.recordVersion, appointment.recordVersion + 1)
assert.equal(submit.idempotencyKey, `${input.batch}-submit-review-with-appointment`)
const writes = f.calls.filter(row => row.method !== 'GET' && !row.path.endsWith('/container-arrival-times/query')).length
await runOverseasStage(f)
assert.equal(f.calls.filter(row => row.method !== 'GET' && !row.path.endsWith('/container-arrival-times/query')).length, writes)
})
test('missing FBA appointment never gets fabricated outside explicit GCP mode or for a foreign destination', async () => {
for (const patch of [{}, { environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true, destination: { type: 'FBA', code: 'OTHER' } }]) {
const f = fixture({ missingAppointment: true }); f.input = { ...input, ...patch }
await assert.rejects(runOverseasStage(f), /预约|FBA目的仓/)
assert.equal(f.checkpoint.state.commands['destination-appointment'], undefined)
assert.equal(f.checkpoint.state.commands['submit-review'], undefined)
}
})
test('uncertain appointment update preserves INTENT and cannot be resent or followed by submit review', async () => {
const f = fixture({ missingAppointment: true, loseCommand: 'appointment' })
f.input = { ...input, environment: { mode: 'gcp-test' }, syntheticOnly: true, syntheticLoading: true, destination: { type: 'FBA', code: 'DEST' } }
await assert.rejects(runOverseasStage(f), /appointment response lost/)
assert.equal(f.checkpoint.state.commands['destination-appointment'].status, 'INTENT')
assert.equal(f.checkpoint.state.commands['submit-review'], undefined)
const calls = f.calls.length
await assert.rejects(runOverseasStage(f), /结果未知/)
assert.equal(f.calls.length, calls)
})