test: integrate reliable event recovery and receiving evidence
This commit is contained in:
@@ -383,6 +383,7 @@ export function parseArguments(argv) {
|
||||
routeMode: values.get('route-mode') || 'fixed',
|
||||
quiet: values.get('quiet') === 'true',
|
||||
returnMainChainOnly: values.get('return-main-chain-only') === 'true',
|
||||
verifyUnloadEventRecovery: values.get('verify-unload-event-recovery') === 'true',
|
||||
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
|
||||
manualSplitPlanId: values.get('manual-split-plan-id') || '',
|
||||
manualPreloadSourceReport: values.get('manual-preload-source-report') || '',
|
||||
@@ -394,6 +395,9 @@ export function parseArguments(argv) {
|
||||
}
|
||||
|
||||
export function validateExecutionOptions(options) {
|
||||
if (options.verifyUnloadEventRecovery && !options.modules.includes('phase4-overseas-pda')) {
|
||||
throw new Error('卸柜事件恢复验收必须包含正式 phase4-overseas-pda 收货链')
|
||||
}
|
||||
const unavailable = options.modules.filter((module) => !AVAILABLE_MODULES.has(module))
|
||||
if (unavailable.length > 0) throw new Error(`未注册的 HTTP 验收模块: ${unavailable.join(',')}`)
|
||||
for (const [name, value] of [['gateway', options.gateway], ['username', options.username], ['password', options.password]]) {
|
||||
@@ -1424,7 +1428,7 @@ export class AcceptanceRun {
|
||||
const attempt = (this.phase4ProducerAttempts.get(label) || 0) + 1
|
||||
this.phase4ProducerAttempts.set(label, attempt)
|
||||
const variantMarker = `${matrixMarker}-P4-${label}${attempt === 1 ? '' : `-R${attempt}`}`
|
||||
const contextKeys = ['orderDestinationType', 'orderFulfillmentType', 'phase4ProducerMatrixMarker']
|
||||
const contextKeys = ['orderDestinationType', 'orderFulfillmentType', 'phase4ProducerMatrixMarker', 'waitSplitRoute']
|
||||
const createdKeys = [
|
||||
'phase3Loading', 'phase3LoadingCompletedProducer', 'phase3ContainerDocuments', 'orchestrationFixtures', 'oceanBooking',
|
||||
'phase3CustomsSupplier', 'phase3Customs', 'serviceCenterFlow', 'portSchedule',
|
||||
@@ -1557,6 +1561,11 @@ export class AcceptanceRun {
|
||||
if (!hasOperationsFbaProducer()) {
|
||||
await this.runPhase4ProducerVariant('FBA-OPERATIONS', 'FBA', 'STANDARD', true)
|
||||
}
|
||||
const hasRouteFbaProducer = () => (this.createdResources.phase4InboundProducerFixtures?.containers || [])
|
||||
.some((item) => /-P4-FBA-ROUTE(?:-R\d+)?:/.test(String(item?.resourceKey || '')))
|
||||
if (!hasRouteFbaProducer()) {
|
||||
await this.runPhase4ProducerVariant('FBA-ROUTE', 'FBA', 'STANDARD', true)
|
||||
}
|
||||
if (Number(readiness().overseasContainerCount || 0) < 1) {
|
||||
await this.runPhase4ProducerVariant('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true)
|
||||
}
|
||||
@@ -1576,6 +1585,8 @@ export class AcceptanceRun {
|
||||
/-P4-FBA-SECOND(?:-R\d+)?:/.test(String(item?.resourceKey || '')))
|
||||
const operationsFbaReady = containers.find((item) =>
|
||||
/-P4-FBA-OPERATIONS(?:-R\d+)?:/.test(String(item?.resourceKey || '')))
|
||||
const routeFbaReady = containers.find((item) =>
|
||||
/-P4-FBA-ROUTE(?:-R\d+)?:/.test(String(item?.resourceKey || '')))
|
||||
const awaitingVariants = containers.filter((item) => ['SECONDARY']
|
||||
.some((label) => new RegExp(`-P4-${label}(?:-R\\d+)?:`).test(String(item?.resourceKey || ''))))
|
||||
const shippingMatrixReady = dropshipReady?.shippingReady === true
|
||||
@@ -1583,6 +1594,7 @@ export class AcceptanceRun {
|
||||
&& standardFbaReady?.shippingReady === true && Boolean(standardFbaReady?.carrierScheduleQueryId)
|
||||
&& secondStandardFbaReady?.shippingReady === true && Boolean(secondStandardFbaReady?.carrierScheduleQueryId)
|
||||
&& operationsFbaReady?.shippingReady === true && Boolean(operationsFbaReady?.carrierScheduleQueryId)
|
||||
&& routeFbaReady?.shippingReady === true && Boolean(routeFbaReady?.carrierScheduleQueryId)
|
||||
&& overseasReady?.shippingReady === true && Boolean(overseasReady?.carrierScheduleQueryId)
|
||||
&& awaitingVariants.length >= 1
|
||||
&& awaitingVariants.every((item) => item.shippingReady === false
|
||||
@@ -1830,6 +1842,11 @@ export class AcceptanceRun {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.options.verifyUnloadEventRecovery) {
|
||||
this.addStep('phase4-overseas-pda', '显式卸柜故障场景必须实际完成且只验证本轮一个柜', null,
|
||||
unloadRecoveryWasVerified(this), '不以未触发故障或普通收货成功代替事件恢复证据',
|
||||
{ scenario: 'reverse-unload-event-recovery', deploymentBlocker: true })
|
||||
}
|
||||
const reportSurface = JSON.stringify({
|
||||
steps: this.steps, modules: this.modules, scenarios: this.scenarios,
|
||||
deploymentBlockers: this.deploymentBlockers, coverageGaps: this.coverageGaps,
|
||||
@@ -1870,11 +1887,72 @@ export class AcceptanceRun {
|
||||
}
|
||||
}
|
||||
|
||||
export function unloadRecoveryWasVerified(run) {
|
||||
const records = Object.entries(run.createdResources?.phase4OverseasPda?.receivingCompletionEvidence || {})
|
||||
.filter(([, row]) => row.eventRecovery)
|
||||
if (records.length !== 1) return false
|
||||
const [bookingId, row] = records[0]
|
||||
return row.status === 'VERIFIED' && row.eventRecoveryInventoryUnchanged === true
|
||||
&& row.eventRecovery.restored === true && row.eventRecovery.marker === run.marker
|
||||
&& row.eventRecovery.containerBookingId === bookingId
|
||||
}
|
||||
|
||||
export async function configureReadOnlyEvidence(run, environment, createReaders, faultFactories) {
|
||||
if (!run.options.modules.some(name => ['phase4-overseas-pda', 'phase5-root-fulfillment', 'manual-state-matrix'].includes(name))) return
|
||||
const factory = createReaders || (await import('./fba-core-regression-evidence.mjs')).createTestEvidenceReaders
|
||||
const readers = factory(environment)
|
||||
await readers.preflight()
|
||||
run.context.phase4ReceivingEvidenceReader = readers.readReceiving
|
||||
run.context.f40ReadOnlyOutboxEvidence = readers.readCompletion
|
||||
const faults = []
|
||||
if (run.options.modules.includes('phase4-cross-region-events')) {
|
||||
const { createTestFaultExecutor } = faultFactories || await import('./test-event-fault-executor.mjs')
|
||||
const fault = createTestFaultExecutor(environment)
|
||||
run.context.beforeLoadingComplete = fault.prepare
|
||||
run.context.directDeliveryReplayEvidenceReader = fault.readProof
|
||||
faults.push(fault)
|
||||
}
|
||||
if (run.options.verifyUnloadEventRecovery) {
|
||||
const { createTestUnloadFaultExecutor } = faultFactories || await import('./test-event-fault-executor.mjs')
|
||||
const fault = createTestUnloadFaultExecutor(environment)
|
||||
faults.push(fault)
|
||||
let selected = null
|
||||
run.context.beforeReceivingComplete = async scope => {
|
||||
if (selected) return
|
||||
selected = { marker: run.marker, customerId: scope.customerId, containerBookingId: scope.bookingId }
|
||||
await fault.prepare(selected)
|
||||
}
|
||||
run.context.afterReceivingComplete = async scope => {
|
||||
if (!selected || scope.bookingId !== selected.containerBookingId || scope.customerId !== selected.customerId) return null
|
||||
return fault.readProof(selected)
|
||||
}
|
||||
}
|
||||
if (faults.length) return {
|
||||
cleanup: async () => {
|
||||
const errors = []
|
||||
for (const fault of [...faults].reverse()) {
|
||||
try { await fault.cleanup() } catch { errors.push('TEST事件故障验收恢复失败,保留原任务恢复记录') }
|
||||
}
|
||||
if (errors.length) throw new Error(errors.join('; '))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = validateExecutionOptions(parseArguments(process.argv.slice(2)))
|
||||
const timestamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14)
|
||||
const outputPath = resolve(options.output || `reports/admin-test-env-acceptance-${timestamp}.json`)
|
||||
const report = await new AcceptanceRun(options).run()
|
||||
const run = new AcceptanceRun(options)
|
||||
const fault = await configureReadOnlyEvidence(run, process.env)
|
||||
let report
|
||||
try { report = await run.run() }
|
||||
finally {
|
||||
try { await fault?.cleanup() }
|
||||
catch (error) { run.addStep('phase4-cross-region-events', '故障验收后必须恢复原服务配置与健康状态', null,
|
||||
false, String(error.message), { scenario: 'direct-delivery-replay', deploymentBlocker: true }) }
|
||||
}
|
||||
report.summary = buildAcceptanceSummary(run.steps, run.modules, run.coverageGaps)
|
||||
report.success = report.fatal === null && report.summary.failed === 0
|
||||
await mkdir(dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
|
||||
console.log(`REPORT=${outputPath}`)
|
||||
|
||||
Reference in New Issue
Block a user