Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-cross-region-events.mjs
T

149 lines
9.1 KiB
JavaScript

const MODULE = 'phase4-cross-region-events'
const OPTIONS = { scenario: 'phase4-cross-region-events', deploymentBlocker: true }
function id(value) { return value == null ? '' : String(value) }
function records(data) { return Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : [] }
function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
function denied(response) {
return response?.http === 401 || response?.http === 403
|| ['401', '403', 'A0401', 'A0403'].includes(String(response?.body?.code))
}
function traced(response) { return Boolean(response?.requestId) && Boolean(response?.traceId) }
function evidence(response, extra = '') {
return `${extra}${extra ? '; ' : ''}HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
}
function blockingGap(context, name, reason, category, response = null) {
context.addCoverageGap(MODULE, OPTIONS.scenario, name, reason, category, { deploymentBlocker: true })
context.addStep(MODULE, name, response, false, reason, OPTIONS)
throw new Error(`blocking coverage gap: ${name}: ${reason}`)
}
async function poll(request, matches, attempts = 12) {
let last = null
for (let index = 0; index < attempts; index += 1) {
last = await request()
if (matches(last)) return { response: last, attempts: index + 1 }
if (index + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000))
}
return { response: last, attempts }
}
function sameIds(left, right) {
const ids = (value) => records(value).map((item) => id(item?.id)).filter(Boolean).sort()
return JSON.stringify(ids(left)) === JSON.stringify(ids(right))
}
export async function run(context) {
if (!context.token || !context.overseasToken) {
blockingGap(context, '双Token前置', '国内或海外访问令牌缺失,不能执行跨区安全门禁', 'missing-dual-token')
}
const domesticTokenAgainstOverseas = await context.requestOverseas('GET',
'/api/platform/v1/last-leg/orders?page=1&size=1', undefined, {}, context.token)
context.assert(MODULE, '国内Token不能访问海外Audience', domesticTokenAgainstOverseas,
denied(domesticTokenAgainstOverseas) && traced(domesticTokenAgainstOverseas),
evidence(domesticTokenAgainstOverseas), OPTIONS)
const phase3 = context.createdResources?.phase3Loading
if (phase3?.marker !== context.marker || !phase3?.finalizeSucceeded
|| !phase3?.containerBookingId || !phase3?.containerNo || !phase3?.loadingPlanId) {
blockingGap(context, '国内装柜完成事件前置',
'缺少本轮marker的LOADED装柜事实;必须先完成三期装柜链,禁止借用历史事件',
'missing-marker-loading-completed')
}
const overseasTokenAgainstDomestic = await context.request('GET',
`/api/platform/v1/containers/domestic/${encodeURIComponent(id(phase3.containerBookingId))}`,
undefined, context.overseasToken)
context.assert(MODULE, '海外Token不能访问国内Audience', overseasTokenAgainstDomestic,
denied(overseasTokenAgainstDomestic) && traced(overseasTokenAgainstDomestic),
evidence(overseasTokenAgainstDomestic), OPTIONS)
const taillegPath = `/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(phase3.containerNo)}`
const delivered = await poll(
() => context.requestOverseas('GET', taillegPath),
(response) => success(response) && records(response.body?.data)
.some((item) => item?.containerNo === phase3.containerNo))
const deliveredRows = records(delivered.response?.body?.data)
.filter((item) => item?.containerNo === phase3.containerNo)
context.assert(MODULE, '国内装柜完成经Bridge生成Lastleg订单', delivered.response,
deliveredRows.length > 0 && traced(delivered.response),
evidence(delivered.response, `pollRequests=${delivered.attempts}, containerNo=${phase3.containerNo}, orders=${deliveredRows.length}`), OPTIONS)
if (deliveredRows.length === 0) {
blockingGap(context, '国内到海外事件闭环',
`本轮柜号${phase3.containerNo}在轮询后仍无Lastleg订单;需结合requestId/traceId检查Bridge重试与下游诊断`,
'cn-overseas-event-not-delivered', delivered.response)
}
const deliveredAgain = await context.requestOverseas('GET', taillegPath)
context.assert(MODULE, '装柜完成事件重试保持建单幂等', deliveredAgain,
success(deliveredAgain) && sameIds(delivered.response.body?.data, deliveredAgain.body?.data)
&& traced(deliveredAgain),
evidence(deliveredAgain, `firstOrders=${deliveredRows.map((item) => id(item.id)).join(',')}`), OPTIONS)
const overseasPda = context.createdResources?.phase4OverseasPda
if (overseasPda?.marker !== context.marker || overseasPda?.finalStatus !== 'COMPLETED'
|| !overseasPda?.customerId || !overseasPda?.orderId) {
blockingGap(context, '海外柜完成事件前置',
'缺少本轮marker的海外PDA COMPLETED柜事实;必须先完成四期海外PDA闭环',
'missing-marker-overseas-container-completed')
}
const orderDetailPath = `/api/platform/v1/last-leg/orders/${encodeURIComponent(overseasPda.orderId)}?customerId=${encodeURIComponent(overseasPda.customerId)}`
const orderDetail = await context.requestOverseas('GET', orderDetailPath)
const order = orderDetail.body?.data?.order || orderDetail.body?.data
const completedContainerNo = order?.containerNo || ''
context.assert(MODULE, '读取海外完成柜来源尾程单', orderDetail,
success(orderDetail) && Boolean(completedContainerNo) && traced(orderDetail),
evidence(orderDetail, `orderId=${overseasPda.orderId}, containerNo=${completedContainerNo || 'missing'}`), OPTIONS)
if (!completedContainerNo) {
blockingGap(context, '海外完成柜来源身份', '尾程订单未返回containerNo,无法核对国内回写',
'missing-overseas-container-identity', orderDetail)
}
const containerPagePath = `/api/platform/v1/containers/domestic?scope=HISTORY&containerNo=${encodeURIComponent(completedContainerNo)}&page=1&size=20`
const containerPage = await context.request('GET', containerPagePath)
const domesticMatches = records(containerPage.body?.data)
.map((item) => item?.container).filter((item) => item?.containerNo === completedContainerNo)
context.assert(MODULE, '物理柜号唯一定位国内柜聚合', containerPage,
success(containerPage) && domesticMatches.length === 1 && traced(containerPage),
evidence(containerPage, `containerNo=${completedContainerNo}, matches=${domesticMatches.length}`), OPTIONS)
if (domesticMatches.length !== 1) {
blockingGap(context, '海外到国内柜身份映射',
`物理柜号${completedContainerNo}匹配国内柜数量为${domesticMatches.length},无法验证回写`,
'domestic-container-identity-not-unique', containerPage)
}
const overviewPath = `/api/platform/v1/containers/${encodeURIComponent(id(domesticMatches[0].id))}/overview`
const writtenBack = await poll(
() => context.request('GET', overviewPath),
(response) => {
const container = response?.body?.data?.container
return success(response) && Boolean(container?.unloadingCompletedAt)
&& Boolean(container?.unloadingPhotoUploadedAt) && Boolean(container?.estimatedEmptyReportedAt)
})
const snapshot = writtenBack.response?.body?.data?.container || {}
const completedAt = Date.parse(snapshot.unloadingCompletedAt)
const uploadedAt = Date.parse(snapshot.unloadingPhotoUploadedAt)
const estimatedAt = Date.parse(snapshot.estimatedEmptyReportedAt)
const validTimeline = Number.isFinite(completedAt) && Number.isFinite(uploadedAt)
&& Number.isFinite(estimatedAt) && completedAt >= uploadedAt
&& estimatedAt - completedAt === 72 * 60 * 60 * 1_000
context.assert(MODULE, '海外柜完成经Bridge回写Firstleg时间事实', writtenBack.response,
validTimeline && traced(writtenBack.response),
evidence(writtenBack.response,
`pollRequests=${writtenBack.attempts}, completed=${snapshot.unloadingCompletedAt || 'missing'}, photo=${snapshot.unloadingPhotoUploadedAt || 'missing'}, estimatedEmpty=${snapshot.estimatedEmptyReportedAt || 'missing'}`), OPTIONS)
if (!validTimeline) {
blockingGap(context, '海外到国内事件闭环',
`柜号${completedContainerNo}轮询后仍无有效卸柜完成/拍照/预计报空回写;需检查Bridge重试与诊断证据`,
'overseas-cn-event-not-delivered', writtenBack.response)
}
const writtenBackAgain = await context.request('GET', overviewPath)
const replaySnapshot = writtenBackAgain.body?.data?.container || {}
const frozen = ['unloadingCompletedAt', 'unloadingPhotoUploadedAt', 'estimatedEmptyReportedAt',
'actualEmptyReportedAt', 'emptyReportSourceVersion']
context.assert(MODULE, '海外柜完成事件重试保持回写幂等', writtenBackAgain,
success(writtenBackAgain) && frozen.every((field) => (replaySnapshot[field] ?? null) === (snapshot[field] ?? null))
&& traced(writtenBackAgain),
evidence(writtenBackAgain, `containerId=${id(domesticMatches[0].id)}, sourceVersion=${snapshot.emptyReportSourceVersion ?? 'missing'}`), OPTIONS)
}