diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 0d31e9a64..d8b7c2597 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -342,10 +342,65 @@ export async function run(context) { context.addStep(MODULE, '已有排货后拒绝解绑或换绑', unlinkAfterLoadingResponse, failure(unlinkAfterLoadingResponse), `HTTP=${unlinkAfterLoadingResponse.http}, code=${codeOf(unlinkAfterLoadingResponse)}`, OPTIONS) + const missingContainerPushResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`, + { expectedVersion: Number(assigned.version) }, + { 'Idempotency-Key': `${context.marker}-container-push-without-container-no`.slice(0, 128) }) + const missingContainerPushRejected = failure(missingContainerPushResponse) + context.addStep(MODULE, '无真实柜号拒绝推送预装计划', missingContainerPushResponse, + missingContainerPushRejected, + `HTTP=${missingContainerPushResponse.http}, code=${codeOf(missingContainerPushResponse)}, requestId=${missingContainerPushResponse.requestId || 'missing'}, traceId=${missingContainerPushResponse.traceId || 'missing'}`, OPTIONS) + if (!missingContainerPushRejected) return + const gatedTaskResponse = await context.request('GET', + `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`) + const gatedTaskAbsent = failure(gatedTaskResponse) + || !id(gatedTaskResponse?.body?.id || gatedTaskResponse?.body?.data?.id) + context.addStep(MODULE, '无真实柜号推送失败后不生成 PDA 任务', gatedTaskResponse, + gatedTaskAbsent, + `HTTP=${gatedTaskResponse.http}, code=${codeOf(gatedTaskResponse)}, task=${id(gatedTaskResponse?.body?.id || gatedTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS) + if (!gatedTaskAbsent) return + + const transportResponse = await request(context, 'PATCH', `${CONTAINERS}/${containerId}/transport`, { + containerNo: `TST-${suffix}`.slice(0, 64), voyageNo: `V-${suffix}`.slice(0, 64), + version: Number(linkedContainer.version), reason: context.marker, + }) + const transported = context.apiSuccess(MODULE, '在放舱前绑定本轮真实箱号', transportResponse, '', OPTIONS) + context.assert(MODULE, '绑定后柜次保留本轮箱号', transportResponse, + transported?.containerNo === `TST-${suffix}`.slice(0, 64) + && id(transported?.id) === containerId && transported?.cabinetNo === linkedContainer.cabinetNo, + `containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}, loadingPlanId=${planId}`, OPTIONS) + if (transported?.containerNo !== `TST-${suffix}`.slice(0, 64)) return + + const confirmedTaskResponse = await context.request('GET', + `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`) + const confirmedTaskAbsent = failure(confirmedTaskResponse) + || !id(confirmedTaskResponse?.body?.id || confirmedTaskResponse?.body?.data?.id) + context.addStep(MODULE, '订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务', confirmedTaskResponse, + confirmedTaskAbsent, + `HTTP=${confirmedTaskResponse.http}, code=${codeOf(confirmedTaskResponse)}, task=${id(confirmedTaskResponse?.body?.id || confirmedTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS) + if (!confirmedTaskAbsent) return + + const sailingAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() + const arrivalAt = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString() + const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { + expectedVersion: Number(confirmedBooking?.oceanBooking?.version), + billOfLadingNo: `BL-${suffix}`.slice(0, 128), vesselName: `VESSEL-${suffix}`.slice(0, 128), + voyageNo: `V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt, + estimatedArrivalAt: arrivalAt, reason: context.marker, + }, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) }) + const releasedBooking = context.apiSuccess(MODULE, '订舱五项运输字段放舱并满足 PDA 订舱门禁', releaseResponse, '', OPTIONS) + context.assert(MODULE, '放舱后订舱进入 RELEASED 且柜次分配保持稳定', releaseResponse, + releasedBooking?.oceanBooking?.status === 'RELEASED' + && releasedBooking?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerId), + `status=${releasedBooking?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}`, OPTIONS) + if (releasedBooking?.oceanBooking?.status !== 'RELEASED') return + + const releasedContainerResponse = await context.request('GET', `${CONTAINERS}/${containerId}`) + const releasedContainer = context.apiSuccess(MODULE, '放舱后回查货柜运输版本', releasedContainerResponse, '', OPTIONS) + const pushKey = `${context.marker}-container-push`.slice(0, 128) const pushResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`, { expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': pushKey }) - const pushed = context.apiSuccess(MODULE, '推送预装计划', pushResponse, '', OPTIONS) + const pushed = context.apiSuccess(MODULE, '柜号与 RELEASED 订舱就绪后推送预装计划', pushResponse, '', OPTIONS) context.assert(MODULE, '推送后计划保持 EDITING 且版本递增', pushResponse, pushed?.status === 'EDITING' && Number(pushed?.version) === Number(assigned.version) + 1, `status=${pushed?.status || 'missing'}, version=${pushed?.version ?? 'missing'}`, OPTIONS) @@ -359,12 +414,6 @@ export async function run(context) { && Number(pushReplay?.version) === Number(pushed.version), `planId=${id(pushReplay?.id) || 'missing'}, version=${pushReplay?.version ?? 'missing'}`, OPTIONS) - const gatedTaskResponse = await context.request('GET', - `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`) - context.addStep(MODULE, '无真实柜号时计划可排柜但 PDA 任务不开放', gatedTaskResponse, - failure(gatedTaskResponse) || !id(gatedTaskResponse?.body?.id || gatedTaskResponse?.body?.data?.id), - `HTTP=${gatedTaskResponse.http}, code=${codeOf(gatedTaskResponse)}, task=${id(gatedTaskResponse?.body?.id || gatedTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS) - 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) @@ -372,38 +421,6 @@ export async function run(context) { records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'EDITING'), `records=${records(preloadPage).length}`, OPTIONS) - const transportResponse = await request(context, 'PATCH', `${CONTAINERS}/${containerId}/transport`, { - containerNo: `TST-${suffix}`.slice(0, 64), voyageNo: `V-${suffix}`.slice(0, 64), - version: Number(linkedContainer.version), reason: context.marker, - }) - const transported = context.apiSuccess(MODULE, '在放舱前绑定本轮真实箱号', transportResponse, '', OPTIONS) - context.assert(MODULE, '绑定后柜次保留本轮箱号', transportResponse, - transported?.containerNo === `TST-${suffix}`.slice(0, 64), - `containerNo=${transported?.containerNo || 'missing'}`, OPTIONS) - - const confirmedTaskResponse = await context.request('GET', - `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`) - context.addStep(MODULE, '订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务', confirmedTaskResponse, - failure(confirmedTaskResponse) || !id(confirmedTaskResponse?.body?.id || confirmedTaskResponse?.body?.data?.id), - `HTTP=${confirmedTaskResponse.http}, code=${codeOf(confirmedTaskResponse)}, task=${id(confirmedTaskResponse?.body?.id || confirmedTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS) - - const sailingAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() - const arrivalAt = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString() - const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { - expectedVersion: Number(confirmedBooking?.oceanBooking?.version), - billOfLadingNo: `BL-${suffix}`.slice(0, 128), vesselName: `VESSEL-${suffix}`.slice(0, 128), - voyageNo: `V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt, - estimatedArrivalAt: arrivalAt, reason: context.marker, - }, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) }) - const releasedBooking = context.apiSuccess(MODULE, '订舱放舱并开放现场 PDA 任务', releaseResponse, '', OPTIONS) - context.assert(MODULE, '放舱后订舱进入 RELEASED 且柜次分配保持稳定', releaseResponse, - releasedBooking?.oceanBooking?.status === 'RELEASED' - && releasedBooking?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerId), - `status=${releasedBooking?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}`, OPTIONS) - - const releasedContainerResponse = await context.request('GET', `${CONTAINERS}/${containerId}`) - const releasedContainer = context.apiSuccess(MODULE, '放舱后回查货柜运输版本', releasedContainerResponse, '', OPTIONS) - const taskResponse = await context.request('GET', `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const task = context.apiSuccess(MODULE, '查询本轮柜次 PDA 拣货任务', taskResponse, '', OPTIONS) diff --git a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs index fd3e3d640..ba932a097 100644 --- a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs @@ -74,11 +74,13 @@ test('existing container number replacement keeps container plan and PDA identit }) test('PDA task opens only after preload push, real container number and ocean release converge', () => { - assert.match(containerLoadingAcceptance, /无真实柜号时计划可排柜但 PDA 任务不开放/) + assert.match(containerLoadingAcceptance, /无真实柜号拒绝推送预装计划/) + assert.match(containerLoadingAcceptance, /无真实柜号推送失败后不生成 PDA 任务/) assert.match(containerLoadingAcceptance, /version: Number\(linkedContainer\.version\), reason: context\.marker/) assert.match(containerLoadingAcceptance, /订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务/) assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/) - assert.match(containerLoadingAcceptance, /订舱放舱并开放现场 PDA 任务/) + assert.match(containerLoadingAcceptance, /订舱五项运输字段放舱并满足 PDA 订舱门禁/) + assert.match(containerLoadingAcceptance, /柜号与 RELEASED 订舱就绪后推送预装计划/) assert.match(containerLoadingAcceptance, /releasedBooking\?\.oceanBooking\?\.status === 'RELEASED'/) }) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index a73403919..75a2638f5 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -274,10 +274,34 @@ export async function run(context) { `containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}, loadingPlanId=${loadingPlanId}`, OPTIONS) if (transported?.containerNo !== containerNo) return + const confirmedBookingTaskResponse = await context.request('GET', + `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) + const confirmedBookingTaskAbsent = failure(confirmedBookingTaskResponse) + || !id(confirmedBookingTaskResponse?.body?.id || confirmedBookingTaskResponse?.body?.data?.id) + context.addStep(MODULE, '订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务', confirmedBookingTaskResponse, + confirmedBookingTaskAbsent, + `HTTP=${confirmedBookingTaskResponse.http}, code=${codeOf(confirmedBookingTaskResponse)}, task=${id(confirmedBookingTaskResponse?.body?.id || confirmedBookingTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS) + if (!confirmedBookingTaskAbsent) return + + const sailingAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() + const arrivalAt = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString() + const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { + expectedVersion: Number(bookingConfirmed?.oceanBooking?.version), + billOfLadingNo: `P3BL-${suffix}`.slice(0, 128), vesselName: `P3VESSEL-${suffix}`.slice(0, 128), + voyageNo: `P3V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt, + estimatedArrivalAt: arrivalAt, reason: context.marker, + }, { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) }) + const bookingReleased = context.apiSuccess(MODULE, '三期订舱补齐五项运输字段并放舱', releaseResponse, '', OPTIONS) + context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份', releaseResponse, + bookingReleased?.oceanBooking?.status === 'RELEASED' + && bookingReleased?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerBookingId), + `status=${bookingReleased?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS) + if (bookingReleased?.oceanBooking?.status !== 'RELEASED') return + const pushResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`, { expectedVersion: Number(link.loadingPlanVersion) }, { 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }) - const pushedLoadingPlan = context.apiSuccess(MODULE, '绑定真实柜号后推送柜次计划并生成 PDA 任务', pushResponse, '', OPTIONS) + const pushedLoadingPlan = context.apiSuccess(MODULE, '真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务', pushResponse, '', OPTIONS) context.assert(MODULE, '推送后柜次计划保持 EDITING 且版本递增', pushResponse, pushedLoadingPlan?.status === 'EDITING' && Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1, 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 aa2b893a7..699c1e32a 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 @@ -90,7 +90,10 @@ test('bridge loading starts through a real PDA batch before preload confirmation '无真实柜号拒绝推送柜次计划', '无真实柜号推送失败后不生成 PDA 任务', '绑定真实柜号后开放三期 PDA 推送门禁', - '绑定真实柜号后推送柜次计划并生成 PDA 任务', + '订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务', + '三期订舱补齐五项运输字段并放舱', + '三期订舱 RELEASED 后保持模拟柜关联身份', + '真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务', "pushedLoadingPlan?.status === 'EDITING'", "claimedPlan?.status === 'LOADING'", "const canonicalPick = await request", @@ -107,6 +110,8 @@ test('bridge loading starts through a real PDA batch before preload confirmation assert.match(value, /const missingContainerTaskAbsent =[\s\S]*if \(!missingContainerTaskAbsent\) return/) assert.match(value, /version: transportVersion/) assert.match(value, /Number\.isInteger\(transportVersion\)/) + assert.match(value, /billOfLadingNo:[\s\S]*vesselName:[\s\S]*voyageNo:[\s\S]*estimatedSailingAt:[\s\S]*estimatedArrivalAt:/) + assert.match(value, /bookingReleased\?\.oceanBooking\?\.status === 'RELEASED'/) }) test('PDA inventory snapshot gates batch and records the only safe mutation gap', () => { @@ -178,7 +183,10 @@ test('container loading arranges a simulation before linking a confirmed real bo assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/) assert.ok(value.includes("'PATCH', `${CONTAINERS}/${containerId}/transport")) assert.match(value, /在放舱前绑定本轮真实箱号/) - assert.match(value, /订舱放舱并开放现场 PDA 任务/) + assert.match(value, /订舱五项运输字段放舱并满足 PDA 订舱门禁/) + assert.match(value, /无真实柜号拒绝推送预装计划/) + assert.match(value, /柜号与 RELEASED 订舱就绪后推送预装计划/) + assert.match(value, /const confirmedTaskAbsent =[\s\S]*if \(!confirmedTaskAbsent\) return/) assert.match(value, /POST', CONTAINERS/) }) @@ -251,10 +259,12 @@ test('container loading enforces channel isolation and stable PDA identity acros '两个真实有效渠道之间拒绝跨渠道装柜', '人工分配本轮候选货物', '首次关联真实已确认订舱柜', - '无真实柜号时计划可排柜但 PDA 任务不开放', + '无真实柜号拒绝推送预装计划', + '无真实柜号推送失败后不生成 PDA 任务', '在放舱前绑定本轮真实箱号', '订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务', - '订舱放舱并开放现场 PDA 任务', + '订舱五项运输字段放舱并满足 PDA 订舱门禁', + '柜号与 RELEASED 订舱就绪后推送预装计划', '换柜号前读取 PDA 稳定库存快照', 'PDA 任务创建后替换真实柜号', 'PDA 任务创建后清空真实柜号失败', diff --git a/scripts/full-regression-entry-contract.test.mjs b/scripts/full-regression-entry-contract.test.mjs index 766665232..6a7a5f624 100644 --- a/scripts/full-regression-entry-contract.test.mjs +++ b/scripts/full-regression-entry-contract.test.mjs @@ -73,11 +73,22 @@ test('test-environment loading chains bind the latest container version before o const bridgeIndex = bridgeLoading.indexOf('原子关联入库预装与柜次装柜计划') const rejectedPushIndex = bridgeLoading.indexOf('无真实柜号拒绝推送柜次计划') const transportIndex = bridgeLoading.indexOf('绑定真实柜号后开放三期 PDA 推送门禁') - const successfulPushIndex = bridgeLoading.indexOf('绑定真实柜号后推送柜次计划并生成 PDA 任务') + const releaseIndex = bridgeLoading.indexOf('三期订舱补齐五项运输字段并放舱') + const successfulPushIndex = bridgeLoading.indexOf('真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务') const pdaIndex = bridgeLoading.indexOf('读取柜次级 PDA 唯一事实任务') assert.ok(bridgeIndex >= 0 && rejectedPushIndex > bridgeIndex && transportIndex > rejectedPushIndex - && successfulPushIndex > transportIndex && pdaIndex > successfulPushIndex) + && releaseIndex > transportIndex && successfulPushIndex > releaseIndex && pdaIndex > successfulPushIndex) assert.match(bridgeLoading, /const missingContainerPushRejected = failure\(missingContainerPushResponse\)[\s\S]*if \(!missingContainerPushRejected\) return/) assert.match(bridgeLoading, /const missingContainerTaskAbsent =[\s\S]*if \(!missingContainerTaskAbsent\) return/) assert.match(bridgeLoading, /version: transportVersion/) + assert.match(bridgeLoading, /bookingReleased\?\.oceanBooking\?\.status === 'RELEASED'/) + + const containerRejectedPushIndex = containerLoading.indexOf('无真实柜号拒绝推送预装计划') + const containerTransportIndex = containerLoading.indexOf('在放舱前绑定本轮真实箱号') + const containerReleaseIndex = containerLoading.indexOf('订舱五项运输字段放舱并满足 PDA 订舱门禁') + const containerSuccessfulPushIndex = containerLoading.indexOf('柜号与 RELEASED 订舱就绪后推送预装计划') + const containerPdaIndex = containerLoading.indexOf('查询本轮柜次 PDA 拣货任务') + assert.ok(containerRejectedPushIndex >= 0 && containerTransportIndex > containerRejectedPushIndex + && containerReleaseIndex > containerTransportIndex && containerSuccessfulPushIndex > containerReleaseIndex + && containerPdaIndex > containerSuccessfulPushIndex) })