From 7feb80689be31b9f0468e22295a8c3f9d658d0a2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 13:48:03 +0800 Subject: [PATCH 1/2] test: verify pallet block settlement over HTTP --- .../phase3-loading-finalize.mjs | 62 ++++++++ .../admin-test-env-modules/phase3-loading.mjs | 137 +++++------------- .../phase3-pda-convergence.mjs | 15 +- .../phase3-producer-chain-contract.test.mjs | 17 +-- .../supplemental-boundaries-contract.test.mjs | 9 +- .../supplemental-boundaries.mjs | 25 ---- 6 files changed, 110 insertions(+), 155 deletions(-) diff --git a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs index 12ee1f485..8d705c2d0 100644 --- a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs +++ b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs @@ -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, diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index da9ad2755..eb677815b 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -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), diff --git a/scripts/admin-test-env-modules/phase3-pda-convergence.mjs b/scripts/admin-test-env-modules/phase3-pda-convergence.mjs index 9657fa6dc..c9d8ccad5 100644 --- a/scripts/admin-test-env-modules/phase3-pda-convergence.mjs +++ b/scripts/admin-test-env-modules/phase3-pda-convergence.mjs @@ -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, diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index 6b2d07802..cbca07372 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -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', () => { diff --git a/scripts/admin-test-env-modules/supplemental-boundaries-contract.test.mjs b/scripts/admin-test-env-modules/supplemental-boundaries-contract.test.mjs index d80e26117..d54cc4b96 100644 --- a/scripts/admin-test-env-modules/supplemental-boundaries-contract.test.mjs +++ b/scripts/admin-test-env-modules/supplemental-boundaries-contract.test.mjs @@ -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/) }) diff --git a/scripts/admin-test-env-modules/supplemental-boundaries.mjs b/scripts/admin-test-env-modules/supplemental-boundaries.mjs index a6057b65d..ec4d559e5 100644 --- a/scripts/admin-test-env-modules/supplemental-boundaries.mjs +++ b/scripts/admin-test-env-modules/supplemental-boundaries.mjs @@ -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() From a08559e1639b329fa993b86e3e0a29ac73b723fb Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 13:50:11 +0800 Subject: [PATCH 2/2] test: use original token for dynamic stale snapshot --- scripts/admin-test-env-modules/phase3-loading.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index eb677815b..5f774d6b6 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -1283,7 +1283,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) })