const MODULE = 'phase3-loading-model-bridge' const OPTIONS = { scenario: 'parallel-loading-models', deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } export function stableLinkSnapshot(value) { return { id: id(value?.id), tenantId: id(value?.tenantId), inboundId: id(value?.inboundId), preloadPlanId: id(value?.preloadPlanId), preloadStatus: id(value?.preloadStatus), preloadVersion: Number(value?.preloadVersion), loadingPlanId: id(value?.loadingPlanId), containerBookingId: id(value?.containerBookingId), containerNo: id(value?.containerNo), loadingPlanStatus: id(value?.loadingPlanStatus), loadingPlanVersion: Number(value?.loadingPlanVersion), allocatedPackageCount: Number(value?.allocatedPackageCount), linkedAt: id(value?.linkedAt), } } function sameStableLink(left, right) { return JSON.stringify(stableLinkSnapshot(left)) === JSON.stringify(stableLinkSnapshot(right)) } 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 loadingPlanId = id(fixture?.loadingPlanId) if (!tenantId || !inboundId || !loadingPlanId || fixture?.marker !== context.marker) { context.addCoverageGap(MODULE, OPTIONS.scenario, '入库单级预装与柜次级智能装柜串联', '本轮未取得同一 marker 的预装/柜次计划关联 fixture,未使用历史资源', 'fixture', { activation: { state: 'pending-fixture', assertions: ['双向关联可查', '双版本冲突原子回滚', '终态门禁一致'] } }) return } const inboundPath = `/api/platform/v1/inbounds/${inboundId}/preload/loading-plans?tenantId=${encodeURIComponent(tenantId)}` const inboundResponse = await request(context, 'GET', inboundPath) const inboundLinks = context.apiSuccess(MODULE, '从入库侧查询预装与柜次关联', inboundResponse, '', OPTIONS) const link = Array.isArray(inboundLinks) ? inboundLinks.find((item) => id(item.loadingPlanId) === loadingPlanId) : null context.assert(MODULE, '入库侧关联只返回本轮柜次和客户隔离键', inboundResponse, Boolean(link) && id(link.inboundId) === inboundId && id(link.tenantId) === tenantId && Number(link.allocatedPackageCount) > 0, `links=${inboundLinks?.length ?? 'n/a'}`, OPTIONS) const planResponse = await request(context, 'GET', `/api/platform/v1/first-leg/smart-loading/plans/${loadingPlanId}/preloads`) const planLinks = context.apiSuccess(MODULE, '从柜次侧查询反向预装关联', planResponse, '', OPTIONS) context.assert(MODULE, '柜次侧反查到同一预装计划', planResponse, Array.isArray(planLinks) && planLinks.some((item) => id(item.inboundId) === inboundId && id(item.preloadPlanId) === id(fixture.preloadPlanId)), `links=${planLinks?.length ?? 'n/a'}`, OPTIONS) const pairReplay = await request(context, 'POST', `/api/platform/v1/inbounds/${inboundId}/preload/loading-plans/${loadingPlanId}?tenantId=${encodeURIComponent(tenantId)}`, { expectedPreloadVersion: 0, expectedLoadingPlanVersion: 0 }, { 'Idempotency-Key': `${context.marker}-bridge-bind`.slice(0, 128) }) const replayedLink = pairReplay?.body?.data context.addStep(MODULE, '已由推送建立的同一桥接 pair 重放返回原关联', pairReplay, pairReplay.http === 200 && codeOf(pairReplay) === '200' && sameStableLink(replayedLink, link), `HTTP=${pairReplay.http}, code=${codeOf(pairReplay)}, linkId=${id(replayedLink?.id) || 'missing'}`, OPTIONS) const afterInboundResponse = await request(context, 'GET', inboundPath) const afterInboundLinks = context.apiSuccess(MODULE, '桥接 pair 重放后回查入库侧关联', afterInboundResponse, '', OPTIONS) const afterInboundLink = Array.isArray(afterInboundLinks) ? afterInboundLinks.find((item) => id(item.loadingPlanId) === loadingPlanId) : null const afterPlanResponse = await request(context, 'GET', `/api/platform/v1/first-leg/smart-loading/plans/${loadingPlanId}/preloads`) const afterPlanLinks = context.apiSuccess(MODULE, '桥接 pair 重放后回查柜次侧关联', afterPlanResponse, '', OPTIONS) const afterPlanLink = Array.isArray(afterPlanLinks) ? afterPlanLinks.find((item) => id(item.inboundId) === inboundId) : null context.assert(MODULE, '桥接 pair 重放不改关联身份、状态或双侧版本', afterPlanResponse, sameStableLink(afterInboundLink, link) && sameStableLink(afterPlanLink, link), `linkId=${id(link?.id)}, preloadVersion=${link?.preloadVersion}, loadingPlanVersion=${link?.loadingPlanVersion}`, OPTIONS) }