This commit is contained in:
2026-08-25 09:52:49 +08:00
parent b211121365
commit b67217332a
37 changed files with 1009 additions and 343 deletions
@@ -1029,13 +1029,111 @@ export async function run(context) {
const dynamicCandidatesResponse = await context.request('GET',
`${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const dynamicCandidates = context.apiSuccess(MODULE, '查询同柜动态增量候选', dynamicCandidatesResponse, '', OPTIONS)
const dynamicCandidate = Array.isArray(dynamicCandidates) ? dynamicCandidates.find((item) =>
id(item?.inboundId) === incrementalInboundId && Number(item?.availablePackageCount) > 0) : null
const markerOwnedDynamicCandidates = Array.isArray(dynamicCandidates) ? dynamicCandidates.filter((item) =>
id(item?.inboundId) === incrementalInboundId && Number(item?.availablePackageCount) > 0) : []
const dynamicCandidate = markerOwnedDynamicCandidates[0]
const skippedExceptionCandidate = markerOwnedDynamicCandidates[1]
const partialExceptionCandidate = markerOwnedDynamicCandidates.find((item, index) =>
index > 1 && Number(item?.availablePackageCount) >= 2)
context.assert(MODULE, '动态增量候选保持客户、仓库和渠道隔离', dynamicCandidatesResponse,
Boolean(dynamicCandidate?.inboundDetailId) && id(dynamicCandidate?.accountId) === tenantId
&& id(dynamicCandidate?.warehouseId) === warehouseId
&& dynamicCandidate?.channelCode === initial?.summary?.channelCode,
`inboundId=${incrementalInboundId}, detailId=${id(dynamicCandidate?.inboundDetailId) || 'missing'}, available=${dynamicCandidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
context.assert(MODULE, '本轮增量 fixture 提供独立 SKIP 与 PARTIAL 合法货物明细', dynamicCandidatesResponse,
Boolean(skippedExceptionCandidate?.inboundDetailId) && Boolean(partialExceptionCandidate?.inboundDetailId)
&& id(skippedExceptionCandidate?.inboundDetailId) !== id(partialExceptionCandidate?.inboundDetailId)
&& id(dynamicCandidate?.inboundDetailId) !== id(skippedExceptionCandidate?.inboundDetailId)
&& id(dynamicCandidate?.inboundDetailId) !== id(partialExceptionCandidate?.inboundDetailId),
`candidateCount=${markerOwnedDynamicCandidates.length}, skipDetailId=${id(skippedExceptionCandidate?.inboundDetailId) || 'missing'}, partialDetailId=${id(partialExceptionCandidate?.inboundDetailId) || 'missing'}`, OPTIONS)
if (skippedExceptionCandidate?.inboundDetailId && partialExceptionCandidate?.inboundDetailId) {
const beforeSkipPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const beforeSkipPlan = context.apiSuccess(MODULE, 'SKIP 异常场景前读取计划即时版本', beforeSkipPlanResponse, '', OPTIONS)
const skipAssignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
inboundId: incrementalInboundId, inboundDetailId: id(skippedExceptionCandidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(beforeSkipPlan?.version), operationId: `${context.marker}-exception-skip-assign`.slice(0, 128),
})
const skipAssignedPlan = context.apiSuccess(MODULE, '加入本轮待跳拣异常明细', skipAssignmentResponse, '', OPTIONS)
const skipPlanItem = skipAssignedPlan?.items?.find?.((item) =>
id(item?.inboundDetailId) === id(skippedExceptionCandidate.inboundDetailId))
const skipTaskResponse = await context.request('GET', taskDetailPath)
const skipTask = context.apiSuccess(MODULE, '读取待跳拣异常 PDA 明细', skipTaskResponse, '', OPTIONS)
const skipTaskItem = skipTask?.items?.find?.((item) => id(item?.loadingPlanItemId) === id(skipPlanItem?.id))
const skipInventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const skipInventory = context.apiSuccess(MODULE, '跳拣前刷新实时库存快照', skipInventoryResponse, '', OPTIONS)
const skipReason = `${context.marker}-箱唛破损待复核`
const skipBatchResponse = await request(context, 'POST', batchPath, {
warehouseId, loadingPlanId, containerBookingId,
expectedVersion: Number(skipTask?.version), inventorySnapshotToken: skipInventory?.snapshotToken,
items: [{ itemId: id(skipTaskItem?.id), action: 'SKIP', version: Number(skipTaskItem?.version),
reason: skipReason }],
}, { 'Idempotency-Key': `${context.marker}-exception-skip-batch`.slice(0, 128) })
const skippedTask = context.apiSuccess(MODULE, 'SKIP 携带异常原因并原子解决待拣明细', skipBatchResponse, '', OPTIONS)
const skippedTaskItem = skippedTask?.items?.find?.((item) => id(item?.id) === id(skipTaskItem?.id))
context.assert(MODULE, '合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW', skipBatchResponse,
Boolean(skipReason.trim()) && skippedTaskItem?.itemStatus === 'SKIPPED'
&& Number(skippedTaskItem?.pickedPackageCount) === 0
&& skippedTask?.workflowStatus === 'AWAITING_REVIEW' && skippedTask?.taskStatus === 'LOADING',
`itemStatus=${skippedTaskItem?.itemStatus || 'missing'}, picked=${skippedTaskItem?.pickedPackageCount ?? 'missing'}, workflow=${skippedTask?.workflowStatus || 'missing'}`, OPTIONS)
const beforePartialPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const beforePartialPlan = context.apiSuccess(MODULE, 'PARTIAL 异常场景前读取计划即时版本', beforePartialPlanResponse, '', OPTIONS)
const partialAssignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
inboundId: incrementalInboundId, inboundDetailId: id(partialExceptionCandidate.inboundDetailId), packageCount: 2,
expectedVersion: Number(beforePartialPlan?.version), operationId: `${context.marker}-exception-partial-assign`.slice(0, 128),
})
const partialAssignedPlan = context.apiSuccess(MODULE, '加入本轮部分拣货异常明细', partialAssignmentResponse, '', OPTIONS)
const partialPlanItem = partialAssignedPlan?.items?.find?.((item) =>
id(item?.inboundDetailId) === id(partialExceptionCandidate.inboundDetailId))
const unresolvedTaskResponse = await context.request('GET', taskDetailPath)
const unresolvedTask = context.apiSuccess(MODULE, '读取存在未解决异常的 PDA 任务', unresolvedTaskResponse, '', OPTIONS)
const unresolvedTaskItem = unresolvedTask?.items?.find?.((item) =>
id(item?.loadingPlanItemId) === id(partialPlanItem?.id))
const operationsBeforeUnresolvedCompleteResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/operations`)
const operationsBeforeUnresolvedComplete = context.apiSuccess(MODULE,
'未解决异常完成前读取 PDA 操作审计基线', operationsBeforeUnresolvedCompleteResponse, '', OPTIONS)
const unresolvedComplete = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`, {
expectedTaskVersion: Number(unresolvedTask?.version),
operationId: `${context.marker}-complete-with-unresolved`.slice(0, 128),
remark: `${context.marker}-must-reject-unresolved`,
}, { 'Idempotency-Key': `${context.marker}-complete-with-unresolved-key`.slice(0, 128) })
context.addStep(MODULE, '存在未解决异常明细时禁止确认装柜完成', unresolvedComplete,
unresolvedComplete.http === 200 && codeOf(unresolvedComplete) === 'A0409'
&& String(unresolvedComplete.body?.message || '').includes('尚未处理'),
responseEvidence(unresolvedComplete), OPTIONS)
const afterUnresolvedCompleteTaskResponse = await context.request('GET', taskDetailPath)
const afterUnresolvedCompleteTask = context.apiSuccess(MODULE,
'未解决异常完成失败后回查 PDA 任务', afterUnresolvedCompleteTaskResponse, '', OPTIONS)
const operationsAfterUnresolvedCompleteResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/operations`)
const operationsAfterUnresolvedComplete = context.apiSuccess(MODULE,
'未解决异常完成失败后回查 PDA 操作审计', operationsAfterUnresolvedCompleteResponse, '', OPTIONS)
context.assert(MODULE, '未解决异常完成失败后 task/items/operation 全部零写入', afterUnresolvedCompleteTaskResponse,
Number(afterUnresolvedCompleteTask?.version) === Number(unresolvedTask?.version)
&& taskItemsSignature(afterUnresolvedCompleteTask) === taskItemsSignature(unresolvedTask)
&& operationsSignature(operationsAfterUnresolvedComplete) === operationsSignature(operationsBeforeUnresolvedComplete),
`taskVersion=${afterUnresolvedCompleteTask?.version ?? 'missing'}, operations=${operationsAfterUnresolvedComplete?.length ?? 'missing'}`, OPTIONS)
const partialInventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const partialInventory = context.apiSuccess(MODULE, '部分拣货前刷新实时库存快照', partialInventoryResponse, '', OPTIONS)
const partialBatchResponse = await request(context, 'POST', batchPath, {
warehouseId, loadingPlanId, containerBookingId,
expectedVersion: Number(afterUnresolvedCompleteTask?.version),
inventorySnapshotToken: partialInventory?.snapshotToken,
items: [{ itemId: id(unresolvedTaskItem?.id), action: 'PARTIAL', finalPickedPackageCount: 1,
releaseMode: 'MANUAL', locationIds: [], version: Number(unresolvedTaskItem?.version) }],
}, { 'Idempotency-Key': `${context.marker}-exception-partial-batch`.slice(0, 128) })
const partialTask = context.apiSuccess(MODULE, '合法 PARTIAL 解决未完成明细', partialBatchResponse, '', OPTIONS)
const partialTaskItem = partialTask?.items?.find?.((item) => id(item?.id) === id(unresolvedTaskItem?.id))
context.assert(MODULE, '部分拣货数量守恒并恢复最终可确认状态', partialBatchResponse,
partialTaskItem?.itemStatus === 'PARTIAL' && Number(partialTaskItem?.plannedPackageCount) === 2
&& Number(partialTaskItem?.pickedPackageCount) === 1
&& partialTask?.workflowStatus === 'AWAITING_REVIEW' && partialTask?.taskStatus === 'LOADING',
`itemStatus=${partialTaskItem?.itemStatus || 'missing'}, planned=${partialTaskItem?.plannedPackageCount ?? 'missing'}, picked=${partialTaskItem?.pickedPackageCount ?? 'missing'}, workflow=${partialTask?.workflowStatus || 'missing'}`, OPTIONS)
}
if (dynamicCandidate?.inboundDetailId) {
const planBeforeIncrementResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const planBeforeIncrement = context.apiSuccess(MODULE, '读取 LOADING 动态增量前计划', planBeforeIncrementResponse, '', OPTIONS)
@@ -458,6 +458,32 @@ test('phase3 loading proves stale inventory zero-write and defers final completi
assert.doesNotMatch(loading, /complete-before-downstream/)
})
test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstream completion', () => {
const loading = source('./phase3-loading.mjs')
assertOrder(loading, [
'本轮增量 fixture 提供独立 SKIP 与 PARTIAL 合法货物明细',
'加入本轮待跳拣异常明细',
'SKIP 携带异常原因并原子解决待拣明细',
'合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW',
'加入本轮部分拣货异常明细',
'未解决异常完成前读取 PDA 操作审计基线',
'存在未解决异常明细时禁止确认装柜完成',
'未解决异常完成失败后 task/items/operation 全部零写入',
'合法 PARTIAL 解决未完成明细',
'部分拣货数量守恒并恢复最终可确认状态',
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
])
assert.match(loading, /action: 'SKIP', version: Number\(skipTaskItem\?\.version\),\s*reason: skipReason/)
assert.match(loading, /skipReason = `\$\{context\.marker\}-箱唛破损待复核`/)
assert.match(loading, /itemStatus === 'SKIPPED'[\s\S]*pickedPackageCount\) === 0/)
assert.match(loading, /action: 'PARTIAL', finalPickedPackageCount: 1/)
assert.match(loading, /plannedPackageCount\) === 2[\s\S]*pickedPackageCount\) === 1/)
assert.match(loading, /String\(unresolvedComplete\.body\?\.message \|\| ''\)\.includes\('尚未处理'\)/)
assert.match(loading, /taskItemsSignature\(afterUnresolvedCompleteTask\) === taskItemsSignature\(unresolvedTask\)/)
assert.match(loading, /operationsSignature\(operationsAfterUnresolvedComplete\) === operationsSignature\(operationsBeforeUnresolvedComplete\)/)
assert.doesNotMatch(loading, /addCoverageGap\([^)]*(SKIP|PARTIAL|未解决异常)/)
})
test('phase3 loading fixture forces A0 through reversible public location status APIs', () => {
const entry = source('../admin-test-env-acceptance.mjs')
const measurement = source('./inbound-measurement.mjs')