From 7e1703bde990adaef3cc680ab1ed207fc4da4e63 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 23 Oct 2026 02:13:47 +0800 Subject: [PATCH] test: prepare measurement review fixtures without approval --- scripts/README.md | 2 ++ scripts/admin-test-env-acceptance.mjs | 11 ++++++++++- scripts/admin-test-env-modules/README.md | 2 ++ ...inbound-measurement-box-code-contract.test.mjs | 13 +++++++++++++ .../inbound-measurement.mjs | 15 +++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index 00b4d5de5..8b9eed3e2 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -284,6 +284,8 @@ Client 全量、Client 入库测量与计费、待审核订单和批量待分柜 同一入口可显式添加 `--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` 覆盖退税文件确认链;真实邮件和供应商自动查询不在此闭环的完成声明中。 diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index ef59d10cd..c404e3d8d 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -432,6 +432,7 @@ export function parseArguments(argv) { 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-measurement-review') && values.get('prepare-measurement-review') !== 'true') throw new Error('prepare-measurement-review 必须明确为 true') return { environment, 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), waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1), prepareDropshipManualCount: values.get('prepare-dropship-manual-count') === 'true', + prepareMeasurementReview: values.get('prepare-measurement-review') === 'true', verifyPalletLocationGuide: values.has('verify-pallet-location-guide'), routeMode: values.get('route-mode') || 'fixed', 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.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.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 (options.modules.some((module) => ['phase3-loading', 'phase3-container-loading', 'phase5-root-fulfillment'].includes(module)) && (!options.pdaUsername?.trim() || !options.pdaPassword?.trim())) { @@ -1956,6 +1963,7 @@ export class AcceptanceRun { break } if (module === 'inbound-measurement' + && !this.options.prepareMeasurementReview && this.captureWaitSplitFixture(this.options.waitSplitSequence || 1)) { await this.createAdditionalWaitSplitFixtures() } @@ -2007,7 +2015,7 @@ export class AcceptanceRun { plannedModules: [...this.options.modules], defaultModules: [...DEFAULT_MODULES], 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, evidenceType: 'HTTP', pageAcceptance: 'NOT_VERIFIED', @@ -2016,6 +2024,7 @@ export class AcceptanceRun { success: this.fatal === null && summary.failed === 0, fatal: this.fatal, fullCoverage: this.coverageGaps.length === 0 && !this.modules.some(module => module.skipped), ...(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, diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index a4ff42202..b0b86691f 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -64,6 +64,8 @@ Admin TEST 入口必须通过显式环境文件配置 `ADMIN_TEST_FIXED_CUSTOMER 固定托盘造数中,`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条正式基线,合法扩展项目保留并单独核对分页和筛选。散板普通状态接口仅验证取消及非法派车拒绝,原单须出现在待匹配需求池;需求池接车到签收另列覆盖缺口,不以手工跳状态冒充履约。 预约工作查询在本轮业务写入后,仅对服务端明确返回的 A0409“预约批次或运输关联已变化,服务端正在协调完整工作清单”或“已开船工作范围变化,请先完成服务端同步”每 5 秒重读,最多等待 240 秒;超时保留失败,其他错误不重试。这是后台完整扫描的就绪等待,不是页面响应性能验收,也不触发同步写接口。 diff --git a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs index 14c7fbcb1..32dc2069d 100644 --- a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' +import { parseArguments, validateExecutionOptions } from '../admin-test-env-acceptance.mjs' import { completeDropshipPalletization, verifyMeasurementSubmitReplay, 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 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', () => { assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/) assert.doesNotMatch(source, /#V\d+#/) diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index f4c7273b1..42e5860bd 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -1313,6 +1313,21 @@ export async function run(context) { && Number.isInteger(Number(pendingSummary?.version)), `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 approvalPayload = { measurementId: draft.measurementId, expectedVersion: Number(pendingSummary?.version), reason: null,