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
@@ -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,
+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),
@@ -124,17 +124,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,
@@ -227,21 +227,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/)
@@ -249,13 +245,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()