test: bound A0 location isolation writes

This commit is contained in:
2026-09-01 10:18:19 +08:00
parent cdcf3e0f4f
commit 2c3efad70a
3 changed files with 146 additions and 28 deletions
+93 -22
View File
@@ -13,6 +13,33 @@ const OPTIONAL_MODULES = ['manual-order-splitting-fixture', 'manual-order-splitt
'manual-loading-complete-fixture', 'manual-loading-complete-verify',
'coload-groups']
const AVAILABLE_MODULES = new Set([...CONFIGURED_MODULES, ...OPTIONAL_MODULES])
const A0_LOCATION_PAGE_SIZE = 200
const A0_MAX_LOCATION_MUTATIONS = 8
export function planA0LocationIsolation(locations, requiredSlots = 1,
maxMutations = A0_MAX_LOCATION_MUTATIONS) {
const slots = Math.max(1, Number.isInteger(Number(requiredSlots)) ? Number(requiredSlots) : 1)
const limit = Math.max(0, Number.isInteger(Number(maxMutations)) ? Number(maxMutations) : 0)
const unique = new Map()
for (const location of Array.isArray(locations) ? locations : []) {
const locationId = location?.id == null ? '' : String(location.id)
if (!locationId || location?.sharedOverflow === true || unique.has(locationId)) continue
unique.set(locationId, location)
}
const normal = [...unique.values()].sort((left, right) =>
String(left?.locationCode || '').localeCompare(String(right?.locationCode || ''))
|| String(left.id).localeCompare(String(right.id)))
const remainingNormal = Math.max(0, slots - 1)
const mutationsRequired = Math.max(0, normal.length - remainingNormal)
return {
enabled: mutationsRequired <= limit,
requiredSlots: slots,
normalCount: normal.length,
expectedRemainingNormal: Math.min(normal.length, remainingNormal),
mutationsRequired,
targets: mutationsRequired <= limit ? normal.slice(0, mutationsRequired) : [],
}
}
const MODULE_DEPENDENCIES = new Map([
['suppliers', ['merchant-qualification-fixture']],
@@ -1236,15 +1263,17 @@ export class AcceptanceRun {
this.assert('phase3-loading-fixture', 'A0 隔离使用本轮订单收货仓', null,
Boolean(warehouseId), `warehouseId=${warehouseId || 'missing'}`,
{ scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true })
const disabledLocations = requireSharedOverflow
? await this.disableAvailableLocationsForA0(warehouseId) : []
this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow
const requiredPalletCount = Math.max(1, Number(this.context?.waitSplitVariant?.palletCount || 1))
const isolation = requireSharedOverflow
? await this.disableAvailableLocationsForA0(warehouseId, requiredPalletCount)
: { forced: false, disabled: [] }
this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true
try {
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 {
if (requireSharedOverflow) {
await this.restoreAvailableLocationsAfterA0(warehouseId, disabledLocations)
await this.restoreAvailableLocationsAfterA0(warehouseId, isolation)
}
}
const fixture = this.createdResources.inboundMeasurement || this.createdResources.orders
@@ -1430,24 +1459,60 @@ export class AcceptanceRun {
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
}
async disableAvailableLocationsForA0(warehouseId) {
async listAvailableLocationsForA0(warehouseId, module, options) {
const locations = []
const seen = new Set()
let expectedTotal = null
for (let pageNumber = 1; pageNumber <= 100; pageNumber += 1) {
const response = await this.request('GET',
`/api/platform/v1/locations?tenantId=0&page=${pageNumber}&size=${A0_LOCATION_PAGE_SIZE}&warehouseId=${encodeURIComponent(warehouseId)}&status=AVAILABLE`)
const page = this.apiSuccess(module, `查询 A0 隔离前可用库位第 ${pageNumber} 页`, response,
`page=${pageNumber}`, options)
if (!page) return null
const pageTotal = Number(page.total)
const records = Array.isArray(page.records) ? page.records : []
if (!Number.isInteger(pageTotal) || pageTotal < 0
|| (expectedTotal !== null && pageTotal !== expectedTotal)) {
this.assert(module, 'A0 隔离库位分页总数稳定', response, false,
`page=${pageNumber}, firstTotal=${expectedTotal ?? 'missing'}, currentTotal=${page.total ?? 'missing'}`, options)
return null
}
expectedTotal ??= pageTotal
for (const location of records) {
const locationId = location?.id == null ? '' : String(location.id)
if (locationId && !seen.has(locationId)) {
seen.add(locationId)
locations.push(location)
}
}
if (locations.length >= expectedTotal) break
if (records.length === 0) break
}
const complete = locations.length === expectedTotal
this.assert(module, 'A0 隔离库位分页完整且无重复', null, complete,
`total=${expectedTotal ?? 'missing'}, uniqueRecords=${locations.length}`, options)
return complete ? locations : null
}
async disableAvailableLocationsForA0(warehouseId, requiredSlots = 1) {
const module = 'phase3-loading-fixture'
const options = { scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true }
if (!warehouseId) {
this.assert(module, 'A0 隔离收货仓 ID 完整', null, false, 'warehouseId=missing', options)
return []
return { forced: false, disabled: [] }
}
const locations = await this.listAvailableLocationsForA0(warehouseId, module, options)
if (!locations) return { forced: false, disabled: [] }
const isolation = planA0LocationIsolation(locations, requiredSlots)
if (!isolation.enabled) {
this.addStep(module, 'A0 大仓保护:不批量停用普通库位', null, true,
`normal=${isolation.normalCount}, requiredSlots=${isolation.requiredSlots}, mutationsRequired=${isolation.mutationsRequired}, safetyLimit=${A0_MAX_LOCATION_MUTATIONS}`,
{ scenario: options.scenario })
return { forced: false, disabled: [], isolation }
}
const pageResponse = await this.request('GET',
`/api/platform/v1/locations?tenantId=0&page=1&size=200&warehouseId=${encodeURIComponent(warehouseId)}&status=AVAILABLE`)
const page = this.apiSuccess(module, '查询 A0 隔离前全部可用普通库位', pageResponse, '', options)
const records = Array.isArray(page?.records) ? page.records : []
const locations = records.filter((item) => item.sharedOverflow !== true)
this.assert(module, 'A0 隔离库位分页完整', pageResponse,
Number(page?.total) === records.length && records.length < 200,
`total=${page?.total ?? 'missing'}, records=${records.length}, normal=${locations.length}`, options)
const disabled = []
const skippedOccupied = []
for (const location of locations) {
for (const location of isolation.targets) {
const response = await this.request('PATCH',
`/api/platform/v1/locations/${encodeURIComponent(location.id)}/status?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`,
{ status: 'DISABLED', version: Number(location.version) })
@@ -1462,16 +1527,19 @@ export class AcceptanceRun {
}
const availableResponse = await this.request('GET',
`/api/platform/v1/locations/available?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}&limit=200`)
const available = this.apiSuccess(module, '停用后回查收货仓无可用普通库位', availableResponse, '', options)
this.assert(module, '测量前 A0 溢出条件成立', availableResponse,
Array.isArray(available) && available.length === 0,
`disabled=${disabled.length}, skippedOccupied=${skippedOccupied.map((item) => `${item.code}:${item.id}`).join('|') || 'none'}, available=${Array.isArray(available) ? available.length : 'missing'}`, options)
return disabled
const available = this.apiSuccess(module, '停用后回查收货仓剩余普通库位', availableResponse, '', options)
const remainingNormal = Array.isArray(available)
? available.filter((item) => item?.sharedOverflow !== true) : null
const forced = Array.isArray(remainingNormal) && remainingNormal.length < isolation.requiredSlots
this.assert(module, '测量前 A0 溢出条件成立', availableResponse, forced,
`requiredSlots=${isolation.requiredSlots}, disabled=${disabled.length}, skippedOccupied=${skippedOccupied.map((item) => `${item.code}:${item.id}`).join('|') || 'none'}, remainingNormal=${Array.isArray(remainingNormal) ? remainingNormal.length : 'missing'}`, options)
return { forced, disabled, isolation }
}
async restoreAvailableLocationsAfterA0(warehouseId, disabledLocations) {
async restoreAvailableLocationsAfterA0(warehouseId, result) {
const module = 'phase3-loading-fixture'
const options = { scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true }
const disabledLocations = Array.isArray(result?.disabled) ? result.disabled : []
for (const location of disabledLocations) {
const response = await this.request('PATCH',
`/api/platform/v1/locations/${encodeURIComponent(location.id)}/status?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`,
@@ -1481,7 +1549,10 @@ export class AcceptanceRun {
`locationId=${location.id}, restoreVersion=${location.version}`, options)
}
this.createdResources.phase3LoadingA0Isolation = {
warehouseId, restored: disabledLocations.map((item) => item.id), marker: this.marker,
warehouseId, forced: result?.forced === true,
normalCount: result?.isolation?.normalCount ?? null,
mutationsRequired: result?.isolation?.mutationsRequired ?? 0,
restored: disabledLocations.map((item) => item.id), marker: this.marker,
}
}