From a02dd3a422448b8a480997f5aa3f1b207f98825d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 11 Aug 2026 17:47:19 +0800 Subject: [PATCH] test(admin): align phase3 loading producer contract --- .../phase3-container-loading.mjs | 10 +- .../admin-test-env-modules/phase3-loading.mjs | 106 ++++++++++-------- .../phase3-producer-chain-contract.test.mjs | 57 ++++++++-- 3 files changed, 111 insertions(+), 62 deletions(-) diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 80c6d9922..d3ff65a11 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -163,7 +163,7 @@ export async function run(context) { const preloadPageResponse = await context.request('GET', `${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}&page=1&size=20`) const preloadPage = context.apiSuccess(MODULE, '查询本轮预装清单', preloadPageResponse, '', OPTIONS) - context.assert(MODULE, '预装清单包含本轮 EDITING 计划', preloadPageResponse, + context.assert(MODULE, '预装清单包含本轮已推送 EDITING 计划', preloadPageResponse, records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'EDITING'), `records=${records(preloadPage).length}`, OPTIONS) @@ -185,7 +185,8 @@ export async function run(context) { failure(wrongWarehouseResponse), `HTTP=${wrongWarehouseResponse.http}, code=${codeOf(wrongWarehouseResponse)}`, OPTIONS) const claimResponse = await request(context, 'POST', `${PDA}/tasks/${taskId}/claim`, { - expectedAssignmentVersion: 0, operationId: `${context.marker}-pda-claim`.slice(0, 128), + expectedAssignmentVersion: Number(task?.assignment?.version || 0), + operationId: `${context.marker}-pda-claim`.slice(0, 128), }) const claimed = context.apiSuccess(MODULE, '领取本轮柜次 PDA 任务', claimResponse, '', OPTIONS) context.assert(MODULE, 'PDA 任务只分配给当前登录作业员', claimResponse, @@ -194,8 +195,8 @@ export async function run(context) { `workflow=${claimed?.workflowStatus || 'missing'}, assigneeId=${id(claimed?.assignment?.assigneeId) || 'missing'}`, OPTIONS) const claimedPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`) - const claimedPlan = context.apiSuccess(MODULE, '领取后回查计划进入 LOADING', claimedPlanResponse, '', OPTIONS) - context.assert(MODULE, '领取后计划进入 LOADING 并记录版本递增', claimedPlanResponse, + const claimedPlan = context.apiSuccess(MODULE, '领取后回查装柜计划', claimedPlanResponse, '', OPTIONS) + context.assert(MODULE, '领取任务后计划进入 LOADING 且版本递增', claimedPlanResponse, claimedPlan?.status === 'LOADING' && Number(claimedPlan?.version) === Number(pushed?.version) + 1, `status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS) @@ -267,6 +268,7 @@ export async function run(context) { id(confirmedDetail?.summary?.planId) === planId && confirmedDetail?.summary?.status === 'COMPLETED' && confirmedDetail?.items?.some?.((item) => id(item.inboundId) === inboundId && id(item.accountId) === tenantId && Boolean(item.frozenAt)) + && confirmedDetail?.histories?.some?.((item) => item.actionType === 'START_LOADING') && confirmedDetail?.histories?.some?.((item) => item.actionType === 'PDA_COMPLETE_LOADING' && Boolean(item.operatorName) && Boolean(item.occurredAt)), `items=${confirmedDetail?.items?.length ?? 'n/a'}, histories=${confirmedDetail?.histories?.length ?? 'n/a'}`, OPTIONS) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index f8954fec8..3df3ddbe8 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -169,16 +169,17 @@ export async function run(context) { { expectedVersion: Number(link.loadingPlanVersion) }, { 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }) const pushedLoadingPlan = context.apiSuccess(MODULE, '桥接后推送柜次计划并生成真实 PDA 任务', pushResponse, '', OPTIONS) - context.assert(MODULE, '推送后计划保持 EDITING 并生成真实 PDA 任务', pushResponse, - pushedLoadingPlan?.status === 'EDITING', `status=${pushedLoadingPlan?.status || 'missing'}`, OPTIONS) - if (pushedLoadingPlan?.status !== 'EDITING') return + context.assert(MODULE, '推送后柜次计划保持 EDITING 且版本递增', pushResponse, + pushedLoadingPlan?.status === 'EDITING' + && Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1, + `status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS) - const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`) - const preloadData = context.apiSuccess(MODULE, '回查预装清单', preload, '', OPTIONS) - context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload, - id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId - && preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker, - `planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS) + const blockedBeforePreloadConfirm = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`, + { expectedVersion: initialVersion, remark: 'must confirm preload first' }, + { 'Idempotency-Key': `${context.marker}-p3-complete-before-preload-confirm`.slice(0, 128) }) + context.addStep(MODULE, '预装未确认时拒绝完成装柜', blockedBeforePreloadConfirm, + failure(blockedBeforePreloadConfirm), + `HTTP=${blockedBeforePreloadConfirm.http}, code=${codeOf(blockedBeforePreloadConfirm)}`, OPTIONS) const canonicalTaskResponse = await context.request('GET', `${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`) @@ -192,49 +193,19 @@ export async function run(context) { if (!canonicalTaskId || !canonicalItem?.id) return const claim = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/claim`, { - expectedAssignmentVersion: 0, operationId: `${context.marker}-bridge-claim`.slice(0, 128), + expectedAssignmentVersion: Number(canonicalTask?.assignment?.version || 0), + operationId: `${context.marker}-bridge-claim`.slice(0, 128), }) const claimed = context.apiSuccess(MODULE, '领取柜次 PDA 任务', claim, '', OPTIONS) context.assert(MODULE, '柜次 PDA 任务绑定当前作业员', claim, claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId), `workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS) - const claimedPlanResponse = await request(context, 'GET', `${smartLoadingBase}/plans/${loadingPlanId}`) - const claimedPlan = context.apiSuccess(MODULE, '领取后回查柜次计划进入 LOADING', claimedPlanResponse, '', OPTIONS) - context.assert(MODULE, '领取后计划进入 LOADING', claimedPlanResponse, - claimedPlan?.status === 'LOADING', `status=${claimedPlan?.status || 'missing'}`, OPTIONS) - if (claimedPlan?.status !== 'LOADING') return - - const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`, - { expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) }) - const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS) - context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming, - confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2, - `status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS) - if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return - - const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`, - { expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) }) - const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS) - context.assert(MODULE, '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm, - confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED' - && Number(confirmed?.preloadPlan?.version) === 3, - `mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS) - if (confirmed?.mainStatus !== 'LOADING') return - - const loadingResponse = await context.request('GET', detailPath) - const loading = context.apiSuccess(MODULE, '读取装柜中入库单版本', loadingResponse, '', OPTIONS) - const loadingVersion = Number(loading?.summary?.version) - context.assert(MODULE, '入库单已进入 LOADING 且版本有效', loadingResponse, - loading?.summary?.processStatus === 'LOADING' && Number.isInteger(loadingVersion), - `status=${loading?.summary?.processStatus || 'missing'}, version=${loading?.summary?.version ?? 'missing'}`, OPTIONS) - if (!Number.isInteger(loadingVersion)) return - - const blockedBeforePicking = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`, - { expectedVersion: loadingVersion, remark: 'must require picking' }, - { 'Idempotency-Key': `${context.marker}-p3-complete-before-picking`.slice(0, 128) }) - context.addStep(MODULE, '未完成 PDA 拣货时拒绝完成装柜', blockedBeforePicking, failure(blockedBeforePicking), - `HTTP=${blockedBeforePicking.http}, code=${codeOf(blockedBeforePicking)}`, OPTIONS) + const claimedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) + const claimedPlan = context.apiSuccess(MODULE, '领取后回查柜次计划', claimedPlanResponse, '', OPTIONS) + context.assert(MODULE, '领取 PDA 任务后计划进入 LOADING 且版本递增', claimedPlanResponse, + claimedPlan?.status === 'LOADING' && Number(claimedPlan?.version) === Number(pushedLoadingPlan?.version) + 1, + `status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS) const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch` const batchKey = `${context.marker}-bridge-batch`.slice(0, 128) @@ -333,6 +304,49 @@ export async function run(context) { atomicBatchReplayVerified, `HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS) + const pickedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) + const pickedPlan = context.apiSuccess(MODULE, '首笔真实拣货后回查柜次计划', pickedPlanResponse, '', OPTIONS) + context.assert(MODULE, '首笔真实拣货触发装柜状态并由单明细批次直接完成', pickedPlanResponse, + directPdaCompletion && pickedPlan?.status === 'COMPLETED', + `status=${pickedPlan?.status || 'missing'}, version=${pickedPlan?.version ?? 'missing'}`, OPTIONS) + + const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`, + { expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) }) + const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS) + context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming, + confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2, + `status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS) + if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return + + const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`, + { expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) }) + const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS) + context.assert(MODULE, directPdaCompletion + ? '装柜已开始时预装确认保持 LOADING 和 CONFIRMING' + : '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm, + directPdaCompletion + ? confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMING' + && Number(confirmed?.preloadPlan?.version) === 2 + : confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED' + && Number(confirmed?.preloadPlan?.version) === 3, + `mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS) + if (confirmed?.mainStatus !== 'LOADING') return + + const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`) + const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS) + context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload, + id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId + && preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker, + `planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS) + + const loadingResponse = await context.request('GET', detailPath) + const loading = context.apiSuccess(MODULE, '读取装柜中入库单版本', loadingResponse, '', OPTIONS) + const loadingVersion = Number(loading?.summary?.version) + context.assert(MODULE, '入库单已进入 LOADING 且版本有效', loadingResponse, + loading?.summary?.processStatus === 'LOADING' && Number.isInteger(loadingVersion), + `status=${loading?.summary?.processStatus || 'missing'}, version=${loading?.summary?.version ?? 'missing'}`, OPTIONS) + if (!Number.isInteger(loadingVersion)) return + // The deployed settlement contract completes a one-item task during pick. A // review call is therefore a negative compatibility assertion, never a second // positive action that can poison the marker fixture with A0409. diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index ced7a193c..5a5016a18 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -1,10 +1,23 @@ import assert from 'node:assert/strict' -import fs from 'node:fs' +import { readFileSync } from 'node:fs' import test from 'node:test' import { selectCustomsFlow } from './phase3-customs.mjs' import { buildFinalizeResource } from './phase3-loading-finalize.mjs' +function source(name) { + return readFileSync(new URL(name, import.meta.url), 'utf8') +} + +function assertOrder(value, snippets) { + let cursor = -1 + for (const snippet of snippets) { + const next = value.indexOf(snippet) + assert.ok(next > cursor, `${snippet} must appear after the previous production step`) + cursor = next + } +} + test('pairs customs source fixture with the matching loading container', () => { const resources = { orchestrationFixtures: { @@ -52,17 +65,37 @@ test('records LOADED and clears the reason only after a real LOADED reread', () assert.equal(result.notCleanedReason, null) }) -test('keeps push and claim state transitions in the verified order', () => { - const containerScript = fs.readFileSync(new URL('./phase3-container-loading.mjs', import.meta.url), 'utf8') - const loadingScript = fs.readFileSync(new URL('./phase3-loading.mjs', import.meta.url), 'utf8') +test('container loading follows push, claim, real pick, completed sequence', () => { + const value = source('./phase3-container-loading.mjs') + assert.match(value, /containerDetail\?\.planStatus === 'WAIT_LOADING'/) + assertOrder(value, [ + "pushed?.status === 'EDITING'", + "claimedPlan?.status === 'LOADING'", + "const pickResponse = await request", + "finalPlan?.status === 'COMPLETED'", + ]) +}) - assert.match(containerScript, /planStatus === 'WAIT_LOADING'/) - assert.match(containerScript, /pushed\?\.status === 'EDITING'/) - assert.match(containerScript, /claimedPlan\?\.status === 'LOADING'/) - assert.ok(containerScript.indexOf("pushed?.status === 'EDITING'") < containerScript.indexOf("claimedPlan?.status === 'LOADING'")) +test('bridge loading starts through a real PDA batch before preload confirmation', () => { + const value = source('./phase3-loading.mjs') + assertOrder(value, [ + "pushedLoadingPlan?.status === 'EDITING'", + "claimedPlan?.status === 'LOADING'", + "const canonicalPick = await request", + "pickedPlan?.status === 'COMPLETED'", + "const confirming = await request", + ]) + assert.match(value, /confirmed\?\.mainStatus === 'LOADING' && confirmed\?\.preloadPlan\?\.status === 'CONFIRMING'/) + assert.match(value, /confirmed\?\.preloadPlan\?\.status === 'CONFIRMED'/) + assert.doesNotMatch(value, /关联柜次计划进入 LOADING/) +}) - assert.match(loadingScript, /pushedLoadingPlan\?\.status === 'EDITING'/) - assert.match(loadingScript, /claimedPlan\?\.status === 'LOADING'/) - assert.ok(loadingScript.indexOf("pushedLoadingPlan?.status === 'EDITING'") < loadingScript.indexOf("claimedPlan?.status === 'LOADING'")) - assert.ok(loadingScript.indexOf("claimedPlan?.status === 'LOADING'") < loadingScript.indexOf("const confirming =")) +test('downstream producers prefer the phase3Loading container fixture', () => { + const ocean = source('./ocean-bookings.mjs') + const customs = source('./phase3-customs.mjs') + const serviceCenter = source('./service-center-flow.mjs') + assert.match(ocean, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/) + assert.match(customs, /createdResources\?\.phase3Loading,\s*\n\s*createdResources\?\.phase3ContainerLoading/) + assert.match(serviceCenter, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/) + assert.match(ocean, /await action\('放舱', 'release'/) })