test: verify pallet block settlement over HTTP
This commit is contained in:
@@ -78,6 +78,13 @@ async function request(context, method, path, body, headers = {}) {
|
||||
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
|
||||
}
|
||||
}
|
||||
function blockResourceIdentity(resource) {
|
||||
return JSON.stringify({
|
||||
blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
|
||||
reservationId: id(resource?.reservationId), locationId: id(resource?.locationId),
|
||||
sharedOverflow: resource?.sharedOverflow === true,
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForCompletion(context, taskDetailPath, operationsPath, attempts = 90) {
|
||||
let taskResponse = null
|
||||
@@ -258,6 +265,59 @@ export async function run(context) {
|
||||
&& item.operatorName && item.fromStatus === 'LOADING' && item.toStatus === 'LOADED'),
|
||||
`status=${finalData?.summary?.processStatus || 'missing'}, version=${finalData?.summary?.version ?? 'missing'}, histories=${finalData?.statusHistory?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
const expectedBlocks = [...(fixture.inventoryBlockSnapshots || []), fixture.partialBlockSnapshot]
|
||||
.filter((item) => item?.inboundId && Array.isArray(item?.resources) && item.resources.length > 0)
|
||||
.filter((item, index, all) => all.findIndex((candidate) => id(candidate.inboundId) === id(item.inboundId)) === index)
|
||||
const releasedOrTransferredBlocks = []
|
||||
for (const expected of expectedBlocks) {
|
||||
const sourceBlockResponse = await context.request('GET',
|
||||
`/api/platform/inbounds/${encodeURIComponent(id(expected.inboundId))}?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const sourceBlock = context.apiSuccess(MODULE, '完成后回查来源入仓单整票块状态', sourceBlockResponse, '', OPTIONS)
|
||||
const expectedIdentity = expected.resources.map(blockResourceIdentity).sort()
|
||||
const sourceLocations = sourceBlock?.palletLocations || []
|
||||
const sourceIdentity = sourceLocations.map(blockResourceIdentity).sort()
|
||||
const statuses = new Set(sourceLocations.map((item) => item?.allocationStatus))
|
||||
const transferred = statuses.size === 1 && statuses.has('TRANSFERRED')
|
||||
const released = statuses.size === 1 && statuses.has('RELEASED')
|
||||
let inheritedByRemainder = false
|
||||
if (transferred) {
|
||||
const sourceCode = String(sourceBlock?.summary?.inboundCode || expected.inboundCode || '').trim()
|
||||
const remainderPageResponse = await context.request('GET',
|
||||
`/api/platform/inbounds?tenantId=${encodeURIComponent(tenantId)}&inboundCode=${encodeURIComponent(`${sourceCode}-`)}&page=1&size=200`)
|
||||
const remainderPage = context.apiSuccess(MODULE, '查询部分拣货生成的余量入仓单', remainderPageResponse, '', OPTIONS)
|
||||
for (const row of remainderPage?.records || []) {
|
||||
if (id(row?.id) === id(expected.inboundId)) continue
|
||||
const remainderResponse = await context.request('GET',
|
||||
`/api/platform/inbounds/${encodeURIComponent(id(row?.id))}?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const remainder = remainderResponse?.http === 200 && codeOf(remainderResponse) === '200'
|
||||
? remainderResponse.body?.data : null
|
||||
const locations = remainder?.palletLocations || []
|
||||
if (locations.length > 0
|
||||
&& locations.every((item) => item?.allocationStatus === 'CONFIRMED')
|
||||
&& JSON.stringify(locations.map(blockResourceIdentity).sort()) === JSON.stringify(expectedIdentity)) {
|
||||
inheritedByRemainder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const identityStable = JSON.stringify(sourceIdentity) === JSON.stringify(expectedIdentity)
|
||||
const verified = identityStable && (released || (transferred && inheritedByRemainder))
|
||||
context.assert(MODULE, '整票块释放或原身份转移给余量入仓单', sourceBlockResponse, verified,
|
||||
`inboundId=${id(expected.inboundId)}, blockId=${expected.resources[0]?.blockId || 'missing'}, statuses=${[...statuses].join(',') || 'missing'}, identityStable=${identityStable}, inheritedByRemainder=${inheritedByRemainder}`, OPTIONS)
|
||||
releasedOrTransferredBlocks.push({ inboundId: id(expected.inboundId), released, transferred, verified })
|
||||
}
|
||||
const completedInventoryResponse = await context.request('GET',
|
||||
`${taskPath}/inventory-snapshot?warehouseId=${encodeURIComponent(id(fixture.warehouseId))}`)
|
||||
const completedInventory = context.apiSuccess(MODULE, '完成后回查 PDA 实时库存快照', completedInventoryResponse, '', OPTIONS)
|
||||
const completedInventoryReleased = expectedBlocks.length > 0
|
||||
&& expectedBlocks.every((expected) => {
|
||||
const current = completedInventory?.inbounds?.find?.((item) => id(item?.inboundId) === id(expected.inboundId))
|
||||
return Number(current?.availablePackageCount || 0) === 0 && (current?.resources || []).length === 0
|
||||
})
|
||||
context.assert(MODULE, '完成后的来源整票块不再作为 PDA 可用资源', completedInventoryResponse,
|
||||
completedInventoryReleased,
|
||||
`expectedBlocks=${expectedBlocks.length}, available=${(completedInventory?.inbounds || []).reduce((sum, item) => sum + Number(item?.availablePackageCount || 0), 0)}`, OPTIONS)
|
||||
|
||||
const slicesPath = `/api/platform/v1/orders/container-bookings/${encodeURIComponent(containerBookingId)}/fulfillment-slices?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const slicesResponse = await context.request('GET', slicesPath)
|
||||
const slices = context.apiSuccess(MODULE, '结算后回查柜次实际履约分片', slicesResponse, '', OPTIONS)
|
||||
@@ -427,6 +487,8 @@ export async function run(context) {
|
||||
...buildFinalizeResource(fixture, finalData, completeResponse),
|
||||
settlementMode,
|
||||
settledOrderId: id(loadedSlice?.childOrderId) || orderId,
|
||||
inventoryBlockSettlementVerified: expectedBlocks.length > 0
|
||||
&& releasedOrTransferredBlocks.every((item) => item.verified) && completedInventoryReleased,
|
||||
}
|
||||
const producer = {
|
||||
marker: context.context?.phase4ProducerMatrixMarker || context.marker,
|
||||
|
||||
@@ -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)
|
||||
@@ -747,105 +761,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'
|
||||
@@ -854,7 +770,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
|
||||
@@ -1179,6 +1095,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,
|
||||
@@ -1277,6 +1194,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),
|
||||
@@ -1510,8 +1443,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),
|
||||
|
||||
@@ -123,17 +123,10 @@ export async function run(context) {
|
||||
'inventory-snapshot 端点已公开,但本轮未完成归属、稳定 token/version、数量及合法 batch 的联合真实 HTTP 验证',
|
||||
'deployment-blocker')
|
||||
}
|
||||
if (fixture?.inventoryMutationRequired !== false && fixture?.inventoryMutationVerified !== true
|
||||
&& !context.coverageGaps.some((gap) => gap.name === 'PDA 真实库存变化后的旧快照冲突与刷新重试')) {
|
||||
context.addStep(MODULE, 'PDA 真实库存变化后旧 token A0409 与刷新重试', null, false,
|
||||
'本轮未取得 inventoryMutationVerified=true;无法安全制造库存变化时必须作为部署阻断失败', OPTIONS)
|
||||
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 真实库存变化后的旧快照冲突与刷新重试',
|
||||
'inventory-snapshot 已公开;本轮没有可通过平台代理安全变更的 A0 临时占用,不能用普通库位改写或内部 Warehouse 接口伪造库存变化',
|
||||
'fixture')
|
||||
} else if (fixture?.inventoryMutationRequired === false) {
|
||||
context.addStep(MODULE, '依赖链不重复执行 A0 旧快照破坏性专项', null, true,
|
||||
'正常库存快照、原子批次和幂等已验证;A0 换绑与旧 token A0409 仅在显式 phase3-loading 专项运行',
|
||||
{ scenario: OPTIONS.scenario })
|
||||
if (fixture?.inventoryBlockIdentityVerified !== true
|
||||
|| !Array.isArray(fixture?.inventoryBlockSnapshots) || fixture.inventoryBlockSnapshots.length === 0) {
|
||||
context.addStep(MODULE, 'PDA 整票库位块身份已由正式库存快照验证', null, false,
|
||||
'phase3-loading 未发布唯一 blockId 及 slotNo/reservationId/locationId/sharedOverflow 身份快照', OPTIONS)
|
||||
}
|
||||
if (fixture?.dynamicIncrementVerified !== true) {
|
||||
context.addStep(MODULE, 'LOADING 增量重开、刷新续拣与已拣不可删减', null, false,
|
||||
|
||||
@@ -240,21 +240,17 @@ test('finalize rechecks the same unified order detail and list as LOADED', () =>
|
||||
assert.match(source('./phase3-loading.mjs'), /tenantId, orderId: id\(source\?\.orderId\), inboundId/)
|
||||
})
|
||||
|
||||
test('PDA inventory snapshot gates batch and records the only safe mutation gap', () => {
|
||||
test('PDA inventory snapshot gates batch and records whole-ticket block identity', () => {
|
||||
const loading = source('./phase3-loading.mjs')
|
||||
assertOrder(loading, [
|
||||
'读取 PDA 提交前实时库存快照',
|
||||
'未发生库存变化时重复读取实时库存快照',
|
||||
'PDA 库存快照按入仓单返回唯一整票库位块身份',
|
||||
'客户身份无权读取平台 PDA 实时库存快照',
|
||||
'const overflowResource =',
|
||||
'通过公开 A0 换绑接口制造真实库存变化',
|
||||
'真实库存变化后旧 snapshot token 提交返回冲突',
|
||||
'旧 token 冲突对任务、明细、已拣数量和操作历史零写入',
|
||||
'库存变化后刷新 PDA 实时库存快照',
|
||||
'const canonicalPick = await request',
|
||||
'const canonicalBatchReplay = await request',
|
||||
])
|
||||
assert.match(loading, /inventorySnapshotToken: refreshedInventory\.snapshotToken/)
|
||||
assert.match(loading, /inventorySnapshotToken: inventory\.snapshotToken/)
|
||||
assert.match(loading, /warehouseId, loadingPlanId, containerBookingId,/)
|
||||
assert.match(loading,
|
||||
/LATE_CONTAINER_GATE_ONLY \? inventory\?\.currentContainerNo == null : inventory\?\.currentContainerNo === containerNo/)
|
||||
@@ -262,13 +258,14 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap'
|
||||
assert.doesNotMatch(batchPayload, /containerNo[,\s]/)
|
||||
assert.doesNotMatch(loading, /\/loading\/picking\/(?:start|complete)/)
|
||||
assert.match(loading, /inventorySnapshotVerified: inventorySnapshotVerified && atomicBatchReplayVerified/)
|
||||
assert.match(loading, /inventoryMutationVerified, inventoryMutationCleanup/)
|
||||
assert.match(loading, /当前唯一平台代理库存变更入口只允许 A0 临时占用换绑/)
|
||||
assert.match(loading, /inventoryBlockIdentityVerified, inventoryBlockSnapshots/)
|
||||
assert.match(loading, /partialBlockSnapshot/)
|
||||
assert.doesNotMatch(loading, /occupancyId|temporary-occupancies|\/rebind/)
|
||||
|
||||
const convergence = source('./phase3-pda-convergence.mjs')
|
||||
assert.doesNotMatch(convergence, /未公开实时库存查询或库存版本快照端点/)
|
||||
assert.match(convergence, /fixture\?\.inventorySnapshotVerified !== true/)
|
||||
assert.match(convergence, /fixture\?\.inventoryMutationVerified !== true/)
|
||||
assert.match(convergence, /fixture\?\.inventoryBlockIdentityVerified !== true/)
|
||||
})
|
||||
|
||||
test('container loading arranges a simulation before linking a released booking requirement', () => {
|
||||
|
||||
@@ -4,11 +4,6 @@ import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./supplemental-boundaries.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('occupied location deletion protection reuses only the marker-owned loading fixture', () => {
|
||||
assert.match(source, /phase3Loading/)
|
||||
assert.match(source, /inventoryMutationCleanup\?\.targetLocationId/)
|
||||
assert.match(source, /loading\?\.marker === context\.marker/)
|
||||
assert.match(source, /真实占用库位删除返回A0409且保留资源/)
|
||||
assert.match(source, /占用删除冲突未造成资源丢失/)
|
||||
assert.doesNotMatch(source, /no-public-fixture-api|当前公共验收 API 未提供/)
|
||||
test('supplemental boundaries do not mutate PDA block locations through retired occupancy APIs', () => {
|
||||
assert.doesNotMatch(source, /inventoryMutationCleanup|temporary-occupancies|occupancyId|\/rebind/)
|
||||
})
|
||||
|
||||
@@ -86,31 +86,6 @@ export async function run(context) {
|
||||
locationVersion = Number(afterData?.location?.version)
|
||||
}
|
||||
|
||||
// phase3-loading has already created and rebound a marker-owned live
|
||||
// occupancy. Reuse that exact location to prove deletion protection without
|
||||
// touching historical resources or inventing a setup-only API.
|
||||
const loading = context.createdResources?.phase3Loading
|
||||
const occupiedLocationId = id(loading?.inventoryMutationCleanup?.targetLocationId)
|
||||
const occupiedWarehouseId = id(loading?.warehouseId)
|
||||
context.assert(MODULE, '取得本轮装柜链真实占用库位', null,
|
||||
Boolean(occupiedLocationId && occupiedWarehouseId && loading?.marker === context.marker),
|
||||
`locationId=${occupiedLocationId || 'missing'}, warehouseId=${occupiedWarehouseId || 'missing'}`, OPTIONS)
|
||||
if (occupiedLocationId && occupiedWarehouseId && loading?.marker === context.marker) {
|
||||
const occupiedPath = `/api/platform/v1/locations/${occupiedLocationId}?tenantId=0&warehouseId=${occupiedWarehouseId}`
|
||||
const occupiedDetail = await context.request('GET', occupiedPath)
|
||||
const occupied = context.apiSuccess(MODULE, '读取本轮真实占用库位版本', occupiedDetail, '', OPTIONS)
|
||||
const occupiedVersion = Number(occupied?.location?.version)
|
||||
const rejectedDelete = await context.request('DELETE', `${occupiedPath}&version=${occupiedVersion}`)
|
||||
context.addStep(MODULE, '真实占用库位删除返回A0409且保留资源', rejectedDelete,
|
||||
rejectedDelete.http === 200 && ['A0409', '409'].includes(codeOf(rejectedDelete)),
|
||||
`HTTP=${rejectedDelete.http}, code=${codeOf(rejectedDelete)}, version=${occupiedVersion}`, OPTIONS)
|
||||
const occupiedAfter = await context.request('GET', occupiedPath)
|
||||
const after = context.apiSuccess(MODULE, '删除冲突后回查占用库位', occupiedAfter, '', OPTIONS)
|
||||
context.assert(MODULE, '占用删除冲突未造成资源丢失', occupiedAfter,
|
||||
id(after?.location?.id) === occupiedLocationId,
|
||||
`expected=${occupiedLocationId}, actual=${id(after?.location?.id) || 'missing'}`, OPTIONS)
|
||||
}
|
||||
|
||||
// Import boundary probes use only in-memory bytes and dry-run where supported.
|
||||
const bogusXlsx = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x00, 0x00])
|
||||
const empty = new Uint8Array()
|
||||
|
||||
Reference in New Issue
Block a user