69 lines
5.6 KiB
JavaScript
69 lines
5.6 KiB
JavaScript
const MODULE = 'phase3-pda-convergence'
|
|
const OPTIONS = { scenario: 'pda-picking-convergence', deploymentBlocker: true }
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
|
function failure(response) { return [400, 401, 403, 404, 409, 503].includes(response?.http) || (response?.http === 200 && codeOf(response) !== '200') }
|
|
async function request(context, method, path, body, headers = {}) {
|
|
try {
|
|
const response = await fetch(`${context.options.gateway}${path}`, { method,
|
|
headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
|
...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers },
|
|
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000) })
|
|
const raw = await response.text(); let payload = null
|
|
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
|
|
return { method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null,
|
|
requestId: payload?.requestId || response.headers.get('x-request-id'), traceId: payload?.traceId || response.headers.get('x-trace-id') }
|
|
} catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
|
|
}
|
|
export async function run(context) {
|
|
const fixture = context.createdResources?.phase3Loading
|
|
const tenantId = id(fixture?.tenantId); const inboundId = id(fixture?.inboundId)
|
|
const taskId = id(fixture?.canonicalPdaTaskId); const containerNo = fixture?.containerNo
|
|
const warehouseId = id(fixture?.warehouseId); const loadingPlanId = id(fixture?.loadingPlanId)
|
|
if (!tenantId || !inboundId || !taskId || !containerNo || !warehouseId || !loadingPlanId
|
|
|| fixture?.marker !== context.marker) {
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 柜次拣货与入库单汇总拣货收敛',
|
|
'本轮未取得 marker 隔离的柜次 PDA 与入库汇总 fixture,未使用历史资源', 'fixture')
|
|
return
|
|
}
|
|
const loadingListResponse = await request(context, 'GET',
|
|
`/api/platform/v1/first-leg/loading-lists/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
const loadingList = context.apiSuccess(MODULE, '回查柜次 PDA 完成后的装柜清单', loadingListResponse, '', OPTIONS)
|
|
context.assert(MODULE, '装柜清单保留 PDA 完成终态和操作者审计', loadingListResponse,
|
|
id(loadingList?.summary?.planId) === loadingPlanId && loadingList?.summary?.status === 'COMPLETED'
|
|
&& loadingList?.histories?.some?.((item) => item.actionType === 'PDA_COMPLETE_LOADING'
|
|
&& Boolean(item.operatorName) && Boolean(item.occurredAt)),
|
|
`planId=${id(loadingList?.summary?.planId) || 'missing'}, status=${loadingList?.summary?.status || 'missing'}`, OPTIONS)
|
|
const projectionResponse = await request(context, 'GET',
|
|
`/api/platform/v1/inbounds/${inboundId}/loading/picking?tenantId=${encodeURIComponent(tenantId)}`)
|
|
const projection = context.apiSuccess(MODULE, '查询入库级 PDA 服务端聚合投影', projectionResponse, '', OPTIONS)
|
|
context.assert(MODULE, '入库投影与柜次事实数量和终态一致', projectionResponse,
|
|
projection?.status === 'COMPLETED' && Number(projection?.expectedCount) > 0
|
|
&& Number(projection?.expectedCount) === Number(projection?.pickedCount),
|
|
`status=${projection?.status || 'missing'}, expected=${projection?.expectedCount ?? 'missing'}, picked=${projection?.pickedCount ?? 'missing'}`, OPTIONS)
|
|
const operationsResponse = await request(context, 'GET',
|
|
`/api/platform/v1/first-leg/pda-loading/tasks/${taskId}/operations`)
|
|
const operations = context.apiSuccess(MODULE, '查询柜次 PDA 不可变操作历史', operationsResponse, '', OPTIONS)
|
|
context.assert(MODULE, '直接完成契约保留领取操作且不伪造复核历史', operationsResponse,
|
|
Array.isArray(operations) && operations.some((item) => item.actionType === 'CLAIM'
|
|
&& Boolean(item.operatorId) && Boolean(item.createdAt))
|
|
&& !operations.some((item) => item.actionType === 'REVIEW'),
|
|
`actions=${Array.isArray(operations) ? operations.map((item) => item.actionType).join(',') : 'missing'}`, OPTIONS)
|
|
const forged = await request(context, 'POST',
|
|
`/api/platform/v1/inbounds/${inboundId}/loading/picking/start?tenantId=${encodeURIComponent(tenantId)}`,
|
|
{ expectedVersion: Number(projection?.version), expectedCount: 999, pickedCount: 999, remark: 'forged' },
|
|
{ 'Idempotency-Key': `${context.marker}-pda-forged-projection`.slice(0, 128) })
|
|
context.addStep(MODULE, '入库兼容入口拒绝客户端伪造汇总数量', forged, failure(forged),
|
|
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
|
|
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 批量提交、设备草稿版本与批次幂等',
|
|
'当前 PlatformLoadingPdaController 仅公开单明细 pick/partial/skip 与任务 review,没有批量提交或设备草稿 DTO/OpenAPI',
|
|
'contract-gap')
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 实时库存查询与提交前库存快照校验',
|
|
'当前 PDA Controller/OpenAPI 未公开实时库存查询或库存版本快照端点,不能用智能装柜候选列表伪装库存契约',
|
|
'contract-gap')
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, '不同仓库作业员竞争同一 PDA 明细',
|
|
'单明细领取和版本 CAS 已自动化;当前总控没有第二个具备 warehouse PDA work 权限的隔离账号 fixture,不能伪造跨操作者冲突',
|
|
'fixture')
|
|
}
|