Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
3 changed files with 146 additions and 28 deletions
Showing only changes of commit 2c3efad70a - Show all commits
+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,
}
}
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { planA0LocationIsolation } from '../admin-test-env-acceptance.mjs'
function locations(count) {
return Array.from({ length: count }, (_, index) => ({
id: String(index + 1), locationCode: `L-${String(index + 1).padStart(4, '0')}`,
sharedOverflow: false, version: 0,
}))
}
test('large warehouse is never selected for default mass disable', () => {
const plan = planA0LocationIsolation(locations(614), 1)
assert.equal(plan.enabled, false)
assert.equal(plan.mutationsRequired, 614)
assert.deepEqual(plan.targets, [])
})
test('minimum mutations leave fewer normal locations than required pallets', () => {
const plan = planA0LocationIsolation(locations(3), 3)
assert.equal(plan.enabled, true)
assert.equal(plan.mutationsRequired, 1)
assert.equal(plan.targets.length, 1)
assert.equal(plan.expectedRemainingNormal, 2)
assert.ok(plan.expectedRemainingNormal < plan.requiredSlots)
})
test('natural shortage proves A0 fallback without any location mutation', () => {
const plan = planA0LocationIsolation(locations(1), 2)
assert.equal(plan.enabled, true)
assert.equal(plan.mutationsRequired, 0)
assert.deepEqual(plan.targets, [])
assert.ok(plan.normalCount < plan.requiredSlots)
})
test('shared A0 and duplicate rows never inflate the normal location plan', () => {
const normal = locations(2)
const plan = planA0LocationIsolation([
...normal, normal[0], { id: 'A0', locationCode: 'A0', sharedOverflow: true, version: 0 },
], 2)
assert.equal(plan.normalCount, 2)
assert.equal(plan.mutationsRequired, 1)
assert.deepEqual(plan.targets.map((item) => item.id), ['1'])
})
@@ -511,20 +511,22 @@ test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstrea
assert.doesNotMatch(loading, /if \(Number\(dynamicCandidate\?\.availablePackageCount\) < 2\) return/)
})
test('phase3 loading fixture forces A0 through reversible public location status APIs', () => {
test('phase3 loading fixture paginates A0 candidates and refuses mass location mutations', () => {
const entry = source('../admin-test-env-acceptance.mjs')
const measurement = source('./inbound-measurement.mjs')
const orders = source('./orders.mjs')
assertOrder(entry, [
'disableAvailableLocationsForA0(warehouseId)',
'this.context.phase3LoadingRequireSharedOverflow = true',
'disableAvailableLocationsForA0(warehouseId, requiredPalletCount)',
'this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true',
"executeModule('inbound-measurement-phase3-loading-fixture'",
'restoreAvailableLocationsAfterA0(warehouseId, disabledLocations)',
'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, /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(measurement, /phase3LoadingRequireSharedOverflow !== true/)
assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/)
})