优化
This commit is contained in:
@@ -5,17 +5,17 @@ import { dirname, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
import { normalizeFixedCustomerId, requireFixedSecondaryCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs'
|
||||
import { FIXED_RESOURCE_READONLY_MODULES, runFixedResourceReadOnly } from './admin-test-env-modules/fixed-resource-readonly.mjs'
|
||||
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'pda-releases', 'audit', 'dashboard', 'self-operated-analytics', 'master-data', 'suppliers', 'domestic-container', 'orders', 'inbound-hold', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'sailing-calendar', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'appointment-work', 'phase4-tail-order-controls', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-return-receipt', 'phase4-overseas-return-followups', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase5-terminal-drayage', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'fleet-management', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-direct-paid-isolation', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'phase4-overseas-wallet-and-pallet-cost', 'customer-operating-statistics']
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'pda-releases', 'audit', 'dashboard', 'self-operated-analytics', 'master-data', 'suppliers', 'domestic-container', 'orders', 'inbound-hold', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'sailing-calendar', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'appointment-work', 'phase4-tail-order-controls', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-return-receipt', 'phase4-overseas-return-followups', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase5-terminal-drayage', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'fleet-management', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-direct-paid-isolation', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'phase4-overseas-wallet-and-pallet-cost', 'customer-operating-statistics', 'mixed-container-receiving']
|
||||
const OPTIONAL_MODULES = ['phase5-root-fulfillment', 'manual-order-splitting-fixture', 'manual-order-splitting-verify',
|
||||
'manual-preload-push-fixture', 'manual-preload-push-verify',
|
||||
'manual-pda-ready-fixture', 'manual-pda-ready-verify',
|
||||
'manual-pda-batch-fixture', 'manual-pda-batch-verify',
|
||||
'manual-loading-complete-fixture', 'manual-loading-complete-verify',
|
||||
'coload-groups', 'dropship-pallet-matrix', 'dropship-mixed-palletization', 'dropship-physical-carton-chain', 'dropship-browser-fixture', 'mixed-container-receiving', 'pda-loading-link-regression', 'fleet-vehicle-care', 'fleet-driver-qualification']
|
||||
'coload-groups', 'dropship-pallet-matrix', 'dropship-mixed-palletization', 'dropship-physical-carton-chain', 'dropship-browser-fixture', 'pda-loading-link-regression', 'fleet-vehicle-care', 'fleet-driver-qualification']
|
||||
const AVAILABLE_MODULES = new Set([...CONFIGURED_MODULES, ...OPTIONAL_MODULES])
|
||||
const FIXED_RESOURCE_SKIPPED_MODULES = new Set(['customers', 'supplemental-boundaries', 'system', 'master-data',
|
||||
'notifications', 'pricing', 'phase4-overseas-master-data'])
|
||||
const FIXED_RESOURCE_SKIPPED_MODULES = new Set(['supplemental-boundaries'])
|
||||
const A0_LOCATION_PAGE_SIZE = 200
|
||||
const A0_MAX_LOCATION_MUTATIONS = 8
|
||||
|
||||
@@ -295,6 +295,7 @@ export function describeAcceptanceScope() {
|
||||
defaultModules: [...DEFAULT_MODULES],
|
||||
optionalModules: OPTIONAL_MODULES.filter((name) => !DEFAULT_MODULES.includes(name)),
|
||||
fixedResourceSkippedModules: [...FIXED_RESOURCE_SKIPPED_MODULES],
|
||||
fixedResourceReadOnlyModules: [...FIXED_RESOURCE_READONLY_MODULES],
|
||||
}
|
||||
}
|
||||
const WEIGHT_IMAGE_PATH = process.env.ADMIN_TEST_WEIGHT_IMAGE_PATH || ''
|
||||
@@ -675,6 +676,33 @@ export function buildAcceptanceSummary(steps = [], modules = [], coverageGaps =
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一规则用于正常结束及 finally 清理之后,覆盖率不能脱离实际执行结果。 */
|
||||
export function finalizeAcceptanceReport(report) {
|
||||
const steps = report.steps || [], modules = report.modules || [], gaps = report.coverageGaps || []
|
||||
const scope = report.executionScope
|
||||
const planned = [...new Set(scope.plannedModules)]
|
||||
const rows = name => modules.filter(module => module.name === name)
|
||||
scope.completedModules = planned.filter(name => rows(name).length > 0 && rows(name).every(module =>
|
||||
!module.skipped && module.success === true && Number(module.passed) > 0 && Number(module.failed) === 0))
|
||||
scope.skippedModules = planned.filter(name => rows(name).some(module => module.skipped))
|
||||
scope.failedModules = planned.filter(name => rows(name).some(module => !module.skipped
|
||||
&& (module.success === false || module.fatal || Number(module.failed) > 0)))
|
||||
scope.unexecutedModules = planned.filter(name => rows(name).length === 0)
|
||||
scope.unverifiedModules = planned.filter(name => !scope.completedModules.includes(name)
|
||||
&& !scope.skippedModules.includes(name) && !scope.failedModules.includes(name))
|
||||
scope.defaultPlanSelected = scope.defaultModules.length > 0 && scope.defaultModules.every(name => planned.includes(name))
|
||||
scope.planCompleted = planned.length > 0 && scope.completedModules.length === planned.length
|
||||
report.summary = buildAcceptanceSummary(steps, modules, gaps)
|
||||
report.success = !report.fatal && !scope.stoppedByRequest && planned.length > 0
|
||||
&& report.summary.failed === 0 && !gaps.some(gap => gap.blocking || gap.deploymentBlocker)
|
||||
&& !(report.deploymentBlockers || []).length && !scope.failedModules.length && !scope.unverifiedModules.length
|
||||
report.fullCoverage = report.success && scope.planCompleted && scope.defaultPlanSelected
|
||||
&& !scope.preparationOnly && !scope.returnMainChainOnly && report.fullAcceptance !== false
|
||||
&& gaps.length === 0 && !steps.some(step => step.skipped)
|
||||
report.coverageStatus = !report.success ? 'FAILED' : report.fullCoverage ? 'FULL' : 'PARTIAL'
|
||||
return report
|
||||
}
|
||||
|
||||
export class AcceptanceRun {
|
||||
constructor(options) {
|
||||
assertGrayExecutionScope(options)
|
||||
@@ -885,13 +913,35 @@ export class AcceptanceRun {
|
||||
reason: '固定资源模式排除生命周期;不计为验收通过' })
|
||||
return
|
||||
}
|
||||
const readOnly = Boolean(this.options.fixedCustomerId && FIXED_RESOURCE_READONLY_MODULES.includes(name))
|
||||
const startedAt = new Date().toISOString(); const start = this.steps.length; let fatal = null
|
||||
try { await handler() } catch (error) {
|
||||
try {
|
||||
if (readOnly) await runFixedResourceReadOnly(this, name)
|
||||
else await handler()
|
||||
} catch (error) {
|
||||
fatal = error instanceof Error ? error.message : String(error)
|
||||
this.addStep(name, '模块执行', null, false, fatal)
|
||||
}
|
||||
const steps = this.steps.slice(start)
|
||||
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 })
|
||||
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,
|
||||
...(readOnly ? { coverageMode: 'READ_ONLY', lifecycleSkipped: true } : {}) })
|
||||
}
|
||||
|
||||
async runIsolatedModule(name, handler) {
|
||||
const child = Object.create(this)
|
||||
child.options = { ...this.options }
|
||||
child.context = { ...this.context }
|
||||
child.createdResources = {}
|
||||
child.phase4ProducerAttempts = new Map()
|
||||
child.marker = `${this.marker}-MIXED`
|
||||
try { await handler(child) }
|
||||
finally {
|
||||
for (const field of ['token', 'refreshToken', 'expiresAt']) {
|
||||
if (Object.hasOwn(child, field)) this[field] = child[field]
|
||||
}
|
||||
this.createdResources.isolatedModules ??= {}
|
||||
this.createdResources.isolatedModules[name] = child.createdResources
|
||||
}
|
||||
}
|
||||
|
||||
/** 在模块边界结束,随后仍由main的finally恢复故障注入配置并写出失败报告。 */
|
||||
@@ -2040,7 +2090,9 @@ export class AcceptanceRun {
|
||||
try {
|
||||
const extension = await import(new URL(`./admin-test-env-modules/${module}.mjs`, import.meta.url))
|
||||
if (typeof extension.run !== 'function') throw new Error('模块必须导出 async function run(context)')
|
||||
handler = () => extension.run(this)
|
||||
handler = module === 'mixed-container-receiving'
|
||||
? () => this.runIsolatedModule(module, child => extension.run(child))
|
||||
: () => extension.run(this)
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
this.addStep(module, '模块注册检查', null, false, `无法加载模块: ${reason}`)
|
||||
@@ -2067,7 +2119,7 @@ export class AcceptanceRun {
|
||||
}
|
||||
const laterOverseasModule = this.options.modules.slice(moduleIndex + 1)
|
||||
.some((name) => name.startsWith('phase4-overseas-'))
|
||||
if (module === 'system' && laterOverseasModule) {
|
||||
if (module === 'system' && laterOverseasModule && !this.options.fixedCustomerId) {
|
||||
const refreshed = await this.refreshSession('system模块权限与角色变更完成')
|
||||
if (refreshed) {
|
||||
const probe = await this.request('GET', '/api/platform/v1/last-leg/orders?page=1&size=1')
|
||||
@@ -2105,15 +2157,14 @@ export class AcceptanceRun {
|
||||
this.addStep('report-security', '报告敏感信息扫描', null, !sensitive,
|
||||
sensitive ? '检测到密码、Token、二进制/base64 或 OCR 原文字段' : '未发现密码、Token、文件二进制/base64 或 OCR 原文字段')
|
||||
|
||||
const summary = buildAcceptanceSummary(this.steps, this.modules, this.coverageGaps)
|
||||
return {
|
||||
return finalizeAcceptanceReport({
|
||||
reportName: this.options.prepareDomesticPdaH5 ? '国内 PDA H5 待拣任务准备(非全链路验收)' : this.options.environment === 'gray' ? '灰度环境 FBA 定向闭环 HTTP API 验收' : '后台管理测试环境 HTTP API 验收', marker: this.marker,
|
||||
environment: this.options.environment || 'test', gateway: this.options.gateway,
|
||||
executionScope: {
|
||||
requestedModules: [...(this.options.requestedModules || [])],
|
||||
plannedModules: [...this.options.modules],
|
||||
defaultModules: [...DEFAULT_MODULES],
|
||||
defaultPlanSelected: DEFAULT_MODULES.every((name) => this.options.modules.includes(name)),
|
||||
notSelectedModules: [...AVAILABLE_MODULES].filter(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,
|
||||
@@ -2121,11 +2172,9 @@ export class AcceptanceRun {
|
||||
pageAcceptance: 'NOT_VERIFIED',
|
||||
},
|
||||
generatedAt: new Date().toISOString(), startedAt: this.startedAt,
|
||||
success: this.fatal === null && summary.failed === 0, fatal: this.fatal,
|
||||
fullCoverage: this.coverageGaps.length === 0 && !this.modules.some(module => module.skipped),
|
||||
fatal: this.fatal,
|
||||
...(this.options.prepareDomesticPdaH5 ? { fullAcceptance: false, purpose: 'DOMESTIC_PDA_H5_FIXTURE' } : {}),
|
||||
...(this.options.prepareMeasurementReview ? { fullAcceptance: false, purpose: 'MEASUREMENT_REVIEW_UI_FIXTURE' } : {}),
|
||||
summary,
|
||||
modules: this.modules, scenarios: this.scenarios, deploymentBlockers: this.deploymentBlockers,
|
||||
coverageGaps: this.coverageGaps,
|
||||
createdResources: this.createdResources,
|
||||
@@ -2136,7 +2185,7 @@ export class AcceptanceRun {
|
||||
],
|
||||
securityNote: '报告不保存登录密码、Access Token、Refresh Token、图片二进制或 OCR 原文全文。',
|
||||
steps: this.steps,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2208,8 +2257,7 @@ async function main() {
|
||||
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
|
||||
finalizeAcceptanceReport(report)
|
||||
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