test: prepare isolated gray domestic PDA H5 tasks without auto-picking

This commit is contained in:
2026-10-09 17:21:56 +08:00
parent 023b28e31f
commit 74bf0e73c5
7 changed files with 53 additions and 2 deletions
+3
View File
@@ -205,6 +205,9 @@ Client 全量、Client 入库测量与计费、待审核订单和批量待分柜
### 灰度环境 FBA 定向闭环
同一入口可显式添加 `--prepare-domestic-pda-h5 true`,仅允许单独指定 `phase5-root-fulfillment`:创建本轮客户和两箱根订单,经测量、分柜、订舱放舱和计划确认生成一箱待拣任务及箱唛后停止,不领取/拣货/完成任务、不进入海外链。报告标记 `fullAcceptance=false`、`PREPARED_NOT_VERIFIED`,`createdResources.phase5RootFulfillment.domesticPdaH5Fixture` 保存任务、柜次、计划、仓库、入仓来源及箱信息供 H5;成功只代表准备完成。样本保留,失败不自动重试或清理,默认不传该参数时继续完整链。
`admin-test-env-acceptance.mjs --environment gray` 显式支持临时灰度环境,当前仅开放 `--modules phase5-root-fulfillment` 及自动展开的本轮客户资质 fixture。该链会通过正式 HTTP 新建隔离客户、两箱根订单及分柜子来源,推进国内测量、装柜、报关、人工船节点、海外收货、排车、出库和签收;不是默认全量验收。默认全量中包含 TEST 专用故障注入和未经灰度副作用核对的模块,因此 gray 拒绝省略模块、附加模块、批量订单和卸柜故障开关,也不会创建 TEST fault executor。`--f40-tax-mode TAX_REFUND` 覆盖退税文件确认链;真实邮件和供应商自动查询不在此闭环的完成声明中。
明确的仓库外环境文件须配置 `ADMIN_GRAY_GATEWAY`(统一 HTTPS 源站,不带 `/api`)、`ADMIN_GRAY_USERNAME`、`ADMIN_GRAY_PASSWORD`、`ADMIN_GRAY_PDA_USERNAME`、`ADMIN_GRAY_PDA_PASSWORD`;可选 `ADMIN_GRAY_CUSTOMER_NAME`。gray 不读取 TEST Gateway、管理员或 PDA 凭据。F40 始终创建本轮客户,不需要传已有 merchant 账号。数据库只读取证使用 `JENKINS_URL`、`JENKINS_USER`、`JENKINS_API_TOKEN` 及 Jenkins 已登记的 production SSH/host/known-hosts/env 凭据,由 `gray-regression-evidence.mjs` 实现,不调用 TEST agent 或写业务 SQL。
+7 -2
View File
@@ -313,6 +313,7 @@ function usage() {
--merchant-username <name> 复用既有已审核客户账号,不再创建客户
--merchant-password <value> 复用客户账号密码,不写入报告
--origin-warehouse-name <name> 固定国内起运仓名称
--prepare-domestic-pda-h5 true 仅准备新待拣任务供H5,不执行完整履约验收
--channel-name <name> 固定物流渠道名称
--channel-code <code> 固定物流渠道编码
--order-profile-mode <mode> 订单档案模式:fixed 或 randomized,默认fixed
@@ -408,6 +409,7 @@ export function parseArguments(argv) {
const fixtureIndex = modules.indexOf('merchant-qualification-fixture')
if (fixtureIndex >= 0) modules.splice(fixtureIndex, 1)
}
if (values.has('prepare-domestic-pda-h5') && values.get('prepare-domestic-pda-h5') !== 'true') throw new Error('prepare-domestic-pda-h5 必须明确为 true;完整验收请省略此参数')
return {
environment,
gateway: (values.get('gateway') || envValue('ADMIN_TEST_GATEWAY', 'ADMIN_GRAY_GATEWAY')).replace(/\/$/, ''),
@@ -437,6 +439,7 @@ export function parseArguments(argv) {
returnMainChainOnly: values.get('return-main-chain-only') === 'true',
verifyUnloadEventRecovery: values.get('verify-unload-event-recovery') === 'true',
f40TaxMode: values.get('f40-tax-mode') || '',
prepareDomesticPdaH5: values.get('prepare-domestic-pda-h5') === 'true',
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
manualSplitPlanId: values.get('manual-split-plan-id') || '',
manualPreloadSourceReport: values.get('manual-preload-source-report') || '',
@@ -465,6 +468,7 @@ export function validateExecutionOptions(options) {
}
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 (options.prepareDomesticPdaH5 && (options.requestedModules?.length !== 1 || options.requestedModules[0] !== 'phase5-root-fulfillment')) throw new Error('prepare-domestic-pda-h5 仅允许单独指定 phase5-root-fulfillment')
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())) {
@@ -624,7 +628,7 @@ export class AcceptanceRun {
this.phase4ProducerAttempts = new Map()
this.deploymentBlockers = []
this.coverageGaps = []
this.context = options.f40TaxMode ? { f40TaxMode: options.f40TaxMode } : {}
this.context = { ...(options.f40TaxMode ? { f40TaxMode: options.f40TaxMode } : {}), ...(options.prepareDomesticPdaH5 ? { prepareDomesticPdaH5: true } : {}) }
this.token = ''
this.pdaToken = ''
this.refreshToken = ''
@@ -1932,10 +1936,11 @@ export class AcceptanceRun {
const summary = buildAcceptanceSummary(this.steps, this.modules, this.coverageGaps)
return {
reportName: this.options.environment === 'gray' ? '灰度环境 FBA 定向闭环 HTTP API 验收' : '后台管理测试环境 HTTP API 验收', marker: this.marker,
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,
generatedAt: new Date().toISOString(), startedAt: this.startedAt,
success: this.fatal === null && summary.failed === 0, fatal: this.fatal,
...(this.options.prepareDomesticPdaH5 ? { fullAcceptance: false, purpose: 'DOMESTIC_PDA_H5_FIXTURE' } : {}),
summary,
modules: this.modules, scenarios: this.scenarios, deploymentBlockers: this.deploymentBlockers,
coverageGaps: this.coverageGaps,
+2
View File
@@ -1,5 +1,7 @@
# Admin 测试环境 HTTP 模块
`phase5-root-fulfillment` 可由总入口显式 `--prepare-domestic-pda-h5 true` 仅准备本轮国内H5待拣任务。调用 `phase3-order-splitting.loadOneFragment` 到正式计划确认、待拣任务及箱唛就绪即返回,不领取、不拣货、不继续海外;结果固定 `PREPARED_NOT_VERIFIED`,不算全链验收。默认完整链不变,范围及授权见根脚本README。
`fleet-capacity-pool-schema-contract.test.mjs` 是离线数据库合同:验证发布与持久命令唯一约束、唯一商业履约来源、可空实际数量、服务与统一基线一致、非破坏性结构补齐及独立销售/调度权限;不访问环境、不证明真实履约已通过。
## 当前新增脚本的执行边界
@@ -136,3 +136,16 @@ test('booking fixture never invents supplier names without formal IDs', () => {
assert.doesNotMatch(source, /SPLIT-SHIPPING|SPLIT-AGENT/)
assert.doesNotMatch(source, /shippingCompanyName: `|bookingAgentName: `/)
})
test('H5 preparation stops after real labels but before claim and refuses already picked data', () => {
const checkpoint = source.indexOf('if (context.context?.prepareDomesticPdaH5)')
const claim = source.indexOf('const claim = await request', checkpoint)
assert.ok(checkpoint > source.indexOf('const labelResponse ='))
assert.ok(claim > checkpoint)
const boundary = source.slice(checkpoint, claim)
assert.match(boundary, /pickedPackageCount\) !== 0/)
assert.match(boundary, /throw new Error/)
assert.match(boundary, /return \{ preparedForH5: true/)
assert.doesNotMatch(boundary, /request\(/)
})
@@ -392,6 +392,14 @@ export async function loadOneFragment(context, state) {
`HTTP=${labelResponse.http}, contentType=${labelResponse.contentType || 'missing'}, bytes=${labelResponse.bytes?.length ?? 0}`, OPTIONS)
if (labelResponse.http !== 200 || !Number(labelResponse.bytes?.length)) return null
if (context.context?.prepareDomesticPdaH5) {
if (task?.taskStatus === 'COMPLETED' || !task.items?.length || task.items.some(row => Number(row.pickedPackageCount) !== 0))
throw new Error('H5准备只允许尚未拣货的新任务')
return { preparedForH5: true, taskId, planId, containerId, oceanBookingId,
warehouseId, sourceInboundId: id(candidate.inboundId), sourceOrderId,
containerNo: realContainerNo, taskNo: task.taskNo, items: task.items }
}
const claim = await request(context, 'POST', `${PDA}/tasks/${taskId}/claim`, {
expectedAssignmentVersion: 0, operationId: `${operationBase}-claim`,
})
@@ -400,6 +400,7 @@ async function fragment(context, source, rootOrderId, sequence) {
warehouseId: id(summary.warehouseId), warehouseName: summary.warehouseName,
sourcePackageCount: sequence === 0 ? 2 : 1, sequence,
})
if (context.context?.prepareDomesticPdaH5 && result?.preparedForH5) return result
check(context, 'F40 一箱来源通过真实 PDA 完成装柜', Boolean(result?.taskId && result?.sourceInboundId), `sequence=${sequence}`)
const current = await api(context, 'GET', '回查该片真实入仓来源',
query(`/api/platform/inbounds/${result.sourceInboundId}`, { tenantId: id(summary.tenantId) }))
@@ -618,6 +619,14 @@ async function runRootFulfillment(context) {
&& Number(scope.createdResources.inboundMeasurement.actualPackageCount) === 2, `root=${rootOrderId}`)
proof.currentStage = 'ROOT_LOAD_AND_SHIP'
const first = await fragment(scope, rootInbound, rootOrderId, 0)
if (scope.context.prepareDomesticPdaH5) {
check(scope, '国内H5待拣任务已准备且未执行拣货', first?.preparedForH5 === true && Boolean(first.taskId), 'PREPARED_NOT_VERIFIED')
proof.domesticPdaH5Fixture = first
proof.finalStatus = 'PREPARED_NOT_VERIFIED'
proof.currentStage = 'DOMESTIC_PDA_H5_READY'
proof.cleanup = 'Fresh customer/order/container/task retained for manual H5 execution; no picking, completion or overseas work executed'
return proof
}
proof.fragments.push(first)
proof.currentStage = 'REMAINDER_IDENTITY'
const tree = await api(scope, 'GET', '首柜 PDA 完成后读取正式余量树',
@@ -86,3 +86,14 @@ test('direct gray evidence use cannot bypass module/fault restrictions or failed
})), /gray SSH unavailable/)
assert.equal(run.context.phase4ReceivingEvidenceReader, undefined)
})
test('domestic H5 fixture is opt-in and cannot expand the gray module scope', () => {
const defaults = validateExecutionOptions(parseArguments(args))
assert.equal(defaults.prepareDomesticPdaH5, false)
const prepared = validateExecutionOptions(parseArguments([...args, '--prepare-domestic-pda-h5', 'true']))
assert.equal(new AcceptanceRun(prepared).context.prepareDomesticPdaH5, true)
for (const invalid of ['yes', 'false', '1']) assert.throws(() => parseArguments([...args, '--prepare-domestic-pda-h5', invalid]), /必须明确为 true/)
assert.deepEqual(prepared.requestedModules, ['phase5-root-fulfillment'])
assert.throws(() => validateExecutionOptions({ ...prepared, environment: 'test', requestedModules: ['orders'] }), /prepare-domestic-pda-h5/)
})