test: correct container loading acceptance order

This commit is contained in:
2026-08-14 21:45:54 +08:00
parent dbca4a1403
commit fdd011bc57
5 changed files with 75 additions and 16 deletions
@@ -374,7 +374,7 @@ export async function run(context) {
const transportResponse = await request(context, 'PATCH', `${CONTAINERS}/${containerId}/transport`, {
containerNo: `TST-${suffix}`.slice(0, 64), voyageNo: `V-${suffix}`.slice(0, 64),
version: Number(containerDetail?.version ?? 0), reason: context.marker,
version: Number(linkedContainer.version), reason: context.marker,
})
const transported = context.apiSuccess(MODULE, '在放舱前绑定本轮真实箱号', transportResponse, '', OPTIONS)
context.assert(MODULE, '绑定后柜次保留本轮箱号', transportResponse,
@@ -75,6 +75,7 @@ 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, /version: Number\(linkedContainer\.version\), reason: context\.marker/)
assert.match(containerLoadingAcceptance, /订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务/)
assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/)
assert.match(containerLoadingAcceptance, /订舱放舱并开放现场 PDA 任务/)
@@ -170,6 +170,7 @@ export async function run(context) {
id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo
&& id(linked?.oceanBookingId) === bookingId,
`containerBookingId=${containerBookingId}, cabinetNo=${linked?.cabinetNo || 'missing'}`, OPTIONS)
if (id(linked?.id) !== containerBookingId) return
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
@@ -233,29 +234,55 @@ export async function run(context) {
`linkId=${id(link?.id) || 'missing'}, preloadVersion=${link?.preloadVersion ?? 'missing'}, loadingVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (!link?.id) return
const missingContainerPushResponse = await request(context, 'POST',
`${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
{ expectedVersion: Number(link.loadingPlanVersion) },
{ 'Idempotency-Key': `${context.marker}-bridge-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 missingContainerTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const missingContainerTaskAbsent = failure(missingContainerTaskResponse)
|| !id(missingContainerTaskResponse?.body?.id || missingContainerTaskResponse?.body?.data?.id)
context.addStep(MODULE, '无真实柜号推送失败后不生成 PDA 任务', missingContainerTaskResponse,
missingContainerTaskAbsent,
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, task=${id(missingContainerTaskResponse?.body?.id || missingContainerTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS)
if (!missingContainerTaskAbsent) return
const containerBeforeTransportResponse = await context.request('GET',
`/api/platform/v1/containers/${containerBookingId}`)
const containerBeforeTransport = context.apiSuccess(MODULE, '读取补真实柜号前柜次最新版本',
containerBeforeTransportResponse, '', OPTIONS)
const transportVersion = Number(containerBeforeTransport?.version)
context.assert(MODULE, '补真实柜号使用订舱关联后的最新柜次版本', containerBeforeTransportResponse,
id(containerBeforeTransport?.id) === containerBookingId && Number.isInteger(transportVersion),
`containerBookingId=${id(containerBeforeTransport?.id) || 'missing'}, version=${containerBeforeTransport?.version ?? 'missing'}`, OPTIONS)
if (!Number.isInteger(transportVersion)) return
const transportResponse = await request(context, 'PATCH',
`/api/platform/v1/containers/${containerBookingId}/transport`, {
containerNo, voyageNo: `P3V-${suffix}`, version: transportVersion,
reason: `${context.marker}-bind-container-no`,
})
const transported = context.apiSuccess(MODULE, '绑定真实柜号后开放三期 PDA 推送门禁', transportResponse, '', OPTIONS)
context.assert(MODULE, '真实柜号绑定到稳定柜次且计划身份不变', transportResponse,
transported?.containerNo === containerNo && id(transported?.id) === containerBookingId
&& transported?.cabinetNo === linked.cabinetNo,
`containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}, loadingPlanId=${loadingPlanId}`, OPTIONS)
if (transported?.containerNo !== containerNo) 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, '绑定真实柜号后推送柜次计划并生成 PDA 任务', pushResponse, '', OPTIONS)
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 containerBeforeTransportResponse = await context.request('GET',
`/api/platform/v1/containers/${containerBookingId}`)
const containerBeforeTransport = context.apiSuccess(MODULE, '读取补真实柜号前柜次版本',
containerBeforeTransportResponse, '', OPTIONS)
const transportResponse = await request(context, 'PATCH',
`/api/platform/v1/containers/${containerBookingId}/transport`, {
containerNo, voyageNo: `P3V-${suffix}`, version: Number(containerBeforeTransport?.version),
reason: `${context.marker}-bind-container-no`,
})
const transported = context.apiSuccess(MODULE, '补真实柜号并开放三期 PDA 任务', transportResponse, '', OPTIONS)
context.assert(MODULE, '真实柜号绑定到稳定柜次且计划身份不变', transportResponse,
transported?.containerNo === containerNo && id(transported?.id) === containerBookingId,
`containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}`, 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) })
@@ -86,6 +86,11 @@ test('container loading follows push, claim, real pick, completed sequence', ()
test('bridge loading starts through a real PDA batch before preload confirmation', () => {
const value = source('./phase3-loading.mjs')
assertOrder(value, [
'原子关联入库预装与柜次装柜计划',
'无真实柜号拒绝推送柜次计划',
'无真实柜号推送失败后不生成 PDA 任务',
'绑定真实柜号后开放三期 PDA 推送门禁',
'绑定真实柜号后推送柜次计划并生成 PDA 任务',
"pushedLoadingPlan?.status === 'EDITING'",
"claimedPlan?.status === 'LOADING'",
"const canonicalPick = await request",
@@ -98,6 +103,10 @@ test('bridge loading starts through a real PDA batch before preload confirmation
assert.match(value, /confirmed\?\.mainStatus === 'LOADING'/)
assert.match(value, /confirmed\?\.preloadPlan\?\.status === 'CONFIRMING'/)
assert.match(value, /Number\(confirmed\?\.preloadPlan\?\.version\) === 2/)
assert.match(value, /const missingContainerPushRejected = failure\(missingContainerPushResponse\)[\s\S]*if \(!missingContainerPushRejected\) return/)
assert.match(value, /const missingContainerTaskAbsent =[\s\S]*if \(!missingContainerTaskAbsent\) return/)
assert.match(value, /version: transportVersion/)
assert.match(value, /Number\.isInteger\(transportVersion\)/)
})
test('PDA inventory snapshot gates batch and records the only safe mutation gap', () => {
@@ -149,6 +158,8 @@ test('container loading arranges a simulation before linking a confirmed real bo
const draft = value.slice(value.indexOf('const bookingDraft = {'), value.indexOf('context.assert(MODULE, \'订舱渠道与仓库快照完整\''))
assert.doesNotMatch(draft, /billOfLadingNo|vesselName|voyageNo|estimatedSailingAt|estimatedArrivalAt/)
assert.match(value, /containerAllocations\.length === 0/)
assert.match(value, /version: Number\(linkedContainer\.version\), reason: context\.marker/)
assert.doesNotMatch(value, /version: Number\(containerDetail\?\.version \?\? 0\), reason: context\.marker/)
assert.match(value, /function responseEvidence\(response\)[\s\S]*HTTP=\$\{response\?\.http \?\? 'missing'\}[\s\S]*code=\$\{codeOf\(response\)\}[\s\S]*requestId=\$\{response\?\.requestId \|\| 'missing'\}[\s\S]*traceId=\$\{response\?\.traceId \|\| 'missing'\}/)
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜首次关联真实订舱', emptyLink,\s+responseEvidence\(emptyLink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyUnlink = await request/)
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜允许解除关联', emptyUnlink,\s+responseEvidence\(emptyUnlink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyRelink = await request/)