优化
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { missingRequiredStateFixtures, normalizeStateRequirements } from './manual-state-matrix.mjs'
|
||||
import { normalizeModuleOrder, parseArguments } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
const source = readFileSync(new URL('./manual-state-matrix.mjs', import.meta.url), 'utf8')
|
||||
@@ -11,14 +13,77 @@ test('default full acceptance includes the retained manual state matrix after li
|
||||
assert.ok(entry.indexOf("'manual-state-matrix'") < entry.indexOf("'finance-full-fixtures'"))
|
||||
})
|
||||
|
||||
test('focused matrix execution injects real container lifecycle prerequisites and removes its internal group', () => {
|
||||
const expected = [
|
||||
'merchant-qualification-fixture',
|
||||
'phase3-container-loading',
|
||||
'phase3-loading',
|
||||
'phase3-customs',
|
||||
'phase3-loading-finalize',
|
||||
'manual-state-matrix',
|
||||
]
|
||||
assert.deepEqual(normalizeModuleOrder(['order-inbound-status-fixtures', 'manual-state-matrix']), expected)
|
||||
assert.deepEqual(parseArguments([
|
||||
'--modules', 'order-inbound-status-fixtures,manual-state-matrix',
|
||||
]).modules, expected)
|
||||
})
|
||||
|
||||
test('matrix delegates bounded groups and asserts every declared status has a retained id', () => {
|
||||
for (const group of ['order-inbound-status-fixtures', 'manual-state-loading-pda',
|
||||
'manual-state-ocean-trucking']) assert.match(source, new RegExp(group))
|
||||
assert.doesNotMatch(source, /manual-state-customs-dispatch/)
|
||||
assert.match(source, /createdResources\.manualStateMatrix/)
|
||||
assert.match(source, /supportedStatuses/)
|
||||
assert.match(source, /requiredStatuses/)
|
||||
assert.match(source, /fixtures\?\.\[status\]\?\.id/)
|
||||
assert.match(source, /requiredActions/)
|
||||
assert.match(source, /actions\?\.\[action\]\?\.documentId/)
|
||||
assert.match(source, /actions\?\.\[action\]\?\.completedAt/)
|
||||
})
|
||||
|
||||
test('fixture-unavailable statuses stay supported without becoming hard requirements', () => {
|
||||
const matrix = { pages: {
|
||||
inbounds: { requiredStatuses: ['WAIT_MEASURE', 'WAIT_PRICING', 'FEE_CONFIRMING', 'INBOUNDING', 'VOIDED'], fixtures: {
|
||||
WAIT_MEASURE: { id: '1' },
|
||||
} },
|
||||
unifiedOrders: { requiredStatuses: ['WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED'], fixtures: {
|
||||
WAIT_SPLIT: { id: '2' }, LOADING: { id: '3' }, LOADED: { id: '4' },
|
||||
} },
|
||||
} }
|
||||
|
||||
normalizeStateRequirements(matrix)
|
||||
|
||||
assert.deepEqual(matrix.pages.inbounds.supportedStatuses,
|
||||
['WAIT_MEASURE', 'WAIT_PRICING', 'FEE_CONFIRMING', 'INBOUNDING', 'VOIDED'])
|
||||
assert.deepEqual(matrix.pages.inbounds.requiredStatuses, ['WAIT_MEASURE'])
|
||||
assert.deepEqual(matrix.pages.unifiedOrders.requiredStatuses, ['WAIT_SPLIT', 'LOADING', 'LOADED'])
|
||||
assert.equal(matrix.pages.inbounds.coverageGaps.WAIT_PRICING.category, 'fixture-unavailable')
|
||||
assert.equal(matrix.pages.unifiedOrders.coverageGaps.DELIVERED.category, 'fixture-unavailable')
|
||||
assert.deepEqual(missingRequiredStateFixtures(matrix), [])
|
||||
})
|
||||
|
||||
test('unknown missing status is never swallowed by the known coverage-gap allowlist', () => {
|
||||
const matrix = { pages: {
|
||||
unifiedOrders: { supportedStatuses: ['WAIT_SPLIT', 'UNKNOWN_FUTURE_STATUS'], requiredStatuses: [], fixtures: {
|
||||
WAIT_SPLIT: { id: '1' },
|
||||
} },
|
||||
} }
|
||||
|
||||
normalizeStateRequirements(matrix)
|
||||
|
||||
assert.deepEqual(matrix.pages.unifiedOrders.requiredStatuses, ['WAIT_SPLIT', 'UNKNOWN_FUTURE_STATUS'])
|
||||
assert.deepEqual(missingRequiredStateFixtures(matrix), ['unifiedOrders:UNKNOWN_FUTURE_STATUS'])
|
||||
})
|
||||
|
||||
test('LOADING and LOADED fixtures are resolved through inbound and unified-order details', () => {
|
||||
assert.match(source, /attachUnifiedOrderFixture\(context, matrix, 'phase3ContainerLoading', 'LOADING'\)/)
|
||||
assert.match(source, /attachUnifiedOrderFixture\(context, matrix, 'phase3Loading', 'LOADED'\)/)
|
||||
assert.match(source, /\/api\/platform\/inbounds\/\$\{encodeURIComponent\(inboundId\)\}/)
|
||||
assert.match(source, /id\(inbound\?\.summary\?\.id\) === inboundId/)
|
||||
assert.match(source, /inbound\?\.summary\?\.processStatus === expectedStatus/)
|
||||
assert.match(source, /\/api\/platform\/v1\/orders\/\$\{encodeURIComponent\(orderId\)\}/)
|
||||
assert.match(source, /unified\?\.summary\?\.mainStatus === expectedStatus/)
|
||||
assert.match(source, /matrix\.pages\.unifiedOrders\.fixtures\[expectedStatus\]/)
|
||||
assert.match(source, /clearResolvedProvisionalGap\(context, expectedStatus\)/)
|
||||
assert.match(source, /gap\?\.name === `统一订单列表保留 \$\{status\}`/)
|
||||
})
|
||||
|
||||
@@ -7,7 +7,100 @@ const GROUPS = [
|
||||
'manual-state-ocean-trucking',
|
||||
]
|
||||
|
||||
const FIXTURE_UNAVAILABLE = {
|
||||
inbounds: {
|
||||
WAIT_PRICING: '自动计费会继续推进,无法稳定保留 WAIT_PRICING',
|
||||
FEE_CONFIRMING: '当前无可稳定保留的费用确认前公开生成链路',
|
||||
INBOUNDING: '自动入库在同一调用内继续推进 WAIT_SPLIT,无可暂停 API',
|
||||
VOIDED: '当前 Controller/Service 没有作废入库单的公开 API',
|
||||
},
|
||||
unifiedOrders: {
|
||||
DELIVERED: '当前后端没有完成派送并写入 DELIVERED 的公开业务 API',
|
||||
},
|
||||
}
|
||||
|
||||
function rows(value) { return Array.isArray(value) ? value : [] }
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
|
||||
export function normalizeStateRequirements(matrix) {
|
||||
for (const [page, value] of Object.entries(matrix?.pages || {})) {
|
||||
const supportedStatuses = [...new Set([
|
||||
...rows(value?.supportedStatuses), ...rows(value?.requiredStatuses),
|
||||
])]
|
||||
const unavailable = FIXTURE_UNAVAILABLE[page] || {}
|
||||
value.supportedStatuses = supportedStatuses
|
||||
value.requiredStatuses = supportedStatuses.filter((status) => !unavailable[status])
|
||||
if (Object.keys(unavailable).length > 0) {
|
||||
value.coverageGaps = Object.fromEntries(Object.entries(unavailable).map(([status, reason]) => [status, {
|
||||
category: 'fixture-unavailable', reason,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
return matrix
|
||||
}
|
||||
|
||||
export function missingRequiredStateFixtures(matrix) {
|
||||
return Object.entries(matrix?.pages || {}).flatMap(([page, value]) => rows(value?.requiredStatuses)
|
||||
.filter((status) => !value?.fixtures?.[status]?.id)
|
||||
.map((status) => `${page}:${status}`))
|
||||
}
|
||||
|
||||
function registerFixtureUnavailableGaps(context, matrix) {
|
||||
for (const [page, value] of Object.entries(matrix?.pages || {})) {
|
||||
for (const [status, gap] of Object.entries(value?.coverageGaps || {})) {
|
||||
if (value?.fixtures?.[status]?.id) continue
|
||||
if (rows(context.coverageGaps).some((item) => item?.category === gap.category
|
||||
&& String(item?.name || '').includes(status))) continue
|
||||
context.addCoverageGap?.(MODULE, OPTIONS.scenario, `${page}:${status}`, gap.reason,
|
||||
gap.category, { marker: context.marker, page, status })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearResolvedProvisionalGap(context, status) {
|
||||
if (!Array.isArray(context.coverageGaps)) return
|
||||
for (let index = context.coverageGaps.length - 1; index >= 0; index -= 1) {
|
||||
const gap = context.coverageGaps[index]
|
||||
if (gap?.module === 'order-inbound-status-fixtures'
|
||||
&& gap?.scenario === 'manual-order-inbound-status-matrix'
|
||||
&& gap?.category === 'fixture-unavailable'
|
||||
&& gap?.name === `统一订单列表保留 ${status}`) context.coverageGaps.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function attachUnifiedOrderFixture(context, matrix, resourceName, expectedStatus) {
|
||||
const resource = context.createdResources?.[resourceName]
|
||||
const inboundId = id(resource?.inboundId)
|
||||
const tenantId = id(resource?.tenantId)
|
||||
context.assert(MODULE, `${expectedStatus} 专项资源已生成`, null,
|
||||
Boolean(inboundId && tenantId),
|
||||
`resource=${resourceName}, inboundId=${inboundId || 'missing'}, tenantId=${tenantId || 'missing'}`, OPTIONS)
|
||||
if (!inboundId || !tenantId) return
|
||||
|
||||
const inboundResponse = await context.request('GET',
|
||||
`/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const inbound = context.apiSuccess(MODULE, `读取 ${expectedStatus} 专项入库详情`, inboundResponse, '', OPTIONS)
|
||||
const orderId = id(inbound?.summary?.orderId)
|
||||
context.assert(MODULE, `${expectedStatus} 入库详情身份与状态正确`, inboundResponse,
|
||||
id(inbound?.summary?.id) === inboundId && id(inbound?.summary?.tenantId) === tenantId
|
||||
&& Boolean(orderId) && inbound?.summary?.processStatus === expectedStatus,
|
||||
`inboundId=${id(inbound?.summary?.id) || 'missing'}, orderId=${orderId || 'missing'}, status=${inbound?.summary?.processStatus || 'missing'}`, OPTIONS)
|
||||
if (!orderId || inbound?.summary?.processStatus !== expectedStatus) return
|
||||
|
||||
const unifiedResponse = await context.request('GET',
|
||||
`/api/platform/v1/orders/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const unified = context.apiSuccess(MODULE, `读取 ${expectedStatus} 统一订单详情`, unifiedResponse, '', OPTIONS)
|
||||
const valid = id(unified?.summary?.orderId) === orderId && unified?.summary?.mainStatus === expectedStatus
|
||||
context.assert(MODULE, `${expectedStatus} 统一订单详情可打开`, unifiedResponse, valid,
|
||||
`orderId=${id(unified?.summary?.orderId) || 'missing'}, mainStatus=${unified?.summary?.mainStatus || 'missing'}`, OPTIONS)
|
||||
if (!valid) return
|
||||
|
||||
matrix.pages.unifiedOrders.fixtures[expectedStatus] = {
|
||||
id: orderId, status: expectedStatus, inboundId,
|
||||
detailPath: `/orders/${orderId}`,
|
||||
}
|
||||
clearResolvedProvisionalGap(context, expectedStatus)
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const matrix = { marker: context.marker, generatedAt: new Date().toISOString(), pages: {} }
|
||||
@@ -19,10 +112,13 @@ export async function run(context) {
|
||||
await extension.run(context, matrix)
|
||||
}
|
||||
|
||||
await attachUnifiedOrderFixture(context, matrix, 'phase3ContainerLoading', 'LOADING')
|
||||
await attachUnifiedOrderFixture(context, matrix, 'phase3Loading', 'LOADED')
|
||||
normalizeStateRequirements(matrix)
|
||||
registerFixtureUnavailableGaps(context, matrix)
|
||||
|
||||
const pages = Object.entries(matrix.pages)
|
||||
const missing = pages.flatMap(([page, value]) => rows(value?.requiredStatuses)
|
||||
.filter((status) => !value?.fixtures?.[status]?.id)
|
||||
.map((status) => `${page}:${status}`))
|
||||
const missing = missingRequiredStateFixtures(matrix)
|
||||
const missingActions = pages.flatMap(([page, value]) => rows(value?.requiredActions)
|
||||
.filter((action) => !value?.actions?.[action]?.id
|
||||
|| !value?.actions?.[action]?.documentId || !value?.actions?.[action]?.completedAt)
|
||||
|
||||
@@ -11,6 +11,14 @@ test('retains every ocean booking and truck appointment list status through real
|
||||
'PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED',
|
||||
'COMPLETED','RESCHEDULED','EXCEPTION']) assert.match(source, new RegExp(`['"]${status}['"]`))
|
||||
assert.match(source, /change-request/)
|
||||
const draftPayload = source.slice(source.indexOf('function oceanPayload'), source.indexOf('async function createOcean'))
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
|
||||
assert.doesNotMatch(draftPayload, new RegExp(`${field}:`))
|
||||
}
|
||||
const lifecycle = source.slice(source.indexOf('async function buildOcean'), source.indexOf('function truckPayload'))
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
|
||||
assert.match(lifecycle, new RegExp(`${field}:`))
|
||||
}
|
||||
assert.match(source, /\/reschedule\?plannedLoadingAt=/)
|
||||
assert.match(source, /page=1&size=100&status=/)
|
||||
assert.match(source, /customerId: oceanBooking\.customerId/)
|
||||
|
||||
@@ -17,18 +17,17 @@ async function post(context, path, body, operation) {
|
||||
}
|
||||
|
||||
function oceanPayload(context, template, index) {
|
||||
const value = suffix(context, index); const source = template.oceanBooking
|
||||
const source = template.oceanBooking
|
||||
return { expectedVersion: 0, data: {
|
||||
customerId: source.customerId, customerName: source.customerName,
|
||||
channelCode: source.channelCode, channelName: source.channelName,
|
||||
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
|
||||
billOfLadingNo: `MAN-BL-${value}`, shippingCompanyId: source.shippingCompanyId,
|
||||
shippingCompanyId: source.shippingCompanyId,
|
||||
shippingCompanyName: source.shippingCompanyName, bookingAgentId: source.bookingAgentId,
|
||||
bookingAgentName: source.bookingAgentName, vesselName: `人工验收轮-${value}`, voyageNo: `V${value}`,
|
||||
bookingAgentName: source.bookingAgentName,
|
||||
departurePortCode: source.departurePortCode, departurePortName: source.departurePortName,
|
||||
destinationPortCode: source.destinationPortCode, destinationPortName: source.destinationPortName,
|
||||
placeOfDelivery: source.placeOfDelivery, serviceTerm: source.serviceTerm, freightTerm: source.freightTerm,
|
||||
estimatedSailingAt: '2026-09-15T08:00:00', estimatedArrivalAt: '2026-10-15T08:00:00',
|
||||
remark: `${context.marker} 人工列表状态 ${index}`,
|
||||
parties: template.parties.map(({ partyType, partyName, address, contactName, contactPhone }) =>
|
||||
({ partyType, partyName, address, contactName, contactPhone })),
|
||||
|
||||
@@ -45,7 +45,11 @@ test('FBA 状态样本为订单头和每条货物行提供四个必填标识', (
|
||||
test('测量状态截停发生在正确的 HTTP 边界', () => {
|
||||
assert.match(source, /includes\(summary\.mainStatus\)/)
|
||||
assert.doesNotMatch(source, /includes\(summary\.processStatus\)/)
|
||||
assert.match(source, /target === 'MEASURING' && ok\(response\) && data\(response\)\?\.measurementId/)
|
||||
assert.match(source, /target === 'MEASURING'/)
|
||||
assert.match(source, /\.\/inbound-weight-ocr\.mjs/)
|
||||
assert.match(source, /await weightOcr\.run\(context\)/)
|
||||
assert.match(source, /target !== 'MEASURING'/)
|
||||
assert.doesNotMatch(source, /target === 'MEASURING' && ok\(response\)/)
|
||||
assert.match(source, /target === 'MEASURE_PENDING' && isSubmit/)
|
||||
assert.match(source, /target === 'MEASURE_REJECTED' && isSubmit/)
|
||||
assert.match(source, /measurements\/reject/)
|
||||
|
||||
@@ -174,14 +174,13 @@ async function prepareInboundStatus(context, target) {
|
||||
if (!inboundId || !tenantId) throw new Error(`${target} 未生成独立待测量入库单`)
|
||||
const detailPath = `/api/platform/inbounds/${query(inboundId)}?tenantId=${query(tenantId)}`
|
||||
|
||||
if (target === 'MEASURING') {
|
||||
const weightOcr = await import(new URL('./inbound-weight-ocr.mjs', import.meta.url))
|
||||
await weightOcr.run(context)
|
||||
}
|
||||
|
||||
context.request = async (method, path, body, token, headers) => {
|
||||
const isSubmit = path.includes(`/inbounds/${inboundId}/measurement/submit`)
|
||||
const isMeasurementQuery = method === 'GET' && path.includes(`/inbounds/${inboundId}/measurement?`)
|
||||
if (isMeasurementQuery) {
|
||||
const response = await originalRequest.call(context, method, path, body, token, headers)
|
||||
if (target === 'MEASURING' && ok(response) && data(response)?.measurementId) throw stop
|
||||
return response
|
||||
}
|
||||
if (target === 'MEASURE_PENDING' && isSubmit) {
|
||||
const response = await originalRequest.call(context, method, path, body, token, headers)
|
||||
if (!ok(response)) throw new Error(`MEASURE_PENDING 提交失败: HTTP=${response?.http}, code=${response?.body?.code}`)
|
||||
@@ -201,13 +200,15 @@ async function prepareInboundStatus(context, target) {
|
||||
}
|
||||
return originalRequest.call(context, method, path, body, token, headers)
|
||||
}
|
||||
try {
|
||||
const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url))
|
||||
await measurement.run(context)
|
||||
} catch (error) {
|
||||
if (error !== stop) throw error
|
||||
} finally {
|
||||
context.request = originalRequest
|
||||
if (target !== 'MEASURING') {
|
||||
try {
|
||||
const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url))
|
||||
await measurement.run(context)
|
||||
} catch (error) {
|
||||
if (error !== stop) throw error
|
||||
} finally {
|
||||
context.request = originalRequest
|
||||
}
|
||||
}
|
||||
|
||||
const response = await originalRequest.call(context, 'GET',
|
||||
@@ -232,6 +233,11 @@ async function prepareInboundStatus(context, target) {
|
||||
}
|
||||
|
||||
export async function run(context, matrix = context.createdResources.manualStateMatrix) {
|
||||
if (!matrix) {
|
||||
matrix = { marker: context.marker, generatedAt: new Date().toISOString(), pages: {} }
|
||||
context.createdResources.manualStateMatrix = matrix
|
||||
}
|
||||
matrix.pages ||= {}
|
||||
const loginResponse = await context.request('POST', '/api/auth/login',
|
||||
{ username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD }, '')
|
||||
const token = context.apiSuccess(MODULE, 'IT 测试客户管理员登录', loginResponse)?.accessToken
|
||||
|
||||
Reference in New Issue
Block a user