test: prepare measurement review fixtures without approval

This commit is contained in:
2026-10-23 02:13:47 +08:00
parent 6cd27658f7
commit 7e1703bde9
5 changed files with 42 additions and 1 deletions
+2
View File
@@ -284,6 +284,8 @@ Client 全量、Client 入库测量与计费、待审核订单和批量待分柜
同一入口可显式添加 `--prepare-domestic-pda-h5 true`,仅允许单独指定 `phase5-root-fulfillment`:创建本轮客户和两箱根订单,经测量、分柜、订舱放舱和计划确认生成一箱待拣任务及箱唛后停止,不领取/拣货/完成任务、不进入海外链。报告标记 `fullAcceptance=false`、`PREPARED_NOT_VERIFIED`,`createdResources.phase5RootFulfillment.domesticPdaH5Fixture` 保存任务、柜次、计划、仓库、入仓来源及箱信息供 H5;成功只代表准备完成。样本保留,失败不自动重试或清理,默认不传该参数时继续完整链。 同一入口可显式添加 `--prepare-domestic-pda-h5 true`,仅允许单独指定 `phase5-root-fulfillment`:创建本轮客户和两箱根订单,经测量、分柜、订舱放舱和计划确认生成一箱待拣任务及箱唛后停止,不领取/拣货/完成任务、不进入海外链。报告标记 `fullAcceptance=false`、`PREPARED_NOT_VERIFIED`,`createdResources.phase5RootFulfillment.domesticPdaH5Fixture` 保存任务、柜次、计划、仓库、入仓来源及箱信息供 H5;成功只代表准备完成。样本保留,失败不自动重试或清理,默认不传该参数时继续完整链。
测量审核页面准备使用 `--modules inbound-measurement --prepare-measurement-review true --output <独立报告>`,环境和凭据沿用本入口显式配置;会创建本轮订单与测量证据并提交待审,不审核通过。仅允许 TEST 单票最小依赖,不能混用其他准备模式或后续模块;报告标注 `MEASUREMENT_REVIEW_UI_FIXTURE/fullAcceptance=false`,失败保留数据,不自动重试或清理。具体样本字段见模块说明。
`admin-test-env-acceptance.mjs --environment gray` 显式支持临时灰度环境,当前仅开放 `--modules phase5-root-fulfillment` 及自动展开的本轮客户资质 fixture。该链会通过正式 HTTP 新建隔离客户、两箱根订单及分柜子来源,推进国内测量、装柜、报关、人工船节点、海外收货、排车、出库和签收;不是默认全量验收。默认全量中包含 TEST 专用故障注入和未经灰度副作用核对的模块,因此 gray 拒绝省略模块、附加模块、批量订单和卸柜故障开关,也不会创建 TEST fault executor。`--f40-tax-mode TAX_REFUND` 覆盖退税文件确认链;真实邮件和供应商自动查询不在此闭环的完成声明中。 `admin-test-env-acceptance.mjs --environment gray` 显式支持临时灰度环境,当前仅开放 `--modules phase5-root-fulfillment` 及自动展开的本轮客户资质 fixture。该链会通过正式 HTTP 新建隔离客户、两箱根订单及分柜子来源,推进国内测量、装柜、报关、人工船节点、海外收货、排车、出库和签收;不是默认全量验收。默认全量中包含 TEST 专用故障注入和未经灰度副作用核对的模块,因此 gray 拒绝省略模块、附加模块、批量订单和卸柜故障开关,也不会创建 TEST fault executor。`--f40-tax-mode TAX_REFUND` 覆盖退税文件确认链;真实邮件和供应商自动查询不在此闭环的完成声明中。
+10 -1
View File
@@ -432,6 +432,7 @@ export function parseArguments(argv) {
if (fixtureIndex >= 0) modules.splice(fixtureIndex, 1) 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;完整验收请省略此参数') if (values.has('prepare-domestic-pda-h5') && values.get('prepare-domestic-pda-h5') !== 'true') throw new Error('prepare-domestic-pda-h5 必须明确为 true;完整验收请省略此参数')
if (values.has('prepare-measurement-review') && values.get('prepare-measurement-review') !== 'true') throw new Error('prepare-measurement-review 必须明确为 true')
return { return {
environment, environment,
gateway: (values.get('gateway') || envValue('ADMIN_TEST_GATEWAY', 'ADMIN_GRAY_GATEWAY')).replace(/\/$/, ''), gateway: (values.get('gateway') || envValue('ADMIN_TEST_GATEWAY', 'ADMIN_GRAY_GATEWAY')).replace(/\/$/, ''),
@@ -456,6 +457,7 @@ export function parseArguments(argv) {
waitSplitSequence: Number(values.get('wait-split-sequence') || 1), waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1), waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
prepareDropshipManualCount: values.get('prepare-dropship-manual-count') === 'true', prepareDropshipManualCount: values.get('prepare-dropship-manual-count') === 'true',
prepareMeasurementReview: values.get('prepare-measurement-review') === 'true',
verifyPalletLocationGuide: values.has('verify-pallet-location-guide'), verifyPalletLocationGuide: values.has('verify-pallet-location-guide'),
routeMode: values.get('route-mode') || 'fixed', routeMode: values.get('route-mode') || 'fixed',
quiet: values.get('quiet') === 'true', quiet: values.get('quiet') === 'true',
@@ -492,6 +494,11 @@ 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.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.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 (options.prepareDomesticPdaH5 && (options.requestedModules?.length !== 1 || options.requestedModules[0] !== 'phase5-root-fulfillment')) throw new Error('prepare-domestic-pda-h5 仅允许单独指定 phase5-root-fulfillment')
if (options.prepareMeasurementReview && (options.environment !== 'test'
|| options.requestedModules?.length !== 1 || options.requestedModules[0] !== 'inbound-measurement'
|| options.waitSplitCount !== 1 || options.prepareDomesticPdaH5 || options.prepareDropshipManualCount)) {
throw new Error('prepare-measurement-review 仅允许 TEST 单票 inbound-measurement 待审准备,不能混用其他准备模式或后续模块')
}
if ('overseasGateway' in options) throw new Error('Use the single gateway option') 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)) if (options.modules.some((module) => ['phase3-loading', 'phase3-container-loading', 'phase5-root-fulfillment'].includes(module))
&& (!options.pdaUsername?.trim() || !options.pdaPassword?.trim())) { && (!options.pdaUsername?.trim() || !options.pdaPassword?.trim())) {
@@ -1956,6 +1963,7 @@ export class AcceptanceRun {
break break
} }
if (module === 'inbound-measurement' if (module === 'inbound-measurement'
&& !this.options.prepareMeasurementReview
&& this.captureWaitSplitFixture(this.options.waitSplitSequence || 1)) { && this.captureWaitSplitFixture(this.options.waitSplitSequence || 1)) {
await this.createAdditionalWaitSplitFixtures() await this.createAdditionalWaitSplitFixtures()
} }
@@ -2007,7 +2015,7 @@ export class AcceptanceRun {
plannedModules: [...this.options.modules], plannedModules: [...this.options.modules],
defaultModules: [...DEFAULT_MODULES], defaultModules: [...DEFAULT_MODULES],
defaultPlanSelected: DEFAULT_MODULES.every((name) => this.options.modules.includes(name)), defaultPlanSelected: DEFAULT_MODULES.every((name) => this.options.modules.includes(name)),
preparationOnly: this.options.prepareDomesticPdaH5 === true || this.options.prepareDropshipManualCount === true, preparationOnly: this.options.prepareDomesticPdaH5 === true || this.options.prepareDropshipManualCount === true || this.options.prepareMeasurementReview === true,
returnMainChainOnly: this.options.returnMainChainOnly === true, returnMainChainOnly: this.options.returnMainChainOnly === true,
evidenceType: 'HTTP', evidenceType: 'HTTP',
pageAcceptance: 'NOT_VERIFIED', pageAcceptance: 'NOT_VERIFIED',
@@ -2016,6 +2024,7 @@ export class AcceptanceRun {
success: this.fatal === null && summary.failed === 0, fatal: this.fatal, success: this.fatal === null && summary.failed === 0, fatal: this.fatal,
fullCoverage: this.coverageGaps.length === 0 && !this.modules.some(module => module.skipped), fullCoverage: this.coverageGaps.length === 0 && !this.modules.some(module => module.skipped),
...(this.options.prepareDomesticPdaH5 ? { fullAcceptance: false, purpose: 'DOMESTIC_PDA_H5_FIXTURE' } : {}), ...(this.options.prepareDomesticPdaH5 ? { fullAcceptance: false, purpose: 'DOMESTIC_PDA_H5_FIXTURE' } : {}),
...(this.options.prepareMeasurementReview ? { fullAcceptance: false, purpose: 'MEASUREMENT_REVIEW_UI_FIXTURE' } : {}),
summary, summary,
modules: this.modules, scenarios: this.scenarios, deploymentBlockers: this.deploymentBlockers, modules: this.modules, scenarios: this.scenarios, deploymentBlockers: this.deploymentBlockers,
coverageGaps: this.coverageGaps, coverageGaps: this.coverageGaps,
+2
View File
@@ -64,6 +64,8 @@ Admin TEST 入口必须通过显式环境文件配置 `ADMIN_TEST_FIXED_CUSTOMER
固定托盘造数中,`inbound-measurement.mjs` 只复用 `TP-001`、`YW-TP-002` 指定池中的现有托盘;一只可复用,不要求两只同时存在,两只均缺失才阻断,不自动选择池外托盘。仅明确设置内部 `allowAnyExistingPallet` 的通用场景可读取当前仓库已有资源;两者均不创建托盘。 固定托盘造数中,`inbound-measurement.mjs` 只复用 `TP-001`、`YW-TP-002` 指定池中的现有托盘;一只可复用,不要求两只同时存在,两只均缺失才阻断,不自动选择池外托盘。仅明确设置内部 `allowAnyExistingPallet` 的通用场景可读取当前仓库已有资源;两者均不创建托盘。
待审页面验收可用总入口 `--modules inbound-measurement --prepare-measurement-review true`:只允许 TEST 单票及最小依赖,正式创建、测量并提交后停在 `MEASURE_PENDING/PENDING`,不调用审核或生成 WAIT_SPLIT 后续样本。报告 `fullAcceptance=false`、`preparationOnly=true`,业务身份在 `createdResources.measurementReviewUiFixture`;失败保留原数据和报告,不自动重跑或清理。默认完整测量链不受影响。
OCR模块必须通过`ADMIN_TEST_OCR_IMAGE`显式指定本轮图片,不依赖个人桌面路径;定向运行自动展开客户资质及订单前置。识别失败的空字段允许省略,但识别成功必须有有效重量与单位;人工修正和确认分别增加版本。合成称重测试图只证明上传、识别响应及人工确认协议,不代表现场称重照片或设备验收。费用项目模块按受控编码验证112条正式基线,合法扩展项目保留并单独核对分页和筛选。散板普通状态接口仅验证取消及非法派车拒绝,原单须出现在待匹配需求池;需求池接车到签收另列覆盖缺口,不以手工跳状态冒充履约。 OCR模块必须通过`ADMIN_TEST_OCR_IMAGE`显式指定本轮图片,不依赖个人桌面路径;定向运行自动展开客户资质及订单前置。识别失败的空字段允许省略,但识别成功必须有有效重量与单位;人工修正和确认分别增加版本。合成称重测试图只证明上传、识别响应及人工确认协议,不代表现场称重照片或设备验收。费用项目模块按受控编码验证112条正式基线,合法扩展项目保留并单独核对分页和筛选。散板普通状态接口仅验证取消及非法派车拒绝,原单须出现在待匹配需求池;需求池接车到签收另列覆盖缺口,不以手工跳状态冒充履约。
预约工作查询在本轮业务写入后,仅对服务端明确返回的 A0409“预约批次或运输关联已变化,服务端正在协调完整工作清单”或“已开船工作范围变化,请先完成服务端同步”每 5 秒重读,最多等待 240 秒;超时保留失败,其他错误不重试。这是后台完整扫描的就绪等待,不是页面响应性能验收,也不触发同步写接口。 预约工作查询在本轮业务写入后,仅对服务端明确返回的 A0409“预约批次或运输关联已变化,服务端正在协调完整工作清单”或“已开船工作范围变化,请先完成服务端同步”每 5 秒重读,最多等待 240 秒;超时保留失败,其他错误不重试。这是后台完整扫描的就绪等待,不是页面响应性能验收,也不触发同步写接口。
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import test from 'node:test' import test from 'node:test'
import { parseArguments, validateExecutionOptions } from '../admin-test-env-acceptance.mjs'
import { completeDropshipPalletization, verifyMeasurementSubmitReplay, import { completeDropshipPalletization, verifyMeasurementSubmitReplay,
verifyMeasurementApprovalReplay } from './inbound-measurement.mjs' verifyMeasurementApprovalReplay } from './inbound-measurement.mjs'
@@ -11,6 +12,18 @@ const measurementTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/
const cartonTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java') const cartonTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java')
const reviewTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java') const reviewTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java')
test('measurement review preparation is explicit and cannot continue downstream or bulk flows', () => {
const args = ['--environment', 'test', '--gateway', 'https://test.invalid', '--username', 'fixture', '--password', 'fixture-only', '--modules', 'inbound-measurement', '--prepare-measurement-review', 'true']
const options = parseArguments(args)
assert.equal(options.prepareMeasurementReview, true)
assert.doesNotThrow(() => validateExecutionOptions(options))
assert.throws(() => parseArguments([...args.slice(0, -1), 'false']), /必须明确/)
for (const change of [{ waitSplitCount: 2 }, { prepareDropshipManualCount: true }, { requestedModules: ['inbound-measurement', 'phase3-customs'] }]) {
assert.throws(() => validateExecutionOptions({ ...options, ...change }), /待审准备/)
}
assert.equal(parseArguments(args.slice(0, -2)).prepareMeasurementReview, false)
})
test('measurement acceptance scans the stable box code frozen by first print', () => { test('measurement acceptance scans the stable box code frozen by first print', () => {
assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/) assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/)
assert.doesNotMatch(source, /#V\d+#/) assert.doesNotMatch(source, /#V\d+#/)
@@ -1313,6 +1313,21 @@ export async function run(context) {
&& Number.isInteger(Number(pendingSummary?.version)), && Number.isInteger(Number(pendingSummary?.version)),
`status=${pendingSummary?.processStatus ?? 'missing'}, version=${pendingSummary?.version ?? 'missing'}`, OPTIONS) `status=${pendingSummary?.processStatus ?? 'missing'}, version=${pendingSummary?.version ?? 'missing'}`, OPTIONS)
if (context.options?.prepareMeasurementReview) {
const ready = pendingSummary?.processStatus === 'MEASURE_PENDING'
&& pendingSummary?.measureReviewStatus === 'PENDING'
&& Number.isInteger(Number(pendingSummary?.version))
context.createdResources.measurementReviewUiFixture = {
marker: context.marker, inboundId, tenantId, inboundCode,
measurementId: asId(draft.measurementId), expectedVersion: pendingSummary?.version,
status: ready ? 'PREPARED_NOT_VERIFIED' : 'PREPARATION_FAILED', fullAcceptance: false,
}
context.assert(MODULE, '待审页面样本准备完成,未执行审核', pendingInboundResponse, ready,
`inboundCode=${inboundCode}, measurementId=${asId(draft.measurementId)}`, OPTIONS)
finishScenario(context, start)
return
}
const approvalPath = `/api/platform/v1/inbounds/${inboundId}/measurements/approve?${query(tenantId)}` const approvalPath = `/api/platform/v1/inbounds/${inboundId}/measurements/approve?${query(tenantId)}`
const approvalPayload = { const approvalPayload = {
measurementId: draft.measurementId, expectedVersion: Number(pendingSummary?.version), reason: null, measurementId: draft.measurementId, expectedVersion: Number(pendingSummary?.version), reason: null,