fix: restore phase five development acceptance chain

This commit is contained in:
2026-09-01 21:55:27 +08:00
parent 04e0111756
commit ad5e0812bf
35 changed files with 623 additions and 313 deletions
+21 -106
View File
@@ -1,5 +1,3 @@
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { createHash } from 'node:crypto'
@@ -7,7 +5,7 @@ const MODULE = 'phase3-loading'
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调订舱代理'
const LATE_CONTAINER_GATE_ONLY = process.env.LATE_CONTAINER_GATE_ONLY === '1'
function id(value) { return value == null ? '' : String(value) }
@@ -168,7 +166,7 @@ export async function run(context) {
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
const supplierResponse = await context.request('GET',
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&page=1&size=200')
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=200')
const supplierPage = context.apiSuccess(MODULE, '读取订舱代理与船司供应商事实源', supplierResponse, '', OPTIONS)
const supplierRecords = Array.isArray(supplierPage?.records) ? supplierPage.records : []
const supplier = await ensureMatsonSupplier(context, supplierRecords)
@@ -223,10 +221,9 @@ export async function run(context) {
if (!candidate?.inboundDetailId || !warehouseId) return
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
bookingId: containerBookingId, remark: context.marker,
})
const loadingPlan = context.apiSuccess(MODULE, '创建柜次级智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanResponse = await context.request('GET',
`${smartLoadingBase}/plans/by-booking/${encodeURIComponent(containerBookingId)}`)
const loadingPlan = context.apiSuccess(MODULE, '读取柜次创建时原子生成的智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanId = id(loadingPlan?.id)
if (!loadingPlanId) return
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
@@ -310,119 +307,37 @@ export async function run(context) {
const pendingPreloadResponse = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
const pendingPreload = context.apiSuccess(MODULE, '智能排货自动建立 PENDING 预装聚合', pendingPreloadResponse, '', OPTIONS)
const planId = id(pendingPreload?.id)
context.assert(MODULE, '推送前入库单保持 WAIT_SPLIT 且预装聚合为 PENDING', pendingPreloadResponse,
context.assert(MODULE, '分配后入库单保持 WAIT_SPLIT 且预装聚合为 PENDING', pendingPreloadResponse,
Boolean(planId) && pendingPreload?.status === 'PENDING' && Number.isInteger(Number(pendingPreload?.version)),
`preloadPlanId=${planId || 'missing'}, status=${pendingPreload?.status || 'missing'}`, OPTIONS)
if (!planId) return
const pushRequest = Object.freeze({
path: `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
body: Object.freeze({ expectedVersion: Number(persistedRemarkPlan.version) }),
headers: Object.freeze({ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }),
})
const pushResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
const pushedLoadingPlan = context.apiSuccess(MODULE, '无真实柜号推送预装成功并冻结柜次明细', pushResponse, '', OPTIONS)
context.assert(MODULE, '无号推送保持 EDITING、身份稳定且版本递增', pushResponse,
id(pushedLoadingPlan?.id) === loadingPlanId && pushedLoadingPlan?.status === 'EDITING'
&& pushedLoadingPlan?.containerNo == null
&& Number(pushedLoadingPlan?.version) > Number(assignedLoadingPlan.version),
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
if (pushedLoadingPlan?.status !== 'EDITING') return
const pushedDetailResponse = await context.request('GET',
const synchronizedDetailResponse = await context.request('GET',
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS)
const pushedHistories = visibleLoadingHistories(pushedDetail?.histories)
context.assert(MODULE, '首次 push 只保留一条 PUSH_PRELOAD 且无纯状态 no-op', pushedDetailResponse,
pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1
&& !(pushedDetail?.histories || []).some(isPureStatusNoop),
`raw=${pushedDetail?.histories?.length ?? 'invalid'}, effective=${pushedHistories.length}, push=${pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
OPTIONS)
const pushedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const pushedLinks = context.apiSuccess(MODULE, '无号推送自动建立 bridge 关联', pushedLinksResponse, '', OPTIONS)
const link = Array.isArray(pushedLinks) ? pushedLinks.find((item) => id(item.inboundId) === inboundId) : null
context.assert(MODULE, 'push 自动建立唯一预装关联且分配身份稳定', pushedLinksResponse,
const synchronizedDetail = context.apiSuccess(MODULE, '分配后预装清单正式可见', synchronizedDetailResponse, '', OPTIONS)
const synchronizedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const synchronizedLinks = context.apiSuccess(MODULE, '分配同步建立 bridge 关联', synchronizedLinksResponse, '', OPTIONS)
const link = Array.isArray(synchronizedLinks) ? synchronizedLinks.find((item) => id(item.inboundId) === inboundId) : null
context.assert(MODULE, '分配自动建立唯一预装关联且身份稳定', synchronizedLinksResponse,
Boolean(link?.id) && id(link?.loadingPlanId) === loadingPlanId && id(link?.preloadPlanId) === planId
&& Number(link?.allocatedPackageCount) === 1,
`linkId=${id(link?.id) || 'missing'}, preloadPlanId=${id(link?.preloadPlanId) || 'missing'}`, OPTIONS)
if (!link?.id) return
const postPushPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const postPushPlan = context.apiSuccess(MODULE, '首次 push 后读取事务完成态柜次计划', postPushPlanResponse, '', OPTIONS)
context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', postPushPlanResponse,
postPushPlan?.status === 'EDITING' && Number(postPushPlan?.version) === Number(link?.loadingPlanVersion)
&& Number(postPushPlan?.version) > Number(assignedLoadingPlan.version),
`pushResponseVersion=${pushedLoadingPlan?.version ?? 'missing'}, persistedVersion=${postPushPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (postPushPlan?.status !== 'EDITING' || !Number.isInteger(Number(postPushPlan?.version))) return
const synchronizedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const synchronizedPlan = context.apiSuccess(MODULE, '读取分配事务完成态柜次计划', synchronizedPlanResponse, '', OPTIONS)
context.assert(MODULE, '分配事务完成态计划与 bridge 一致且保持 EDITING', synchronizedPlanResponse,
synchronizedPlan?.status === 'EDITING' && Number(synchronizedPlan?.version) === Number(link?.loadingPlanVersion)
&& Number(synchronizedPlan?.version) === Number(persistedRemarkPlan.version),
`persistedVersion=${synchronizedPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (synchronizedPlan?.status !== 'EDITING' || !Number.isInteger(Number(synchronizedPlan?.version))) return
const missingContainerTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const missingContainerTaskAbsent = missingContainerTaskResponse.http === 200 && codeOf(missingContainerTaskResponse) === 'A0404'
context.addStep(MODULE, '无号 push 成功后 PDA 查询明确 A0404', missingContainerTaskResponse,
context.addStep(MODULE, '无真实柜号时 PDA 查询明确 A0404', missingContainerTaskResponse,
missingContainerTaskAbsent,
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, requestId=${missingContainerTaskResponse.requestId || 'missing'}, traceId=${missingContainerTaskResponse.traceId || 'missing'}`, OPTIONS)
if (!missingContainerTaskAbsent) return
const replayBaseline = projectPushPreloadStability({
plan: postPushPlan, detail: pushedDetail, links: pushedLinks,
task: { http: missingContainerTaskResponse.http, code: codeOf(missingContainerTaskResponse) },
})
const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
context.apiSuccess(MODULE, 'push exact replay 返回首次结果', pushReplayResponse, '', OPTIONS)
const replayPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const replayPlan = context.apiSuccess(MODULE, '重放后回查柜次计划', replayPlanResponse, '', OPTIONS)
const replayDetailResponse = await context.request('GET',
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const replayDetail = context.apiSuccess(MODULE, '重放后回查预装历史', replayDetailResponse, '', OPTIONS)
const replayHistories = visibleLoadingHistories(replayDetail?.histories)
context.assert(MODULE, 'push 同幂等请求重放历史零新增', replayDetailResponse,
replayHistories.length === pushedHistories.length
&& replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
`before=${pushedHistories.length}, after=${replayHistories.length}, push=${replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
OPTIONS)
const replayLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
const replayTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const replaySnapshot = projectPushPreloadStability({
plan: replayPlan, detail: replayDetail, links: replayLinks,
task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) },
})
const replayDiff = diffPushPreloadStability(replayBaseline, replaySnapshot)
context.assert(MODULE, 'push exact replay 对计划版本历史关联和任务零漂移', pushReplayResponse,
replayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
`changed=${replayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
{ expectedVersion: Number(postPushPlan.version) }, pushRequest.headers)
context.addStep(MODULE, 'push 同 key 异参返回冲突', sameKeyDifferentBody,
failure(sameKeyDifferentBody), `HTTP=${sameKeyDifferentBody.http}, code=${codeOf(sameKeyDifferentBody)}`, OPTIONS)
const newKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body,
{ 'Idempotency-Key': `${context.marker}-bridge-push-duplicate`.slice(0, 128) })
context.addStep(MODULE, '已推送计划使用新 key 重复 push 返回冲突', newKeyDuplicate,
failure(newKeyDuplicate), `HTTP=${newKeyDuplicate.http}, code=${codeOf(newKeyDuplicate)}`, OPTIONS)
const rejectedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const rejectedPlan = context.apiSuccess(MODULE, '非法 push 后回查柜次计划', rejectedPlanResponse, '', OPTIONS)
const rejectedDetailResponse = await context.request('GET',
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const rejectedDetail = context.apiSuccess(MODULE, '非法 push 后回查预装历史', rejectedDetailResponse, '', OPTIONS)
const rejectedHistories = visibleLoadingHistories(rejectedDetail?.histories)
const rejectedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS)
const rejectedTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const rejectedSnapshot = projectPushPreloadStability({
plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks,
task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) },
})
const rejectedDiff = diffPushPreloadStability(replayBaseline, rejectedSnapshot)
context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse,
failure(sameKeyDifferentBody) && failure(newKeyDuplicate)
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404'
&& rejectedHistories.length === pushedHistories.length
&& rejectedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
const unlinkedTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
context.addStep(MODULE, '无号且未关联订舱仍无 PDA 任务', unlinkedTaskResponse,
@@ -695,7 +610,7 @@ export async function run(context) {
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, items=${taskItemsSignature(unchangedAfterInvalid)}, operations=${afterInvalidOperations?.length ?? 'missing'}`, OPTIONS)
const merchantLogin = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantToken = merchantLogin.http === 200 && codeOf(merchantLogin) === '200'
? merchantLogin.body?.data?.accessToken : null