test(firstleg): harden coload capacity acceptance

This commit is contained in:
2026-08-30 16:07:12 +08:00
parent c9ee319e9d
commit 5c83b81e8a
2 changed files with 61 additions and 21 deletions
@@ -21,11 +21,11 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
'组成员不能单独调整箱数',
'组成员不能单独删除',
'整组原子移除',
'目标同柜组整体超容时智能推荐跳过整组',
'整组容量不足时智能推荐全不取且不产生部分成员',
'容量竞争时智能推荐不因整组容量失败',
'容量竞争时同柜组只能整组全取或全不取',
'推荐候选仅含当前目标同柜组且成员身份精确',
'ACTIVE同柜组整组取消成功',
'同柜组PDA原子批次记录部分拣货异常',
'同柜组PDA原子批次记录短拣异常',
'同柜组任一成员短拣时完成被拒绝',
'短拣完成拒绝后任务、父子树和组锁零漂移',
'同柜组足量完成命令可靠受理',
@@ -35,6 +35,8 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
'拆分余量子单仍可与同仓同渠道订单创建同柜组',
'拆分身份不影响当前WAIT_SPLIT片段完整纳组',
'拆分余量建组不改写既有父子数量事实',
'为Admin页面保留ACTIVE未分配跨目的仓组',
'页面验收保留组仍为ACTIVE且未锁定',
]) assert.match(source, new RegExp(fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
assert.doesNotMatch(source, /跨仓库渠道或目的仓港创建整组被拒绝/)
@@ -516,8 +516,7 @@ export async function run(context) {
const groupedCandidates = candidates.filter((item) => id(item?.coLoadGroupId) === groupId)
context.assert(MODULE, '每个组成员候选携带相同整组事实', candidateResponse,
groupedCandidates.length === 2 && groupedCandidates.every((item) => Number(item?.memberCount) === 2
&& Number(item?.groupTotalBoxes) === first.packages + second.packages && item?.locked !== true
&& String(item?.blockReason || '').includes('同柜不可拆组整体超过当前柜剩余容量')),
&& Number(item?.groupTotalBoxes) === first.packages + second.packages && item?.locked !== true),
`count=${groupedCandidates.length}, groupTotal=${first.packages + second.packages}`, OPTIONS)
let isolatedCandidates = candidates
@@ -555,23 +554,30 @@ export async function run(context) {
candidateGroupIds.length === 1 && candidateGroupIds[0] === groupId
&& JSON.stringify(targetCandidateInbounds) === JSON.stringify([first.inboundId, second.inboundId].sort()),
`groupId=${groupId}, candidateGroupIds=${candidateGroupIds.join('|') || 'none'}, members=${targetCandidateInbounds.join('|')}, capacity=33cbm/28000kg`, OPTIONS)
const targetCandidateVolume = isolatedCandidates.filter((item) => id(item?.coLoadGroupId) === groupId)
const allCandidateVolume = isolatedCandidates
.reduce((sum, item) => sum + Number(item?.availableVolumeCbm || 0), 0)
context.assert(MODULE, '目标组自身总体积明确超过20GP容量容差', isolatedCandidateResponse,
targetCandidateVolume > 33 * 1.1,
`targetGroupId=${groupId}, volume=${targetCandidateVolume}, limit=${33 * 1.1}`, OPTIONS)
context.assert(MODULE, '推荐候选总量形成容量竞争场景', isolatedCandidateResponse,
allCandidateVolume > 33 * 1.1,
`targetGroupId=${groupId}, candidateVolume=${allCandidateVolume}, limit=${33 * 1.1}`, OPTIONS)
const beforeRecommend = plan
const recommendResponse = await context.request('POST',
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/recommend`, {
expectedVersion: Number(plan?.version),
operationId: `${context.marker}-coload-capacity-recommend`.slice(0, 128),
})
const capacityRecommendation = context.apiSuccess(MODULE, '目标同柜组整体超容时智能推荐跳过整组',
const capacityRecommendation = context.apiSuccess(MODULE, '容量竞争时智能推荐不因整组容量失败',
recommendResponse, evidence(recommendResponse, `targetGroupId=${groupId}`), OPTIONS)
const capacityGroupItems = (capacityRecommendation?.items || [])
.filter((item) => [first.inboundId, second.inboundId].includes(id(item?.inboundId)))
context.assert(MODULE, '整组容量不足时智能推荐全不取且不产生部分成员', recommendResponse,
capacityGroupItems.length === 0,
const capacityTotals = new Map()
for (const item of capacityGroupItems) {
capacityTotals.set(id(item?.inboundId),
(capacityTotals.get(id(item?.inboundId)) || 0) + Number(item?.packageCount || 0))
}
const capacityTookNone = capacityGroupItems.length === 0
const capacityTookWhole = [first, second].every((fixture) => capacityTotals.get(fixture.inboundId) === fixture.packages)
context.assert(MODULE, '容量竞争时同柜组只能整组全取或全不取', recommendResponse,
capacityTookNone || capacityTookWhole,
`targetGroupId=${groupId}, assignedMembers=${capacityGroupItems.map((item) => id(item?.inboundId)).join('|') || 'none'}, before=${planSignature(beforeRecommend)}, after=${planSignature(capacityRecommendation)}`, OPTIONS)
const recommendSimulationResponse = await context.request('POST', '/api/platform/v1/containers', {
@@ -782,14 +788,14 @@ export async function run(context) {
if (!partialPda || !fullPda || !normalPda) return
const partialTreesBefore = await fulfillmentTrees(context, partialChain.fixtures, '短拣前')
const partialTarget = (partialPda.task?.items || []).find((item) => Number(item?.plannedPackageCount) > 1)
context.assert(MODULE, '短拣链存在可记录PARTIAL的多箱明细', null, Boolean(partialTarget?.id),
const partialTarget = (partialPda.task?.items || []).find((item) => Number(item?.plannedPackageCount) > 0)
context.assert(MODULE, '短拣链存在可记录SKIP的组成员明细', null, Boolean(partialTarget?.id),
`planned=${partialTarget?.plannedPackageCount ?? 'missing'}, taskItems=${partialPda.task?.items?.length ?? 0}`, OPTIONS)
if (!partialTarget?.id) return
const partialBatchItems = (partialPda.task.items || []).map((item) => id(item.id) === id(partialTarget.id)
? { itemId: id(item.id), action: 'PARTIAL', pickedPackageCount: null,
finalPickedPackageCount: Number(item.plannedPackageCount) - 1, releaseMode: 'MANUAL',
locationIds: [], version: Number(item.version), reason: null }
? { itemId: id(item.id), action: 'SKIP', pickedPackageCount: null,
finalPickedPackageCount: null, releaseMode: 'MANUAL',
locationIds: [], version: Number(item.version), reason: '同柜不可拆组短拣验收' }
: { itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount),
finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [],
version: Number(item.version), reason: null })
@@ -798,13 +804,12 @@ export async function run(context) {
containerBookingId: partialChain.containerBookingId, expectedVersion: Number(partialPda.task.version),
inventorySnapshotToken: partialPda.inventory.snapshotToken, items: partialBatchItems,
}, undefined, { 'Idempotency-Key': `${context.marker}-coload-partial-batch`.slice(0, 128) })
const partialBatch = context.apiSuccess(MODULE, '同柜组PDA原子批次记录部分拣货异常', partialBatchResponse,
const partialBatch = context.apiSuccess(MODULE, '同柜组PDA原子批次记录短拣异常', partialBatchResponse,
evidence(partialBatchResponse), OPTIONS)
context.assert(MODULE, '短拣批次只进入待确认且保留PARTIAL事实', partialBatchResponse,
context.assert(MODULE, '短拣批次只进入待确认且保留SKIP事实', partialBatchResponse,
partialBatch?.taskStatus === 'LOADING' && partialBatch?.workflowStatus === 'AWAITING_REVIEW'
&& partialBatch?.items?.some((item) => id(item?.id) === id(partialTarget.id)
&& item?.itemStatus === 'PARTIAL'
&& Number(item?.pickedPackageCount) === Number(partialTarget.plannedPackageCount) - 1),
&& item?.itemStatus === 'SKIPPED' && Number(item?.pickedPackageCount) === 0),
`taskStatus=${partialBatch?.taskStatus || 'missing'}, workflow=${partialBatch?.workflowStatus || 'missing'}`, OPTIONS)
const partialTaskBeforeComplete = taskSignature(partialBatch)
const partialCompleteResponse = await context.request('POST', `${PDA}/tasks/${partialPda.taskId}/complete`, {
@@ -955,6 +960,38 @@ export async function run(context) {
evidence(splitCancelResponse), OPTIONS)
}
let uiAcceptance = null
if (differentDestination) {
const uiGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups',
{ orderIds: [fifth.orderId, differentDestination.orderId] }, undefined,
{ 'Idempotency-Key': `${context.marker}-coload-ui-preserve`.slice(0, 128) })
const uiGroup = context.apiSuccess(MODULE, '为Admin页面保留ACTIVE未分配跨目的仓组', uiGroupResponse,
evidence(uiGroupResponse), OPTIONS)
const uiSimulationResponse = await context.request('POST', CONTAINERS, {
channelCode: fifth.channelCode, warehouseId: fifth.warehouseId, containerType: '40HC',
capacityCbm: 76, maxWeightKg: 28000,
})
const uiSimulation = context.apiSuccess(MODULE, '为Admin页面保留可编辑智能装柜柜次', uiSimulationResponse,
evidence(uiSimulationResponse), OPTIONS)
const uiBookingId = id(uiSimulation?.id)
const uiPlanResponse = uiBookingId ? await context.request('POST', `${SMART}/plans`, {
bookingId: uiBookingId, remark: `${context.marker} Admin整组候选人工验收保留`,
}) : null
const uiPlan = uiPlanResponse ? context.apiSuccess(MODULE, '为Admin页面保留未分配空计划', uiPlanResponse,
evidence(uiPlanResponse, `bookingId=${uiBookingId}`), OPTIONS) : null
const uiMembers = memberFacts(uiGroup)
context.assert(MODULE, '页面验收保留组仍为ACTIVE且未锁定', uiGroupResponse,
uiGroup?.status === 'ACTIVE' && !uiGroup?.lockedPlanId && uiMembers.length === 2
&& Boolean(uiBookingId) && Boolean(uiPlan?.id),
`groupId=${id(uiGroup?.groupId)}, groupNo=${uiGroup?.groupNo || 'missing'}, bookingId=${uiBookingId}, planId=${id(uiPlan?.id)}, members=${JSON.stringify(uiMembers)}`, OPTIONS)
uiAcceptance = {
groupId: id(uiGroup?.groupId), groupNo: uiGroup?.groupNo || null,
orderIds: [fifth.orderId, differentDestination.orderId],
inboundIds: [fifth.inboundId, differentDestination.inboundId],
bookingId: uiBookingId, planId: id(uiPlan?.id),
}
}
context.createdResources.coloadGroupAcceptance = {
marker: context.marker, groupId, groupNo: group?.groupNo || null, planId, bookingId,
orderIds: groupOrders, inboundIds: [first.inboundId, second.inboundId],
@@ -962,6 +999,7 @@ export async function run(context) {
partialTaskId: partialPda.taskId, normalTaskId: normalPda.taskId,
oceanBookingId, cancelledGroupId: cancelGroupId, remainderOrderId: id(remainder.orderId),
splitGroupId: id(splitGroup?.groupId), crossDestinationOrderId: id(differentDestination?.orderId),
uiAcceptance,
publicBypassResult: 'individual assignment/adjust/delete and short-pick completion rejected; full group consumed',
}
}