test(fleet): cover partial returns and redispatch through real API flow

This commit is contained in:
2026-10-03 16:11:52 +08:00
parent 8700fc3423
commit d05ff1cd26
4 changed files with 395 additions and 3 deletions
+19 -3
View File
@@ -12,6 +12,7 @@ import { verifyFleetExternalReceipt } from './admin-test-env-modules/fleet-exter
import { verifyFleetExternalDispatch } from './admin-test-env-modules/fleet-external-dispatch-acceptance.mjs'
import { verifyFleetHandover, cleanupFleetHandoverResources } from './admin-test-env-modules/fleet-handover-acceptance.mjs'
import { verifyFleetTripCosts } from './admin-test-env-modules/fleet-trip-cost-acceptance.mjs'
import { verifyFleetReturnRedispatch } from './admin-test-env-modules/fleet-return-redispatch-acceptance.mjs'
import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
@@ -23,7 +24,7 @@ const success = response => response?.http === 200 && id(response?.body?.code) =
const key = () => `fleet-${randomUUID()}`
export function parseFleetArguments(args) {
const config = { execute: false, environment: 'TEST', runId: 'plan' }
const config = { execute: false, environment: 'TEST', runId: 'plan', scenario: 'handover' }
let action = null
const seen = new Set()
for (let index = 0; index < args.length; index++) {
@@ -32,7 +33,7 @@ export function parseFleetArguments(args) {
if (action) throw Error('Choose exactly one plan or execute action')
action = name; config.execute = name === '--execute'; continue
}
if (!['--environment', '--run-id', '--output', '--resume-report', '--resume-stage'].includes(name) || !args[index + 1] || args[index + 1].startsWith('--')) throw Error('Unsupported or missing argument')
if (!['--environment', '--run-id', '--output', '--resume-report', '--resume-stage', '--scenario'].includes(name) || !args[index + 1] || args[index + 1].startsWith('--')) throw Error('Unsupported or missing argument')
if (seen.has(name)) throw Error('Duplicate argument')
seen.add(name)
const value = args[++index]
@@ -40,16 +41,21 @@ export function parseFleetArguments(args) {
else if (name === '--run-id') config.runId = value
else if (name === '--output') config.outputDir = value
else if (name === '--resume-report') config.resumeReport = value
else if (name === '--scenario') config.scenario = value
else config.resumeStage = value
}
if (config.environment !== 'TEST' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(config.runId)
|| (config.execute && (!args.includes('--environment') || config.runId === 'plan'))) throw Error('Explicit TEST and fresh run-id required')
if (Boolean(config.resumeReport) !== Boolean(config.resumeStage) || (config.resumeStage && !['FULL_COVERAGE', 'FULL_COVERAGE_PERSISTED', 'EXTERNAL_RECEIPT_READY', 'EXTERNAL_PALLETIZED', 'EXTERNAL_RECEIPT_CONFIRMED', 'TERMINAL_READONLY'].includes(config.resumeStage))) throw Error('Explicit reviewed checkpoint required')
if (config.outputDir && !/^reports\/[A-Za-z0-9][A-Za-z0-9_-]{0,95}$/.test(config.outputDir)) throw Error('Output must be a fresh named directory directly under reports')
if (!['handover', 'return-redispatch'].includes(config.scenario) || (config.scenario === 'return-redispatch' && config.resumeReport))
throw Error('Return redispatch only accepts a fresh run, never a reviewed old checkpoint')
return config
}
export function fleetExecutionPlan(config) {
return { environment: 'TEST', execution: config.execute ? 'EXECUTE' : 'PLAN', runId: config.runId,
return { environment: 'TEST', execution: config.execute ? 'EXECUTE' : 'PLAN', runId: config.runId, scenario: config.scenario || 'handover',
returnRedispatch: config.scenario === 'return-redispatch' ? { required: 'Two current-run domestic sources; A >=3 boxes; current departure covers A and B in full',
stages: ['A actual return 2', 'original remaining signatures; A incomplete and B complete', 'RETURN_RECEIPT draft lock and pre-arrange', 'new PDA picks and DEPART 2', 'redispatch signature; A complete', 'source event/ACK read-only evidence remains required'] } : null,
outputDir: config.outputDir || `reports/fleet-${config.runId}`, producer: config.resumeReport ? 'Reviewed read-only FULL_COVERAGE checkpoint in the same original fixture' : 'phase4-overseas-dispatch with its normal fresh prerequisites',
resumeReport: config.resumeReport || null, resumeStage: config.resumeStage || null,
stages: ['FULL original coverage', 'formal cancel/unbind/re-arrange', 'PARTIAL original coverage', 'reservation lifecycle',
@@ -430,6 +436,13 @@ export async function runFleetScenario(context, { wait = ms => new Promise(resol
})
stage('RECEIPTS_AND_RESOURCE_RELEASE')
proof.receipts = []
if (context.context.fleetScenario === 'return-redispatch') {
stage('RETURN_REDISPATCH')
const returned = await verifyFleetReturnRedispatch(context, MODULE, trip, { customerId, warehouseId, pickerToken: picker.token,
dispatchNos, deliveryNos: [...partial.proof.deliveryNos, external.deliveryNo] })
proof.receipts = returned.originalSignatures.map(row => ({ deliveryNo: row.deliveryNo, signedAt: row.signedAt }))
context.context.saveFleetReturnRedispatchFixture(returned)
} else {
for (const [index, deliveryNo] of [...partial.proof.deliveryNos, external.deliveryNo].entries()) {
const delivery = await request('GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}?customerId=${encodeURIComponent(customerId)}`,
undefined, '签收前读取本次实际派送')
@@ -437,6 +450,7 @@ export async function runFleetScenario(context, { wait = ms => new Promise(resol
check('每个目的地真实保存签收资料后完成', completed?.order?.status === 'DELIVERED' && completed.order.signedAt)
proof.receipts.push({ deliveryNo, signedAt: completed.order.signedAt })
}
}
proof.closure = await finishFleetTrip(context, MODULE, trip)
check('本次车次货物结案及车辆司机释放完成', proof.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
stage('HANDOVER_RESOURCE_CLEANUP')
@@ -485,7 +499,9 @@ export async function runFleetAcceptance(config, { env = process.env, readers, c
markerNamespace: createHash('sha256').update(config.runId).digest('hex').slice(0, 12) })
context = createContext ? createContext(options) : new AcceptanceRun(options)
context.context.fleetRunId = config.runId
context.context.fleetScenario = config.scenario || 'handover'
context.context.saveFleetHandoverFixture = fixture => save('handover-fixture.json', fixture)
context.context.saveFleetReturnRedispatchFixture = fixture => save('return-redispatch-fixture.json', fixture)
let handoverCheckpoint = 0
context.context.checkpointFleetHandover = proof => {
const payload = JSON.stringify(redactEvidence({ marker: context.marker, runId: config.runId,