test(acceptance): reuse fixed customers and align native fulfillment flows

This commit is contained in:
2026-10-23 04:48:02 +08:00
parent b92f7fe991
commit 139f6b1932
51 changed files with 1076 additions and 228 deletions
+77 -21
View File
@@ -442,6 +442,7 @@ export function parseArguments(argv) {
pdaPassword: values.get('pda-password') || envValue('ADMIN_PDA_PASSWORD', 'ADMIN_GRAY_PDA_PASSWORD'),
modules,
requestedModules,
stopFile: values.get('stop-file') || '',
fixedCustomerId: environment === 'test' ? process.env.ADMIN_TEST_FIXED_CUSTOMER_ID || '' : '',
customerName: values.get('customer-name') || envValue('ADMIN_TEST_CUSTOMER_NAME', 'ADMIN_GRAY_CUSTOMER_NAME'),
merchantUsername,
@@ -655,6 +656,10 @@ export class AcceptanceRun {
this.options.fixedPdaActors ??= JSON.parse(process.env.ADMIN_TEST_OVERSEAS_PDA_ACTORS || '[]')
this.options.fixedDomesticDenyActor ??= JSON.parse(process.env.ADMIN_TEST_DOMESTIC_DENY_ACTOR || 'null')
this.options.fixedFleetResources ??= JSON.parse(process.env.ADMIN_TEST_FLEET_RESOURCES || '[]')
if (!this.options.fixedSecondaryCustomer && process.env.ADMIN_TEST_SECONDARY_CUSTOMER_ID) {
this.options.fixedSecondaryCustomer = { customerId: process.env.ADMIN_TEST_SECONDARY_CUSTOMER_ID,
username: process.env.ADMIN_TEST_SECONDARY_MERCHANT_USERNAME || '', password: process.env.ADMIN_TEST_SECONDARY_MERCHANT_PASSWORD || '' }
}
}
const markerNamespace = String(options.markerNamespace || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 12)
this.marker = `${options.environment === 'gray' ? 'GRAY' : 'ADMIN'}-API-${new Date().toISOString().replace(/\D/g, '').slice(0, 17)}${markerNamespace ? `-${markerNamespace}` : ''}`
@@ -847,6 +852,19 @@ export class AcceptanceRun {
this.modules.push({ name, startedAt, finishedAt: new Date().toISOString(), success: fatal === null && steps.every((s) => s.passed), passed: steps.filter((s) => s.passed).length, failed: steps.filter((s) => !s.passed).length, fatal })
}
/** 在模块边界结束,随后仍由main的finally恢复故障注入配置并写出失败报告。 */
async stopAtBoundary() {
if (this.stopRequested) return true
if (!this.options.stopFile) return false
try { await readFile(this.options.stopFile) }
catch (error) { if (error.code === 'ENOENT') return false; throw error }
this.stopRequested = true
this.fatal = '收到停止文件,未继续执行后续模块;本报告为中止结果'
this.addStep('execution-control', '停止后续模块并进入正常清理', null, false,
'等待当前模块边界,不强杀进程;故障配置清理由总入口finally执行', { deploymentBlocker: true })
return true
}
captureWaitSplitFixture(sequence) {
const order = this.createdResources.orders || {}
const measurement = this.createdResources.inboundMeasurement || {}
@@ -1424,8 +1442,9 @@ export class AcceptanceRun {
const hadWaitSplitVariant = Object.hasOwn(this.context, 'waitSplitVariant')
const savedWaitSplitVariant = this.context.waitSplitVariant
const primaryDropshipVariant = {
...(savedWaitSplitVariant || {}), skuCount: 1, packageCountPerSku: 4,
packageCounts: [4], palletCount: 2,
// 三箱PDA换标、一箱客户换标、一箱转运,共用同一订单,不另造订单或主档。
...(savedWaitSplitVariant || {}), skuCount: 1, packageCountPerSku: 5,
packageCounts: [5], palletCount: 2,
}
if (primaryDropship) {
this.context.waitSplitVariant = primaryDropshipVariant
@@ -1554,6 +1573,7 @@ export class AcceptanceRun {
}
async runPhase4ProducerVariant(label, destinationType, fulfillmentType = 'STANDARD', pickupRequired = false) {
const shippingRequired = pickupRequired || label === 'SECONDARY'
const matrixMarker = this.marker
const attempt = (this.phase4ProducerAttempts.get(label) || 0) + 1
this.phase4ProducerAttempts.set(label, attempt)
@@ -1579,22 +1599,41 @@ export class AcceptanceRun {
this.context.orderDestinationType = destinationType
this.context.orderFulfillmentType = fulfillmentType
this.context.phase4ProducerMatrixMarker = matrixMarker
if (destinationType === 'FBA' && label === 'FBA-SECOND') {
const existingFba = (this.createdResources.phase4InboundProducerFixtures?.containers || [])
.filter(item => item?.destinationType === 'FBA'
&& item?.fulfillmentType !== 'ONE_CLICK_DROPSHIP')
const primaryCode = existingFba.map(item => String(item?.destinationWarehouseCode || '').trim().toUpperCase()).find(Boolean)
this.context.waitSplitRoute = {
...(this.context.waitSplitRoute || {}),
// The normal order route rotates the destination list from randomIndex;
// force this producer onto the next distinct priced FBA warehouse so
// dispatch can exercise a multi-destination candidate set.
randomIndex: 17,
destinationIndex: 1,
excludeDestinationCodes: primaryCode ? [primaryCode] : [],
}
}
try {
if (label === 'DROPSHIP-PRIMARY' && this.options.fixedCustomerId
&& this.options.modules.includes('phase4-overseas-client-services')) {
// 同一五箱订单还供应转运收货;造单前按既有异仓资源选线,避免出库后才发现无目标托盘。
const { findReusableTransferReceiptTarget } = await import(new URL('./admin-test-env-modules/warehouse-transfer-receipt.mjs', import.meta.url))
const target = await findReusableTransferReceiptTarget({
sourceWarehouseId: String(this.options.fixedPdaActors?.[0]?.warehouseId || ''),
request: async (method, path) => {
const response = await this.request(method, path)
const data = this.apiSuccess('phase4-overseas-pda-fixture', '转运造单前核验既有目标资源', response)
if (response.http !== 200 || String(response.body?.code) !== '200') throw Error('转运目标资源查询失败')
return data
},
check: (name, ok) => { if (!ok) throw Error(name) },
})
if (!target) throw Error('缺少转运收货既有异仓空闲托盘;未创建来源订单')
this.createdResources.phase4TransferReceiptTargetPlan = target
this.context.waitSplitRoute = { ...(this.context.waitSplitRoute || {}),
includeDestinationCodes: [target.pallet.fbaWarehouseCode] }
}
if (destinationType === 'FBA' && label === 'FBA-SECOND') {
const existingFba = (this.createdResources.phase4InboundProducerFixtures?.containers || [])
.filter(item => item?.destinationType === 'FBA'
&& item?.fulfillmentType !== 'ONE_CLICK_DROPSHIP')
const primaryCode = existingFba.map(item => String(item?.destinationWarehouseCode || '').trim().toUpperCase()).find(Boolean)
this.context.waitSplitRoute = {
...(this.context.waitSplitRoute || {}),
// The normal order route rotates the destination list from randomIndex;
// force this producer onto the next distinct priced FBA warehouse so
// dispatch can exercise a multi-destination candidate set.
randomIndex: 17,
destinationIndex: 1,
excludeDestinationCodes: primaryCode ? [primaryCode] : [],
}
}
// Cross-region variants only need a normal completed loading chain. The destructive A0
// inventory-snapshot mutation is covered once by an explicitly requested phase3-loading
// run; repeating it for every producer is unsafe for a populated shared warehouse and can
@@ -1624,12 +1663,12 @@ export class AcceptanceRun {
}
}
if (producerKey) writeShippingEvidence(false)
if (pickupRequired && !producerKey) {
if (shippingRequired && !producerKey) {
this.assert('phase4-overseas-pda-fixture', `${label}产生者身份完整`, null, false,
'phase3LoadingCompletedProducer.resourceKey=missing',
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
}
if (pickupRequired && producerKey) {
if (shippingRequired && producerKey) {
const customs = await import(new URL('./admin-test-env-modules/phase3-customs.mjs', import.meta.url))
await this.executeModule(`phase3-customs-phase4-${label}`, () => customs.run(this))
const serviceCenter = await import(new URL('./admin-test-env-modules/service-center-flow.mjs', import.meta.url))
@@ -1642,6 +1681,15 @@ export class AcceptanceRun {
await this.executeModule(`phase3-container-documents-phase4-${label}`, () => documents.run(this))
const carrierQueries = await import(new URL('./admin-test-env-modules/phase3-carrier-schedule-queries.mjs', import.meta.url))
await this.executeModule(`phase3-carrier-schedule-queries-phase4-${label}`, () => carrierQueries.run(this))
if (!pickupRequired) {
// 第二柜先真实出仓并建立原生船期任务,保留不可接柜状态供PDA拒绝用例;Admin预约前再推进开船。
const source = this.createdResources.phase3CarrierScheduleQuery
const ready = source?.ownedByModule === true && source.marker === variantMarker && Boolean(source.queryId)
writeShippingEvidence(false, ready ? String(source.queryId) : null)
this.assert('phase4-overseas-pda-fixture', `${label}已真实出仓但尚未开放接柜`, null, ready,
`resourceKey=${producerKey}, queryPresent=${Boolean(source?.queryId)}`,
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
} else {
const carrierNodes = await import(new URL('./admin-test-env-modules/phase4-carrier-nodes.mjs', import.meta.url))
await this.executeModule(`phase4-carrier-pickup-ready-phase4-${label}`,
() => carrierNodes.runUntilPickupReady(this))
@@ -1654,6 +1702,7 @@ export class AcceptanceRun {
shippingReady,
`resourceKey=${producerKey}, queryId=${pickup?.queryId || 'missing'}, ready=${pickup?.ready === true}`,
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
}
}
} finally {
if (restoreFixture) restoreFixture()
@@ -1728,7 +1777,7 @@ export class AcceptanceRun {
&& overseasReady?.shippingReady === true && Boolean(overseasReady?.carrierScheduleQueryId)
&& awaitingVariants.length >= 1
&& awaitingVariants.every((item) => item.shippingReady === false
&& item.carrierScheduleQueryId == null)
&& Boolean(item.carrierScheduleQueryId))
this.assert('phase4-overseas-pda-fixture', '本轮三期跨区生产矩阵已就绪', null,
matrix?.marker === this.marker && matrix?.readiness?.readyForPhase4Pda === true && shippingMatrixReady,
`marker=${matrix?.marker || 'missing'}, FBA=${matrix?.readiness?.fbaContainerCount ?? 0}, OVERSEAS=${matrix?.readiness?.overseasContainerCount ?? 0}, DROPSHIP=${matrix?.readiness?.dropshipContainerCount ?? 0}, containers=${matrix?.readiness?.distinctContainerCount ?? 0}, shippingReady=${containers.filter((item) => item?.shippingReady === true).length}, awaiting=${awaitingVariants.length}`,
@@ -1876,6 +1925,7 @@ export class AcceptanceRun {
this.expiresAt = Number(login.expiresIn) > 0 ? Date.now() + Number(login.expiresIn) * 1000 : 0
const handlers = { customers: () => this.runCustomers() }
for (let moduleIndex = 0; moduleIndex < this.options.modules.length; moduleIndex += 1) {
if (await this.stopAtBoundary()) break
const module = this.options.modules[moduleIndex]
if (this.expiresAt > 0 && Date.now() + 120_000 >= this.expiresAt) {
await this.refreshSession(`模块 ${module} 执行前Token临近到期`)
@@ -1938,6 +1988,10 @@ export class AcceptanceRun {
reason, { scenario: 'phase3-container-customs-mixed-fixture', deploymentBlocker: true })
}
}
if (await this.stopAtBoundary()) {
if (restoreFixture) restoreFixture()
break
}
let handler = handlers[module]
if (!handler) {
try {
@@ -1957,6 +2011,7 @@ export class AcceptanceRun {
} finally {
if (restoreFixture) restoreFixture()
}
if (await this.stopAtBoundary()) break
if (module === 'merchant-qualification-fixture' && this.options.fixedCustomerId
&& !this.context.merchantQualificationFixture?.reused) {
this.fatal = '固定客户身份校验失败,停止后续造数'
@@ -1991,7 +2046,7 @@ export class AcceptanceRun {
deploymentBlockers: this.deploymentBlockers, coverageGaps: this.coverageGaps,
createdResources: this.createdResources,
})
const forbiddenValues = [this.options.password, this.options.merchantPassword,
const forbiddenValues = [this.options.password, this.options.merchantPassword, this.options.fixedSecondaryCustomer?.password,
this.token, this.refreshToken]
.filter((value) => typeof value === 'string' && value.length >= 8)
const sensitivePatterns = [
@@ -2015,6 +2070,7 @@ export class AcceptanceRun {
plannedModules: [...this.options.modules],
defaultModules: [...DEFAULT_MODULES],
defaultPlanSelected: DEFAULT_MODULES.every((name) => this.options.modules.includes(name)),
stoppedByRequest: this.stopRequested === true,
preparationOnly: this.options.prepareDomesticPdaH5 === true || this.options.prepareDropshipManualCount === true || this.options.prepareMeasurementReview === true,
returnMainChainOnly: this.options.returnMainChainOnly === true,
evidenceType: 'HTTP',