This commit is contained in:
2026-10-08 17:07:57 +08:00
parent 16d2d9111a
commit c1ddb658bd
111 changed files with 2914 additions and 941 deletions
+72 -20
View File
@@ -5,7 +5,7 @@ import { dirname, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', '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', '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', 'phase4-tail-order-controls', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-return-receipt', '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', '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', 'dashboard', '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', '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', 'phase4-tail-order-controls', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-return-receipt', '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', '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 OPTIONAL_MODULES = ['phase5-root-fulfillment', 'phase4-overseas-return-followups', 'manual-order-splitting-fixture', 'manual-order-splitting-verify',
'manual-preload-push-fixture', 'manual-preload-push-verify',
'manual-pda-ready-fixture', 'manual-pda-ready-verify',
@@ -291,7 +291,9 @@ function usage() {
--password <value> 必填,管理员密码
--pda-username <name> 国内装柜链必填,实际领取任务的 PDA 作业员账号
--pda-password <value> 国内装柜链必填,PDA 作业员密码
--environment <name> 默认 test;gray 仅允许显式 FBA 定向闭环
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
--f40-tax-mode <mode> F40 税制:BUY_DECLARATION 或 TAX_REFUND
--customer-name <name> 指定本轮隔离客户名称,默认自动生成
--merchant-username <name> 复用既有已审核客户账号,不再创建客户
--merchant-password <value> 复用客户账号密码,不写入报告
@@ -317,7 +319,39 @@ function usage() {
也可使用环境变量: ADMIN_TEST_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD,
ADMIN_TEST_WEIGHT_IMAGE_PATH, ADMIN_TEST_CUSTOMER_NAME, MERCHANT_TEST_USERNAME,
MERCHANT_TEST_PASSWORD, ADMIN_PDA_USERNAME, ADMIN_PDA_PASSWORD, ADMIN_WAIT_SPLIT_COUNT`)
MERCHANT_TEST_PASSWORD, ADMIN_PDA_USERNAME, ADMIN_PDA_PASSWORD, ADMIN_WAIT_SPLIT_COUNT。
gray 使用独立 ADMIN_GRAY_GATEWAY/USERNAME/PASSWORD、ADMIN_GRAY_PDA_USERNAME/PASSWORD、
ADMIN_GRAY_CUSTOMER_NAME、MERCHANT_GRAY_USERNAME/PASSWORD;不回落 TEST 凭据。`)
}
const fleetPoolGrayOptions = new WeakSet()
/** Dedicated pool orchestrator transport only: no generic module execution or CLI scope switch. */
export function createFleetPoolGrayOptions(input) {
const keys = ['gateway', 'username', 'password', 'output', 'markerNamespace']
if (!input || Object.keys(input).some(key => !keys.includes(key))
|| keys.some(key => typeof input[key] !== 'string' || !input[key].trim())) throw Error('Explicit gray pool transport configuration required')
const target = new URL(input.gateway)
if (target.protocol !== 'https:' || target.username || target.password || target.search || target.hash || target.pathname !== '/')
throw Error('Gray pool requires the unified HTTPS origin')
const options = Object.freeze({ ...input, environment: 'gray', executionScope: 'FLEET_CAPACITY_POOL_GRAY',
modules: Object.freeze([]), requestedModules: Object.freeze([]), quiet: true, waitSplitCount: 1, verifyUnloadEventRecovery: false })
fleetPoolGrayOptions.add(options)
return options
}
function assertGrayExecutionScope(options) {
if (options.environment !== 'gray') return
if (options.verifyUnloadEventRecovery) throw new Error('gray 禁止 TEST 故障注入')
if (fleetPoolGrayOptions.has(options)) return
if (!Array.isArray(options.requestedModules) || options.requestedModules.length !== 1
|| options.requestedModules[0] !== 'phase5-root-fulfillment'
|| !Array.isArray(options.modules) || options.modules.length !== 2
|| options.modules[0] !== 'merchant-qualification-fixture'
|| options.modules[1] !== 'phase5-root-fulfillment') {
throw new Error('gray 仅支持显式 phase5-root-fulfillment 及其本轮客户依赖;其他模块尚未核对环境副作用')
}
if (options.waitSplitCount !== 1) throw new Error('gray 定向闭环只支持单轮订单,不运行批量造数')
}
export function parseArguments(argv) {
@@ -337,12 +371,15 @@ export function parseArguments(argv) {
}
}
if (values.has('overseas-gateway') || process.env.ADMIN_OVERSEAS_GATEWAY) throw new Error('Remove overseas-gateway / ADMIN_OVERSEAS_GATEWAY; use one TEST gateway')
if (values.has('environment') && values.get('environment') !== 'test') throw new Error('Only test acceptance is supported')
const environment = values.get('environment') || 'test'
if (!['test', 'gray'].includes(environment)) throw new Error('Only test or explicit gray acceptance is supported')
if (environment === 'gray' && !values.get('modules')) throw new Error('gray 必须显式指定 --modules phase5-root-fulfillment,不能运行默认全量')
const envValue = (testKey, grayKey) => process.env[environment === 'gray' ? grayKey : testKey] || ''
const requestedModules = (values.get('modules') || DEFAULT_MODULES.join(','))
.split(',').map((v) => v.trim()).filter(Boolean)
const modules = normalizeModuleOrder(requestedModules)
const merchantUsername = values.get('merchant-username') || process.env.MERCHANT_TEST_USERNAME || ''
const merchantPassword = values.get('merchant-password') || process.env.MERCHANT_TEST_PASSWORD || ''
const merchantUsername = values.get('merchant-username') || envValue('MERCHANT_TEST_USERNAME', 'MERCHANT_GRAY_USERNAME')
const merchantPassword = values.get('merchant-password') || envValue('MERCHANT_TEST_PASSWORD', 'MERCHANT_GRAY_PASSWORD')
const requiresMarkerOwnedMerchantFixture = [
'phase5-root-fulfillment',
'phase4-overseas-business',
@@ -357,22 +394,22 @@ export function parseArguments(argv) {
if (fixtureIndex >= 0) modules.splice(fixtureIndex, 1)
}
return {
environment: 'test',
gateway: (values.get('gateway') || process.env.ADMIN_TEST_GATEWAY || '').replace(/\/$/, ''),
username: values.get('username') || process.env.ADMIN_TEST_USERNAME || '',
password: values.get('password') || process.env.ADMIN_TEST_PASSWORD || '',
pdaUsername: values.get('pda-username') || process.env.ADMIN_PDA_USERNAME || '',
pdaPassword: values.get('pda-password') || process.env.ADMIN_PDA_PASSWORD || '',
environment,
gateway: (values.get('gateway') || envValue('ADMIN_TEST_GATEWAY', 'ADMIN_GRAY_GATEWAY')).replace(/\/$/, ''),
username: values.get('username') || envValue('ADMIN_TEST_USERNAME', 'ADMIN_GRAY_USERNAME'),
password: values.get('password') || envValue('ADMIN_TEST_PASSWORD', 'ADMIN_GRAY_PASSWORD'),
pdaUsername: values.get('pda-username') || envValue('ADMIN_PDA_USERNAME', 'ADMIN_GRAY_PDA_USERNAME'),
pdaPassword: values.get('pda-password') || envValue('ADMIN_PDA_PASSWORD', 'ADMIN_GRAY_PDA_PASSWORD'),
modules,
requestedModules,
customerName: values.get('customer-name') || process.env.ADMIN_TEST_CUSTOMER_NAME || '',
customerName: values.get('customer-name') || envValue('ADMIN_TEST_CUSTOMER_NAME', 'ADMIN_GRAY_CUSTOMER_NAME'),
merchantUsername,
merchantPassword,
originWarehouseName: values.get('origin-warehouse-name') || '',
channelName: values.get('channel-name') || '',
channelCode: values.get('channel-code') || '',
orderProfileMode: values.get('order-profile-mode') || 'fixed',
waitSplitCount: Number(values.get('wait-split-count') || process.env.ADMIN_WAIT_SPLIT_COUNT
waitSplitCount: Number(values.get('wait-split-count') || envValue('ADMIN_WAIT_SPLIT_COUNT', 'ADMIN_GRAY_WAIT_SPLIT_COUNT')
|| (modules.includes('coload-groups') ? 8
: modules.includes('manual-order-splitting-fixture') ? 6 : 1)),
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
@@ -384,6 +421,7 @@ export function parseArguments(argv) {
quiet: values.get('quiet') === 'true',
returnMainChainOnly: values.get('return-main-chain-only') === 'true',
verifyUnloadEventRecovery: values.get('verify-unload-event-recovery') === 'true',
f40TaxMode: values.get('f40-tax-mode') || '',
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
manualSplitPlanId: values.get('manual-split-plan-id') || '',
manualPreloadSourceReport: values.get('manual-preload-source-report') || '',
@@ -395,6 +433,13 @@ export function parseArguments(argv) {
}
export function validateExecutionOptions(options) {
assertGrayExecutionScope(options)
if (options.environment === 'gray') {
const target = new URL(options.gateway)
if (target.protocol !== 'https:' || target.username || target.password || target.search || target.hash
|| target.pathname !== '/') throw new Error('gray gateway 必须为统一 HTTPS 源站,不带 /api、凭据或查询参数')
if (!options.output?.trim()) throw new Error('gray 必须指定独立 --output 报告路径')
}
if (options.verifyUnloadEventRecovery && !options.modules.includes('phase4-overseas-pda')) {
throw new Error('卸柜事件恢复验收必须包含正式 phase4-overseas-pda 收货链')
}
@@ -403,7 +448,8 @@ export function validateExecutionOptions(options) {
for (const [name, value] of [['gateway', options.gateway], ['username', options.username], ['password', options.password]]) {
if (typeof value !== 'string' || value.trim() === '') throw new Error(`缺少必填执行参数: ${name}`)
}
if (options.environment && options.environment !== 'test') throw new Error('Only test acceptance is supported')
if (options.environment && !['test', 'gray'].includes(options.environment)) throw new Error('Only test or explicit gray acceptance is supported')
if (options.f40TaxMode && !['BUY_DECLARATION', 'TAX_REFUND'].includes(options.f40TaxMode)) throw new Error('f40-tax-mode 必须为 BUY_DECLARATION 或 TAX_REFUND')
if ('overseasGateway' in options) throw new Error('Use the single gateway option')
if (options.modules.some((module) => ['phase3-loading', 'phase3-container-loading', 'phase5-root-fulfillment'].includes(module))
&& (!options.pdaUsername?.trim() || !options.pdaPassword?.trim())) {
@@ -551,9 +597,10 @@ export function buildAcceptanceSummary(steps = [], modules = [], coverageGaps =
export class AcceptanceRun {
constructor(options) {
assertGrayExecutionScope(options)
this.options = options
const markerNamespace = String(options.markerNamespace || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 12)
this.marker = `ADMIN-API-${new Date().toISOString().replace(/\D/g, '').slice(0, 17)}${markerNamespace ? `-${markerNamespace}` : ''}`
this.marker = `${options.environment === 'gray' ? 'GRAY' : 'ADMIN'}-API-${new Date().toISOString().replace(/\D/g, '').slice(0, 17)}${markerNamespace ? `-${markerNamespace}` : ''}`
this.startedAt = new Date().toISOString()
this.steps = []
this.modules = []
@@ -562,7 +609,7 @@ export class AcceptanceRun {
this.phase4ProducerAttempts = new Map()
this.deploymentBlockers = []
this.coverageGaps = []
this.context = {}
this.context = options.f40TaxMode ? { f40TaxMode: options.f40TaxMode } : {}
this.token = ''
this.pdaToken = ''
this.refreshToken = ''
@@ -1869,8 +1916,8 @@ export class AcceptanceRun {
const summary = buildAcceptanceSummary(this.steps, this.modules, this.coverageGaps)
return {
reportName: '后台管理测试环境 HTTP API 验收', marker: this.marker,
gateway: this.options.gateway, gateway: this.options.gateway,
reportName: this.options.environment === 'gray' ? '灰度环境 FBA 定向闭环 HTTP API 验收' : '后台管理测试环境 HTTP API 验收', marker: this.marker,
environment: this.options.environment || 'test', gateway: this.options.gateway,
generatedAt: new Date().toISOString(), startedAt: this.startedAt,
success: this.fatal === null && summary.failed === 0, fatal: this.fatal,
summary,
@@ -1878,8 +1925,9 @@ export class AcceptanceRun {
coverageGaps: this.coverageGaps,
createdResources: this.createdResources,
excluded: [
{ scope: 'dashboard', reason: '首页为静态演示数据,不调用后端 API' },
{ scope: 'hidden-prototypes', reason: '隐藏原型页面尚无已确认后端契约' },
...(this.options.environment === 'gray' ? [{ scope: 'full-regression-and-fault-injection',
reason: '仅执行显式 FBA 闭环及所需正式业务依赖;不覆盖默认全量、TEST 故障注入、真实邮件或供应商自动查询' }] : []),
],
securityNote: '报告不保存登录密码、Access Token、Refresh Token、图片二进制或 OCR 原文全文。',
steps: this.steps,
@@ -1898,12 +1946,16 @@ export function unloadRecoveryWasVerified(run) {
}
export async function configureReadOnlyEvidence(run, environment, createReaders, faultFactories) {
assertGrayExecutionScope(run.options)
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 factory = createReaders || (run.options.environment === 'gray'
? (await import('./gray-regression-evidence.mjs')).createGrayEvidenceReaders
: (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
if (run.options.environment === 'gray') return
const faults = []
if (run.options.modules.includes('phase4-cross-region-events')) {
const { createTestFaultExecutor } = faultFactories || await import('./test-event-fault-executor.mjs')