完成单机TEST统一入口与可靠投递回归收尾

This commit is contained in:
2026-09-29 19:48:34 +08:00
parent 64a70c9f28
commit 6c0a026cc4
89 changed files with 968 additions and 983 deletions
@@ -59,20 +59,19 @@ function blockerGap(context, name, reason, category = 'fixture-unavailable', res
}
async function pda(context, token, method, name, path, body = undefined) {
const response = await context.requestOverseas(method, path, body, {}, token)
const response = await context.request(method, path, body, token, {})
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
return { response, data: success(response) ? response.body?.data : null }
}
async function expected(context, token, method, name, path, body, predicate) {
const response = await context.requestOverseas(method, path, body, {}, token)
const response = await context.request(method, path, body, token, {})
context.addStep(MODULE, name, response, predicate(response), trace(response), OPTIONS)
return response
}
async function captureCompletedOrder(context, containerNo, expectedCustomerId) {
const response = await context.requestOverseas('GET',
`/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`)
const response = await context.request('GET', `/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`)
const candidates = records(response?.body?.data)
.filter((item) => id(item?.containerNo) === id(containerNo))
const exactCustomer = expectedCustomerId == null ? candidates
@@ -85,7 +84,7 @@ async function captureCompletedOrder(context, containerNo, expectedCustomerId) {
context.createdResources.phase4OverseasPda.orderId = id(order.id)
context.createdResources.phase4OverseasPda.customerId = id(order.customerId)
const detailPath = `/api/platform/v1/last-leg/orders/${encodeURIComponent(id(order.id))}?customerId=${encodeURIComponent(id(order.customerId))}`
const detail = await context.requestOverseas('GET', detailPath)
const detail = await context.request('GET', detailPath)
context.assert(MODULE, '完成柜回查尾程订单货物明细供后续PDA作业', detail,
success(detail) && id(detail.body?.data?.order?.id) === id(order.id)
&& Array.isArray(detail.body?.data?.cargo) && detail.body.data.cargo.length > 0,
@@ -102,10 +101,10 @@ async function loginMerchantForNegativeProbe(context) {
if (!username || !password) return null
const response = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const data = success(response) ? response.body?.data : null
context.addStep(MODULE, '本轮客户登录取得海外令牌用于PDA越权反证', response,
success(response) && nonBlank(data?.overseasAccessToken),
`overseasTokenPresent=${nonBlank(data?.overseasAccessToken)}; ${trace(response)}`, OPTIONS)
return nonBlank(data?.overseasAccessToken) ? data.overseasAccessToken : null
context.addStep(MODULE, '本轮客户登录取得访问令牌用于PDA越权反证', response,
success(response) && nonBlank(data?.accessToken),
`tokenPresent=${nonBlank(data?.accessToken)}; ${trace(response)}`, OPTIONS)
return nonBlank(data?.accessToken) ? data.accessToken : null
}
export async function resolvePickerAccess(context) {
@@ -144,17 +143,16 @@ export async function createPicker(context, index, overseasWarehouseId, access)
`membershipId=${membershipId || 'missing'}, tenantId=${id(created?.tenantId) || 'missing'}, overseasWarehouseId=${id(created?.overseasWarehouseId) || 'missing'}`, OPTIONS)
if (!longId(membershipId)) return null
const login = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const token = success(login) ? login.body?.data?.overseasAccessToken : null
context.addStep(MODULE, `海外PDA操作员${index}登录取得双区域海外令牌`, login, nonBlank(token),
`overseasTokenPresent=${nonBlank(token)}; ${trace(login)}`, OPTIONS)
const token = success(login) ? login.body?.data?.accessToken : null
context.addStep(MODULE, `海外PDA操作员${index}登录取得统一访问令牌`, login, nonBlank(token),
`tokenPresent=${nonBlank(token)}; ${trace(login)}`, OPTIONS)
if (!nonBlank(token)) return { membershipId, username, token: null }
let projectionResponse = null
let projectionReady = false
let projectionAttempt = 0
for (; projectionAttempt < 30; projectionAttempt += 1) {
projectionResponse = await context.requestOverseas('GET',
'/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token)
projectionResponse = await context.request('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, token, {})
projectionReady = success(projectionResponse) || notFound(projectionResponse)
if (projectionReady || !forbidden(projectionResponse)) break
await new Promise((resolve) => setTimeout(resolve, 500))
@@ -193,12 +191,10 @@ async function resolveGeneratedPallet(context, inbound) {
const pallet = candidates[0]
if (!pallet || !longId(pallet.palletId) || !nonBlank(pallet.palletCode)) return null
const warehouseId = id(inbound?.overseasWarehouseId || pallet.warehouseId)
const listed = await context.requestOverseas('GET',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets?page=1&size=10&keyword=${encodeURIComponent(pallet.palletCode)}`)
const listed = await context.request('GET', `/api/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets?page=1&size=10&keyword=${encodeURIComponent(pallet.palletCode)}`)
const stored = records(listed?.body?.data).find((row) => id(row?.id) === pallet.palletId && row?.palletCode === pallet.palletCode)
const locations = stored && longId(stored?.locationId)
? await context.requestOverseas('GET',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations?page=1&size=200`)
? await context.request('GET', `/api/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/locations?page=1&size=200`)
: null
const location = records(locations?.body?.data).find((row) => id(row?.id) === id(stored?.locationId))
const resolved = success(listed) && success(locations) && stored && longId(stored?.locationId) && nonBlank(location?.locationCode)
@@ -263,14 +259,14 @@ async function verifyWarehouseQueries(context, token, inboundCode, pallet, desti
}
export async function uploadPhotos(context, token) {
const malicious = await context.requestOverseasMultipart('POST', '/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files', {
const malicious = await context.requestMultipart('POST', '/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files', {
name: `${context.marker}-forged.png`, type: 'image/png', content: Uint8Array.from(Buffer.from('<script>alert(1)</script>')),
}, token)
context.addStep(MODULE, '伪造图片内容安全扫描返回A0400', malicious,
malicious?.http === 200 && code(malicious) === 'A0400' && traced(malicious), trace(malicious), OPTIONS)
const photos = []
for (const role of PHOTO_ROLES) {
const response = await context.requestOverseasMultipart('POST', '/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files', {
const response = await context.requestMultipart('POST', '/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files', {
name: `${context.marker}-${role.toLowerCase()}.png`, type: 'image/png', content: PNG,
}, token)
const fileId = id(response?.body?.data?.fileId)
@@ -290,8 +286,7 @@ async function awaitProducerConsumption(context, producers) {
for (let attempt = 1; attempt <= 90; attempt += 1) {
const missing = expected.filter((containerNo) => !consumed.has(containerNo))
const results = await Promise.all(missing.map(async (containerNo) => {
const response = await context.requestOverseas('GET',
`/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`)
const response = await context.request('GET', `/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`)
return { containerNo, response }
}))
for (const { containerNo, response } of results) {
@@ -316,9 +311,7 @@ async function awaitProducerConsumption(context, producers) {
async function convertReceivingContainers(context, producers) {
for (const producer of producers) {
const containerNo = id(producer?.containerNo)
const summaryResponse = await context.requestOverseas('GET',
`/api/platform/v1/last-leg/orders/container-summaries?page=1&size=20&containerNo=${encodeURIComponent(containerNo)}`,
undefined, {}, context.overseasToken)
const summaryResponse = await context.request('GET', `/api/platform/v1/last-leg/orders/container-summaries?page=1&size=20&containerNo=${encodeURIComponent(containerNo)}`, undefined, context.token, {})
const summaries = success(summaryResponse) && Array.isArray(summaryResponse.body?.data?.records)
? summaryResponse.body.data.records : []
const summary = summaries.find((item) => id(item?.containerNo) === containerNo)
@@ -336,9 +329,7 @@ async function convertReceivingContainers(context, producers) {
`sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, allPlansReady=${summary?.allPlansReady === true}, orders=${totalOrders}, inboundCodes=${inboundCodes}, plans=${existingPlans}, pending=${pendingPlans}; ${trace(summaryResponse)}`, OPTIONS)
if (!convertible) return false
const bookingId = id(summary.sourceContainerBookingId)
const arrivalQueryResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/orders/container-arrival-times/query',
{ sourceContainerBookingIds: [bookingId] }, {}, context.overseasToken)
const arrivalQueryResponse = await context.request('POST', '/api/platform/v1/last-leg/orders/container-arrival-times/query', { sourceContainerBookingIds: [bookingId] }, context.token, {})
const arrivalTimes = success(arrivalQueryResponse) && Array.isArray(arrivalQueryResponse.body?.data)
? arrivalQueryResponse.body.data : []
let arrivalTime = arrivalTimes.find((item) => id(item?.sourceContainerBookingId) === bookingId)
@@ -349,12 +340,8 @@ async function convertReceivingContainers(context, producers) {
if (!arrivalTime) return false
if (!nonBlank(arrivalTime.estimatedWarehouseArrivalAt)) {
const estimatedWarehouseArrivalAt = new Date(Date.now() + 2 * 86400000).toISOString().slice(0, 19)
const arrivalUpdateResponse = await context.requestOverseas('PUT',
`/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`,
{ estimatedWarehouseArrivalAt, expectedVersion: Number(arrivalTime.version),
remark: `${context.marker}-overseas-inbound-plan-prerequisite` },
{ 'Idempotency-Key': `${context.marker}-expected-arrival-${containerNo}`.slice(0, 128) },
context.overseasToken)
const arrivalUpdateResponse = await context.request('PUT', `/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`, { estimatedWarehouseArrivalAt, expectedVersion: Number(arrivalTime.version),
remark: `${context.marker}-overseas-inbound-plan-prerequisite` }, context.token, { 'Idempotency-Key': `${context.marker}-expected-arrival-${containerNo}`.slice(0, 128) })
arrivalTime = success(arrivalUpdateResponse) ? arrivalUpdateResponse.body?.data : null
context.addStep(MODULE, `维护整柜预计到仓时间 ${containerNo}`, arrivalUpdateResponse,
id(arrivalTime?.sourceContainerBookingId) === bookingId
@@ -364,8 +351,7 @@ async function convertReceivingContainers(context, producers) {
if (!arrivalTime || !nonBlank(arrivalTime.estimatedWarehouseArrivalAt)) return false
}
const previewPath = `/api/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${encodeURIComponent(bookingId)}`
const previewResponse = await context.requestOverseas('GET', previewPath,
undefined, {}, context.overseasToken)
const previewResponse = await context.request('GET', previewPath, undefined, context.token, {})
const preview = success(previewResponse) ? previewResponse.body?.data : null
context.addStep(MODULE, `读取PDA前柜级计划与自动托盘预览 ${containerNo}`, previewResponse,
preview?.ready === true && Number(preview?.totalOrderCount) === totalOrders
@@ -377,9 +363,7 @@ async function convertReceivingContainers(context, producers) {
const body = { sourceContainerBookingId: bookingId,
snapshotVersion: Number(preview.snapshotVersion), snapshotHash: String(preview.snapshotHash) }
const key = `${context.marker}-pda-convert-${containerNo}`.slice(0, 128)
const convertedResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
const convertedResponse = await context.request('POST', '/api/platform/v1/last-leg/inbound-plans/convert-container', body, context.token, { 'Idempotency-Key': key })
const plans = success(convertedResponse) && Array.isArray(convertedResponse.body?.data)
? convertedResponse.body.data : []
const convertedInboundCodes = new Set(plans
@@ -412,9 +396,7 @@ async function convertReceivingContainers(context, producers) {
OPTIONS)
}
rememberGeneratedPlans(context, plans)
const replayResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
const replayResponse = await context.request('POST', '/api/platform/v1/last-leg/inbound-plans/convert-container', body, context.token, { 'Idempotency-Key': key })
context.addStep(MODULE, `整柜转换幂等重放零新增 ${containerNo}`, replayResponse,
success(replayResponse) && JSON.stringify(replayResponse.body?.data) === JSON.stringify(plans),
trace(replayResponse), OPTIONS)
@@ -428,9 +410,7 @@ async function convertReceivingContainers(context, producers) {
`plans=${existingPlans}, plannedPallets=${preview?.existingPlannedPalletCount ?? 'missing'}; ${trace(previewResponse)}`,
OPTIONS)
}
const progressResponse = await context.requestOverseas('GET',
`/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(summary.sourceContainerBookingId)}`,
undefined, {}, context.overseasToken)
const progressResponse = await context.request('GET', `/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(summary.sourceContainerBookingId)}`, undefined, context.token, {})
const progress = success(progressResponse) ? progressResponse.body?.data?.summary : null
context.addStep(MODULE, `PDA前整柜计划完整度闭环 ${containerNo}`, progressResponse,
progress?.allPlansReady === true && Number(progress?.planCount) === totalOrders
@@ -452,8 +432,7 @@ async function awaitPickupReadyCandidates(context, token, producers, allProducer
let response = null
let candidatesByContainer = new Map()
for (let attempt = 1; attempt <= 30; attempt += 1) {
response = await context.requestOverseas('GET',
'/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, token)
response = await context.request('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, token, {})
const candidateRows = success(response) && Array.isArray(response.body?.data) ? response.body.data : []
candidatesByContainer = new Map(candidateRows
.map((item) => [id(item?.containerNo), item]).filter(([containerNo]) => nonBlank(containerNo)))
@@ -499,8 +478,7 @@ export async function resolveCartonFixtures(context, token, inbounds, producerCa
}
const cartonFixtures = []
for (const [index, cartonMark] of cartonCandidates.entries()) {
const response = await context.requestOverseas('POST',
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }, {}, token)
const response = await context.request('POST', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }, token, {})
if (!strictDropship && overseasCartons.includes(cartonMark)
&& a0404Message(response, /^Carton mark was not found$/)) {
context.addStep(MODULE, `排除未实装箱码候选${index + 1}`, response, true,
@@ -669,15 +647,15 @@ async function receiveDropship(context, token, producer, inbounds) {
}
async function runReceiving(context, token, secondaryToken, merchantToken, preferredProducer = null, runAuthorizationProbes = true) {
if (runAuthorizationProbes && context.overseasToken) {
const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, context.overseasToken)
context.addStep(MODULE, '平台管理员海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
if (runAuthorizationProbes && context.token) {
const denied = await context.request('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, context.token, {})
context.addStep(MODULE, '平台管理员访问令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
}
if (runAuthorizationProbes && merchantToken) {
const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, merchantToken)
context.addStep(MODULE, '客户海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
const denied = await context.request('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, merchantToken, {})
context.addStep(MODULE, '客户访问令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
}
const before = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token)
const before = await context.request('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, token, {})
const noCurrent = notFound(before)
context.addStep(MODULE, '无当前柜返回404属于正常空态', before, noCurrent || success(before),
`${noCurrent ? 'empty=true' : `resumeStatus=${before?.body?.data?.status || 'missing'}`}; ${trace(before)}`, OPTIONS)
@@ -848,7 +826,7 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
const completeReplay = await pda(context, token, 'POST', '完成当前柜同键同载荷重放', '/api/pda/v1/last-leg/receiving-containers/current/complete', completeBody)
context.assert(MODULE, '完成幂等重放仍返回COMPLETED', completeReplay.response,
success(completeReplay.response) && completeReplay.data?.status === 'COMPLETED', trace(completeReplay.response), OPTIONS)
const after = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token)
const after = await context.request('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, token, {})
context.addStep(MODULE, '完成后current返回404正常空态', after, notFound(after), trace(after), OPTIONS)
context.createdResources.phase4OverseasPda.finalStatus = completed.data?.status || null
if (context.createdResources.phase4OverseasPda.finalStatus === 'COMPLETED') {