test: close phase five inventory snapshot coverage

This commit is contained in:
2026-09-02 03:51:07 +08:00
parent a4a4306154
commit 79c390ad58
5 changed files with 59 additions and 6 deletions
+37 -2
View File
@@ -1243,7 +1243,7 @@ export class AcceptanceRun {
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement']
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound',
'phase3LoadingRequireSharedOverflow', 'phase3LoadingIncrementalPackageCount',
'phase3LoadingIncrementalDetailCount', 'orderProfile']
'phase3LoadingIncrementalDetailCount', 'orderProfile', 'measurementPalletOverride']
const createdSnapshot = new Map(createdKeys.map((key) => [key, {
present: Object.hasOwn(this.createdResources, key), value: this.createdResources[key],
}]))
@@ -1268,8 +1268,11 @@ export class AcceptanceRun {
const isolation = requireSharedOverflow
? await this.disableAvailableLocationsForA0(warehouseId, requiredPalletCount)
: { forced: false, disabled: [] }
this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true
this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow
try {
if (requireSharedOverflow) {
this.context.measurementPalletOverride = await this.createPhase3FreshMeasurementPallet(warehouseId)
}
const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url))
await this.executeModule('inbound-measurement-phase3-loading-fixture', () => measurement.run(this))
} finally {
@@ -1297,6 +1300,7 @@ export class AcceptanceRun {
const incrementalMarker = `${originalMarker}-LOAD${suffix}-INC`
this.marker = incrementalMarker
this.context.phase3LoadingRequireSharedOverflow = false
delete this.context.measurementPalletOverride
this.context.phase3LoadingIncrementalPackageCount = 2
this.context.phase3LoadingIncrementalDetailCount = 4
this.context.orderProfile = {
@@ -1462,6 +1466,37 @@ export class AcceptanceRun {
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
}
async createPhase3FreshMeasurementPallet(warehouseId) {
const module = 'phase3-loading-fixture'
const options = { scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true }
const payload = {
warehouseId, palletType: 'STANDARD', capacityCbm: 1.5, weightKg: 25,
color: 'ORANGE', remark: `${this.marker}-inventory-snapshot-a0`,
}
const createResponse = await this.request('POST', '/api/platform/v1/pallets?tenantId=0', payload)
const palletId = String(this.apiSuccess(module, '创建本轮库存快照专用称重托盘', createResponse,
`warehouseId=${warehouseId}`, options) || '')
if (!palletId) return null
const detailResponse = await this.request('GET',
`/api/platform/v1/pallets/${encodeURIComponent(palletId)}?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`)
const pallet = this.apiSuccess(module, '回读本轮库存快照专用称重托盘', detailResponse,
`palletId=${palletId}`, options)
const valid = String(pallet?.id || '') === palletId
&& String(pallet?.warehouseId || '') === String(warehouseId)
&& Boolean(String(pallet?.palletCode || '').trim())
this.assert(module, '本轮专用称重托盘尚未复用历史占用身份', detailResponse, valid,
`palletId=${palletId}, palletCode=${pallet?.palletCode || 'missing'}`, options)
if (!valid) return null
const fixture = {
id: palletId, code: String(pallet.palletCode), warehouseId: String(warehouseId),
marker: this.marker, retainedReason: '测量审核后成为正式库存占用,保留为开发验收证据',
}
this.createdResources.phase3LoadingFreshMeasurementPallet = fixture
return fixture
}
async listAvailableLocationsForA0(warehouseId, module, options) {
const locations = []
const seen = new Set()
+1 -1
View File
@@ -117,7 +117,7 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s
其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;订舱放舱也不创建真实货柜或 allocation。港口取得真实柜号后,四字段 link 直接补全原模拟柜并创建 allocation,原 `containerBookingId/cabinetNo/loadingPlan/items` 不变;未关联、订舱未 `RELEASED` 或整份计划未确认时,PDA 查询必须返回业务码 `A0404`。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。
计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。完整三期装柜 fixture 通过平台托盘 API 创建本轮专用称重托盘,再交给通用测量模块;当普通位不足时由 Warehouse 自然分配到 A0,随后仅使用公开 A0 换绑接口制造库存版本变化,不直连数据库或调用内部 Warehouse 接口。主三期链强制完成该专项,四期生产矩阵的附加柜只负责跨区生产,不重复要求 A0;附加柜若自然落入 A0 仍执行同一验证。批量待分柜入口仍只复用 `TP-001`/`YW-TP-002`,不创建托盘。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
`phase3-pda-convergence` 同时回查 `/tasks/pending` 与稳定 task detail:两者的 `workflowStatus`、`itemCount`、`pendingItemCount` 必须来自同一任务明细事实。全拣任务必须在两处均为 `AWAITING_REVIEW`,可执行证据 GET,但派生 `actions=[]`,不得重复发送 batch;操作历史必须恰有一次有效 `BATCH`。若 pending 仍按 assignment 返回 `IN_PROGRESS`,该轮以真实 HTTP 失败阻断,不由脚本或前端改写状态。
@@ -30,6 +30,13 @@ function validPdf(response) {
async function reusablePallets(context, warehouseId) {
const cacheKey = asId(warehouseId)
const override = context.context?.measurementPalletOverride
if (asId(override?.warehouseId) === cacheKey && asId(override?.id)
&& String(override?.code || '').trim()) {
context.assert(MODULE, '使用编排层提供的本轮专用称重托盘', null, true,
`warehouseId=${warehouseId}, palletId=${asId(override.id)}`, OPTIONS)
return [{ id: asId(override.id), code: String(override.code) }]
}
const allowAnyExistingPallet = context.context?.allowAnyExistingPallet === true
const scopedCacheKey = `${cacheKey}:${allowAnyExistingPallet ? 'any' : 'fixed'}`
if (reusablePalletsByWarehouse.has(scopedCacheKey)) return reusablePalletsByWarehouse.get(scopedCacheKey)
@@ -185,7 +192,7 @@ async function provisionWarehouseResources(context, { inboundId, tenantId, wareh
context.createdResources.inboundWarehouseResources ??= {}
context.createdResources.inboundWarehouseResources[inboundId] = {
marker: context.marker, tenantId: '0', warehouseId: asId(warehouseId), palletCount,
reusablePalletCodes: REUSABLE_PALLET_CODES, resources,
reusablePalletCodes: candidates.map((item) => item.code), resources,
}
return { palletId: palletIds[0], palletIds, palletCount, resources }
}
@@ -760,12 +760,15 @@ export async function run(context) {
if (refreshedInventory?.snapshotToken) {
effectiveBatchPayload = { ...batchPayload, inventorySnapshotToken: refreshedInventory.snapshotToken }
}
} else {
} else if (context.context?.phase3LoadingRequireSharedOverflow === true) {
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,
'主三期装柜链已独立强制验证;当前变体仅生成四期跨区生产者', OPTIONS)
}
const canonicalPick = await request(context, 'POST', batchPath, effectiveBatchPayload,
@@ -546,18 +546,26 @@ test('phase3 loading fixture paginates A0 candidates and refuses mass location m
const orders = source('./orders.mjs')
assertOrder(entry, [
'disableAvailableLocationsForA0(warehouseId, requiredPalletCount)',
'this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true',
'this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow',
'createPhase3FreshMeasurementPallet(warehouseId)',
"executeModule('inbound-measurement-phase3-loading-fixture'",
'restoreAvailableLocationsAfterA0(warehouseId, isolation)',
])
assert.match(entry, /page=\$\{pageNumber\}&size=\$\{A0_LOCATION_PAGE_SIZE\}/)
assert.match(entry, /A0_MAX_LOCATION_MUTATIONS = 8/)
assert.match(entry, /A0 大仓保护:不批量停用普通库位/)
assert.match(entry, /创建本轮库存快照专用称重托盘/)
assert.match(entry, /\/api\/platform\/v1\/pallets\?tenantId=0/)
assert.match(entry, /measurementPalletOverride/)
assert.doesNotMatch(entry, /internal\/warehouse.*inventory-snapshot-a0/)
assert.match(entry, /remainingNormal\.length < isolation\.requiredSlots/)
assert.match(entry, /\/locations\/\$\{encodeURIComponent\(location\.id\)\}\/status/)
assert.match(entry, /\{ status: 'DISABLED', version: Number\(location\.version\) \}/)
assert.match(entry, /\{ status: 'AVAILABLE', version: location\.version \}/)
assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/)
const loading = source('./phase3-loading.mjs')
assert.match(loading, /context\.context\?\.phase3LoadingRequireSharedOverflow === true/)
assert.match(loading, /附加生产者不重复要求 A0 库存快照变更专项/)
})
test('customs reuses the loading chain released booking without pre-completing truck', () => {