Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-04 14:04:07 +08:00
6 changed files with 111 additions and 156 deletions
+36 -103
View File
@@ -645,6 +645,20 @@ export async function run(context) {
context.assert(MODULE, '未变化库存返回稳定 token、版本和数量快照', repeatedInventoryResponse,
inventorySnapshotVerified,
`sameToken=${repeatedInventory?.snapshotToken === inventory.snapshotToken}, taskVersion=${repeatedInventory?.taskVersion ?? 'missing'}`, OPTIONS)
const inventoryBlockSnapshots = (inventory?.inbounds || []).map((entry) => ({
inboundId: id(entry?.inboundId),
inboundCode: canonicalTask?.items?.find?.((item) => id(item?.inboundId) === id(entry?.inboundId))?.inboundCode || null,
resources: (entry?.resources || []).map((resource) => ({
blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
reservationId: id(resource?.reservationId), locationId: id(resource?.locationId),
sharedOverflow: resource?.sharedOverflow === true,
})),
})).filter((entry) => entry.inboundId && entry.resources.length > 0)
const inventoryBlockIdentityVerified = inventoryBlockSnapshots.length > 0
&& inventoryBlockSnapshots.every((entry) => new Set(entry.resources.map((resource) => resource.blockId)).size === 1)
context.assert(MODULE, 'PDA 库存快照按入仓单返回唯一整票库位块身份', inventoryResponse,
inventoryBlockIdentityVerified,
`inbounds=${inventoryBlockSnapshots.length}, blocks=${inventoryBlockSnapshots.map((entry) => entry.resources[0]?.blockId).join(',')}`, OPTIONS)
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
@@ -745,105 +759,7 @@ export async function run(context) {
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
let inventoryMutationVerified = false
let inventoryMutationCleanup = null
let effectiveBatchPayload = batchPayload
const overflowResource = inventoryInbound?.resources?.find?.((resource) =>
resource?.sharedOverflow === true && id(resource?.occupancyId))
if (overflowResource) {
const mutationSuffix = markerSuffix(context.marker)
const targetLocationResponse = await context.request('POST', '/api/platform/v1/locations?tenantId=0', {
warehouseId, zoneCode: `SNAP-${mutationSuffix.slice(-6)}`, zoneName: '库存快照验收区',
locationCode: `SNAP-${mutationSuffix}`, locationName: '库存快照换绑库位',
locationType: 'PALLET', maxWeightKg: 1000, status: 'AVAILABLE',
remark: `${context.marker}-inventory-snapshot-mutation`,
})
const targetLocationId = id(context.apiSuccess(MODULE, '创建库存快照变化目标库位',
targetLocationResponse, '', OPTIONS))
if (!targetLocationId) {
context.addStep(MODULE, '库存变化fixture创建失败后停止旧snapshot提交', targetLocationResponse,
false, responseEvidence(targetLocationResponse), OPTIONS)
return
}
if (targetLocationId) {
inventoryMutationCleanup = {
cleaned: false, targetLocationId,
reason: '目标库位在真实换绑后属于活动占用,公开删除接口会拒绝,不伪报已清理',
}
}
const beforeMutationTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const beforeMutationTask = context.apiSuccess(MODULE, '库存变化前读取 PDA 任务基线', beforeMutationTaskResponse, '', OPTIONS)
const beforeMutationOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const beforeMutationOperations = context.apiSuccess(MODULE, '库存变化前读取 PDA 操作历史基线',
beforeMutationOperationsResponse, '', OPTIONS)
const rebindResponse = targetLocationId
? await request(context, 'POST',
`/api/platform/v1/locations/temporary-occupancies/${encodeURIComponent(id(overflowResource.occupancyId))}/rebind`, {
targetLocationId, idempotencyKey: `${context.marker}-snapshot-rebind`.slice(0, 128),
reason: `${context.marker}-inventory-changed`,
})
: targetLocationResponse
const rebound = context.apiSuccess(MODULE, '通过公开 A0 换绑接口制造真实库存变化', rebindResponse, '', OPTIONS)
const reboundValid = id(rebound?.occupancyId) === id(overflowResource.occupancyId)
&& id(rebound?.previousLocationId) === id(overflowResource.locationId)
&& id(rebound?.locationId) === targetLocationId
context.assert(MODULE, '真实库存换绑保持同仓且位置发生变化', rebindResponse,
reboundValid,
`occupancyId=${id(rebound?.occupancyId) || 'missing'}, previousLocationId=${id(rebound?.previousLocationId) || 'missing'}, locationId=${id(rebound?.locationId) || 'missing'}`, OPTIONS)
if (!reboundValid) {
context.addStep(MODULE, '库存换绑未成功后停止旧snapshot提交', rebindResponse,
false, responseEvidence(rebindResponse), OPTIONS)
return
}
const staleInventoryBatch = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': `${context.marker}-bridge-batch-stale-inventory`.slice(0, 128) })
context.addStep(MODULE, '真实库存变化后旧 snapshot token 提交返回冲突', staleInventoryBatch,
staleInventoryBatch.http === 200 && codeOf(staleInventoryBatch) === 'A0409'
&& String(staleInventoryBatch.body?.message || '').includes('库存已变化'),
`HTTP=${staleInventoryBatch.http}, code=${codeOf(staleInventoryBatch)}, message=${staleInventoryBatch.body?.message || 'missing'}`, OPTIONS)
const afterStaleTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const afterStaleTask = context.apiSuccess(MODULE, '旧库存快照冲突后回查 PDA 任务', afterStaleTaskResponse, '', OPTIONS)
const afterStaleOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const afterStaleOperations = context.apiSuccess(MODULE, '旧库存快照冲突后回查 PDA 操作历史',
afterStaleOperationsResponse, '', OPTIONS)
context.assert(MODULE, '旧 token 冲突对任务、明细、已拣数量和操作历史零写入', afterStaleTaskResponse,
Number(afterStaleTask?.version) === Number(beforeMutationTask?.version)
&& taskItemsSignature(afterStaleTask) === taskItemsSignature(beforeMutationTask)
&& operationsSignature(afterStaleOperations) === operationsSignature(beforeMutationOperations),
`taskVersion=${afterStaleTask?.version ?? 'missing'}, items=${taskItemsSignature(afterStaleTask)}, operations=${afterStaleOperations?.length ?? 'missing'}`, OPTIONS)
const refreshedInventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const refreshedInventory = context.apiSuccess(MODULE, '库存变化后刷新 PDA 实时库存快照',
refreshedInventoryResponse, '', OPTIONS)
const refreshedInbound = refreshedInventory?.inbounds?.find?.((item) => id(item.inboundId) === inboundId)
inventoryMutationVerified = refreshedInventory?.snapshotToken !== inventory.snapshotToken
&& Number(refreshedInventory?.taskVersion) === Number(inventory.taskVersion)
&& refreshedInbound?.resources?.some?.((resource) => id(resource.occupancyId) === id(overflowResource.occupancyId)
&& id(resource.locationId) === targetLocationId)
context.assert(MODULE, '刷新快照返回新 token、同任务版本和变更后库存', refreshedInventoryResponse,
inventoryMutationVerified,
`tokenChanged=${refreshedInventory?.snapshotToken !== inventory.snapshotToken}, taskVersion=${refreshedInventory?.taskVersion ?? 'missing'}, targetLocation=${targetLocationId || 'missing'}`, OPTIONS)
if (refreshedInventory?.snapshotToken) {
effectiveBatchPayload = { ...batchPayload, inventorySnapshotToken: refreshedInventory.snapshotToken }
}
} else if (source?.requireInventoryMutation !== false) {
context.addStep(MODULE, '自动制造库存变化并验证旧 snapshot token A0409', null, false,
`本轮 ${inventoryInbound?.resources?.length ?? 0} 条占用没有可安全换绑的 A0 临时占用;不得以 coverageGap 作为通过`, OPTIONS)
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 真实库存变化后的旧快照冲突与刷新重试',
`inventory-snapshot 已真实返回 taskId=${canonicalTaskId}、warehouseId=${warehouseId} 并验证稳定 token/version=${inventorySnapshotVerified};本轮 ${inventoryInbound?.resources?.length ?? 0} 条占用均为普通库位,当前唯一平台代理库存变更入口只允许 A0 临时占用换绑,不能安全改动普通占用或调用内部 Warehouse 接口`,
'fixture')
} else {
context.addStep(MODULE, '非快照专项装柜链不重复制造 A0 库存变化', null, true,
'当前模块作为四期真实业务前置,仅验证正常装柜链;旧 snapshot token A0409 由独立 phase3-loading 专项覆盖',
{ scenario: OPTIONS.scenario })
}
const canonicalPick = await request(context, 'POST', batchPath, effectiveBatchPayload,
const canonicalPick = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': batchKey })
const pickedCanonical = context.apiSuccess(MODULE, '原子提交柜次 PDA 整批拣货', canonicalPick, '', OPTIONS)
const awaitingReview = pickedCanonical?.taskStatus === 'LOADING'
@@ -852,7 +768,7 @@ export async function run(context) {
awaitingReview && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
`taskStatus=${pickedCanonical?.taskStatus || 'missing'}, workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
const canonicalBatchReplay = await request(context, 'POST', batchPath, effectiveBatchPayload,
const canonicalBatchReplay = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': batchKey })
const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
&& id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId
@@ -1177,6 +1093,7 @@ export async function run(context) {
const incrementalInboundId = id(incrementalFixture?.inboundId)
let skipBatchVerified = false
let partialBatchVerified = false
let partialBlockSnapshot = null
let dynamicIncrementVerified = false
if (!incrementalInboundId || incrementalFixture?.marker !== context.marker || incrementalInboundId === inboundId) {
context.addStep(MODULE, '取得本轮独立动态增量入库单', null, false,
@@ -1275,6 +1192,22 @@ export async function run(context) {
const partialInventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const partialInventory = context.apiSuccess(MODULE, '部分拣货前刷新实时库存快照', partialInventoryResponse, '', OPTIONS)
const partialInventoryInbound = partialInventory?.inbounds?.find?.((item) =>
id(item?.inboundId) === incrementalInboundId)
const partialResources = (partialInventoryInbound?.resources || []).map((resource) => ({
blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
reservationId: id(resource?.reservationId), locationId: id(resource?.locationId),
sharedOverflow: resource?.sharedOverflow === true,
}))
partialBlockSnapshot = {
inboundId: incrementalInboundId,
inboundCode: unresolvedTaskItem?.inboundCode || null,
resources: partialResources,
}
context.assert(MODULE, 'PARTIAL 前余货属于唯一整票库位块', partialInventoryResponse,
partialResources.length > 0
&& new Set(partialResources.map((resource) => resource.blockId)).size === 1,
`inboundId=${incrementalInboundId}, resources=${partialResources.length}, blockId=${partialResources[0]?.blockId || 'missing'}`, OPTIONS)
const partialBatchResponse = await request(context, 'POST', batchPath, {
warehouseId, loadingPlanId, containerBookingId,
expectedVersion: Number(afterUnresolvedCompleteTask?.version),
@@ -1348,7 +1281,7 @@ export async function run(context) {
const staleIncrementBatch = await request(context, 'POST', batchPath, {
warehouseId, loadingPlanId, containerBookingId,
expectedVersion: Number(incrementedTask?.version),
inventorySnapshotToken: effectiveBatchPayload.inventorySnapshotToken,
inventorySnapshotToken: batchPayload.inventorySnapshotToken,
items: [{ itemId: id(incrementalTaskItem.id), action: 'PICK', pickedPackageCount: 1,
version: Number(incrementalTaskItem.version) }],
}, { 'Idempotency-Key': `${context.marker}-dynamic-stale-inventory`.slice(0, 128) })
@@ -1508,8 +1441,8 @@ export async function run(context) {
pickingTaskId: canonicalTaskId, inboundVersion: loadingVersion,
atomicBatchVerified: awaitingReview && atomicBatchReplayVerified,
inventorySnapshotVerified: inventorySnapshotVerified && atomicBatchReplayVerified,
inventoryMutationVerified, inventoryMutationCleanup,
inventoryMutationRequired: source?.requireInventoryMutation !== false,
inventoryBlockIdentityVerified, inventoryBlockSnapshots,
partialBlockSnapshot,
skipBatchVerified, partialBatchVerified, dynamicIncrementVerified,
expectedBatchOperationIds, incrementalInboundId,
pdaTaskVersion: Number(stagedTask?.version),