From 41225248d0eb8b6530c5b1866d93fedb7c7fcd9e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 02:40:42 +0800 Subject: [PATCH] test(acceptance): reconcile dropship runtime facts --- .../container-detail-route-contract.test.mjs | 12 +++--- ...r-simulation-association-contract.test.mjs | 2 +- ...und-measurement-box-code-contract.test.mjs | 7 ++++ .../inbound-measurement.mjs | 34 +++++++++++++---- .../phase3-producer-chain-contract.test.mjs | 2 +- .../phase4-overseas-pda-contract.test.mjs | 2 +- ...-overseas-pda-operations-contract.test.mjs | 5 +++ .../phase4-overseas-pda-operations.mjs | 38 +++++++++++++++---- .../phase4-overseas-pda.mjs | 17 ++++++--- ...ainer-inbound-plan-batch-contract.test.mjs | 2 +- scripts/java-quality-baseline.json | 6 +-- scripts/java-quality-summary.test.mjs | 6 +-- scripts/synchronous-dependency-baseline.mjs | 5 +++ .../synchronous-dependency-contract.test.mjs | 11 +++--- 14 files changed, 108 insertions(+), 41 deletions(-) diff --git a/scripts/admin-test-env-modules/container-detail-route-contract.test.mjs b/scripts/admin-test-env-modules/container-detail-route-contract.test.mjs index 6fafae99b..370dccfea 100644 --- a/scripts/admin-test-env-modules/container-detail-route-contract.test.mjs +++ b/scripts/admin-test-env-modules/container-detail-route-contract.test.mjs @@ -6,17 +6,17 @@ const read = (name) => readFileSync(new URL(name, import.meta.url), 'utf8') test('domestic container fixtures use the current workbench detail route', () => { const containerLoading = read('./phase3-container-loading.mjs') - assert.ok(containerLoading.includes("context.request('GET', `${CONTAINERS}/domestic/${containerId}`)")) + assert.ok(containerLoading.includes("context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)")) assert.ok(!containerLoading.includes("context.request('GET', `${CONTAINERS}/${containerId}`)")) - assert.ok(containerLoading.includes('const containerDetail = containerWorkbench?.container')) - assert.ok(containerLoading.includes('const simulationBeforeLink = simulationWorkbench?.container')) - assert.ok(containerLoading.includes('`${CONTAINERS}/domestic?scope=ACTIVE&page=1&size=50&keyword=')) - assert.ok(containerLoading.includes('records(activeWorkbench).map((item) => item?.container)')) + assert.ok(containerLoading.includes("const containerDetail = containerRecord(context.apiSuccess(MODULE, '回查货柜详情'")) + assert.ok(containerLoading.includes("const simulationBeforeLink = containerRecord(context.apiSuccess(MODULE, '预装确认前读取模拟柜最新版本'")) + assert.ok(containerLoading.includes('`${DOMESTIC_CONTAINERS}?page=1&size=50&scope=ACTIVE&keyword=')) + assert.ok(containerLoading.includes('records(activeContainers).map(containerRecord)')) const loading = read('./phase3-loading.mjs') assert.ok(loading.includes("context.request('GET', `/api/platform/v1/containers/domestic/${containerBookingId}`)")) assert.ok(!loading.includes("context.request('GET', `/api/platform/v1/containers/${containerBookingId}`)")) - assert.ok(loading.includes('const latestContainer = latestWorkbench?.container')) + assert.ok(loading.includes("const latestContainer = containerRecord(context.apiSuccess(MODULE, '合并前读取模拟柜最新版本'")) const crossRegion = read('./phase4-cross-region-events.mjs') const fixtureId = 'encodeURIComponent(id(phase3.containerBookingId))' diff --git a/scripts/admin-test-env-modules/container-simulation-association-contract.test.mjs b/scripts/admin-test-env-modules/container-simulation-association-contract.test.mjs index 2c6c9cb2d..ff27f484d 100644 --- a/scripts/admin-test-env-modules/container-simulation-association-contract.test.mjs +++ b/scripts/admin-test-env-modules/container-simulation-association-contract.test.mjs @@ -36,7 +36,7 @@ test('port link mutates the simulation through the frozen four-field request', ( }) test('HTTP fixture keeps simulation and plan identities and creates allocation at link', () => { - assert.ok(fixture.includes("context.request('GET', `${CONTAINERS}/domestic/${containerId}`)")) + assert.ok(fixture.includes("context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)")) assert.ok(!fixture.includes("context.request('GET', `${CONTAINERS}/${containerId}`)")) assert.match(fixture, /关联前后内部柜次、计划和排货明细身份稳定/) assert.match(fixture, /订舱 allocation 原子创建并指向稳定模拟柜身份/) diff --git a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs index 01193adbc..fb7a05979 100644 --- a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs @@ -110,3 +110,10 @@ test('dropship measurement completes the formal palletization workflow before sm assert.match(source, /dropship \? \['WAIT_PALLETIZE'\] : \['WAIT_SPLIT'\]/) assert.match(source, /一件代发合托完成后进入智能分柜门禁/) }) + +test('confirmed package count reconciles an uncertain transport result before retrying', () => { + assert.match(source, /async function updateConfirmedPackageCount/) + assert.match(source, /recoveredFromTransportFailure: true/) + assert.match(source, /Number\(detail\?\.version\) > Number\(body\.measurementVersion\)/) + assert.match(source, /await updateConfirmedPackageCount\(context, measurementPath/) +}) diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 8be4a3830..3663f1e3d 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -536,6 +536,29 @@ async function requestWithHeaders(context, method, path, body, extraHeaders) { } } +async function updateConfirmedPackageCount(context, measurementPath, itemId, body) { + const updatePath = measurementPath.replace(/\?.*$/, '') + + `/items/${encodeURIComponent(itemId)}/confirmed-package-count?${measurementPath.split('?')[1] || ''}` + let lastResponse = null + for (let attempt = 1; attempt <= 2; attempt += 1) { + lastResponse = await context.request('PUT', updatePath, body) + if (lastResponse?.http !== 0) return lastResponse + const recovered = await context.request('GET', measurementPath) + const detail = recovered?.http === 200 && String(recovered?.body?.code) === '200' + ? recovered.body?.data : null + const item = detail?.items?.find((candidate) => asId(candidate?.id) === asId(itemId)) + if (Number(item?.confirmedPackageCount) === Number(body.confirmedPackageCount) + && Number(detail?.version) > Number(body.measurementVersion)) { + return { + ...recovered, method: 'GET', path: measurementPath, recoveredFromTransportFailure: true, + body: { ...recovered.body, data: { ...item, measurementVersion: Number(detail.version) } }, + } + } + if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 500)) + } + return lastResponse +} + function finishScenario(context, start) { const steps = context.steps.slice(start) context.scenarios.push({ @@ -651,11 +674,10 @@ export async function run(context) { && closeTo(firstItem?.heightCm, firstDimensions.height), `itemFound=${Boolean(firstItem)}, imageFileId=${asId(firstItem?.intelligentImageFileId) || 'missing'}`, OPTIONS) - const countResponse = await context.request('PUT', - `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`, + const countResponse = await updateConfirmedPackageCount(context, measurementPath, firstScan.itemId, { measurementVersion: Number(firstScan.measurementVersion), confirmedPackageCount: originalPackageCount }) const countResult = context.apiSuccess(MODULE, '人工确认当前测量代表箱数', countResponse, - `confirmedPackageCount=${originalPackageCount}`, OPTIONS) + `confirmedPackageCount=${originalPackageCount}, transportRecovered=${countResponse?.recoveredFromTransportFailure === true}`, OPTIONS) context.assert(MODULE, '人工箱数更新且测量版本递增', countResponse, Number(countResult?.confirmedPackageCount) === originalPackageCount && Number(countResult?.measurementVersion) === Number(firstScan.measurementVersion) + 1, @@ -722,8 +744,7 @@ export async function run(context) { `detailCount=${incrementalDetailCount}, packageCounts=${detailPackageCounts.join('+')}, frozen=${originalPackageCount}`, OPTIONS) if (expectedPackages === originalPackageCount) { const firstDetailPackageCount = detailPackageCounts[0] - const resetFirstCountResponse = await context.request('PUT', - `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`, + const resetFirstCountResponse = await updateConfirmedPackageCount(context, measurementPath, firstScan.itemId, { measurementVersion: Number(measured?.version), confirmedPackageCount: firstDetailPackageCount }) const resetFirstCount = context.apiSuccess(MODULE, '增量 fixture 将首条货物确认箱数恢复为自身箱数', resetFirstCountResponse, @@ -751,8 +772,7 @@ export async function run(context) { && asId(scanned?.itemId) !== asId(firstScan.itemId), `itemId=${asId(scanned?.itemId) || 'missing'}, inboundDetailId=${asId(scanned?.inboundDetailId) || 'pending-query'}`, OPTIONS) if (!scanned?.itemId) continue - const countDetailResponse = await context.request('PUT', - `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${scanned.itemId}/confirmed-package-count?${query(tenantId)}`, + const countDetailResponse = await updateConfirmedPackageCount(context, measurementPath, scanned.itemId, { measurementVersion: Number(scanned.measurementVersion), confirmedPackageCount: detailPackageCount }) const countDetail = context.apiSuccess(MODULE, `增量第${detailIndex + 1}条货物确认自身箱数`, countDetailResponse, diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index 043dd6c8d..c32194701 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -71,7 +71,7 @@ test('records LOADED and clears the reason only after a real LOADED reread', () test('container loading follows push, claim, real pick, completed sequence', () => { const value = source('./phase3-container-loading.mjs') - assert.match(value, /const containerDetail = containerWorkbench\?\.container/) + assert.match(value, /const containerDetail = containerRecord\(context\.apiSuccess/) assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/) assertOrder(value, [ "pushed?.status === 'EDITING'", diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs index b7707691f..d5100357a 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs @@ -183,7 +183,7 @@ test('PDA receiving lifecycle is ordered from candidate claim through photos, mo '/current/receipts/bulk', '/current/receipts/carton', "'FBA模式硬拒绝逐箱收货'", "'OVERSEAS模式硬拒绝批量收货'", 'PHOTO_ROLES.every', "finalStatus === 'COMPLETED'", 'completedContainers.push', - 'completedContainerFacts.push', 'expectedPalletCost', 'unitPrice', + 'completedContainerFacts.push', 'expectedPalletCost', 'physicalPallets', 'resolvedPallet.data?.unitPrice', ]) assert.ok(source.includes(token), token) }) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs index e0f31dc7d..754514e19 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs @@ -28,6 +28,7 @@ test('PDA operations run immediately after receiving and all later consumers dep test('receiving hands off only transient credentials and completed warehouse facts', () => { assert.match(receivingSource, /context\.context\.phase4OverseasPdaOperations = \{/) assert.match(receivingSource, /completedFixtures: \[\]/) + assert.match(receivingSource, /merchantToken,/) assert.match(receivingSource, /if \(!handoffReady\) \{[\s\S]*await cleanup\(\)/) assert.doesNotMatch(receivingSource, /createdResources\.phase4OverseasPdaOperations/) assert.doesNotMatch(receivingSource, /primary: primary \? \{[^}]*password/) @@ -47,6 +48,8 @@ test('P2 to P5 cover all frozen endpoints, replays and conflict boundaries witho for (const marker of ['按原数计数', '同键重放', '同键异载荷A0409', '旧版本A0409', 'SUBMITTED', '移回原库位', 'WAIT_REVIEW', 'REWORK', 'APPROVE', 'COMPLETED', 'chargeAmount: 0.01', 'cartonMark:', 'fbaCartonMark:', + 'inventory-candidates', 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:', + 'sourceInboundOrderId:', 'operationWarehouseId:', 'P5换标费用冻结后开放PDA作业', 'P5换标完成后钱包结算收敛']) { assert.ok(source.includes(marker), marker) } @@ -78,6 +81,7 @@ test('fixture failure still disables both picker accounts and removes all transi marker: 'PDAOPS-CONTRACT', primary: { membershipId: '1', username: 'primary', token: 'primary-secret-token' }, secondary: { membershipId: '2', username: 'secondary', token: 'secondary-secret-token' }, + merchantToken: 'merchant-secret-token', completedFixtures: [], async cleanup() { cleanups.push('secondary', 'primary') }, } @@ -95,6 +99,7 @@ test('fixture failure still disables both picker accounts and removes all transi assert.deepEqual(cleanups, ['secondary', 'primary']) assert.equal(handoff.primary.token, undefined) assert.equal(handoff.secondary.token, undefined) + assert.equal(handoff.merchantToken, undefined) assert.equal(context.context.phase4OverseasPdaOperations, undefined) assert.ok(gaps.some((gap) => gap[2] === 'P2-P5本轮PDA收货交接')) assert.ok(steps.some((step) => !step.passed && step.options?.deploymentBlocker)) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs index 584304677..cef16b7bb 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs @@ -328,29 +328,51 @@ async function verifyPalletOperations(context, token, fixture) { return success(unbound.response) } -async function verifyRelabel(context, token, fixture) { +async function verifyRelabel(context, token, merchantToken, fixture) { const customerId = id(fixture.order?.order?.customerId || context.createdResources?.phase4OverseasPda?.customerId) const customerOrderNo = fixture.order?.order?.orderNo || fixture.order?.order?.sourceOrderNo || `PDA-${context.marker}` const inventory = fixture.inventory.find((item) => nonBlank(item?.skuCode)) - if (!customerId || !inventory?.skuCode || !fixture.overseasWarehouseId) { - blocker(context, 'P5本轮库存换标申请', '缺少本轮完成库存的客户、SKU或海外仓事实') + if (!customerId || !inventory?.skuCode || !fixture.overseasWarehouseId || !nonBlank(merchantToken)) { + blocker(context, 'P5本轮库存换标申请', '缺少本轮完成库存的客户、SKU、海外仓或客户令牌事实') return false } if (!await fundRelabelWallet(context, customerId)) return false + const inventoryCandidatesResponse = await context.requestOverseas('GET', query( + '/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', { + operationWarehouseId: fixture.overseasWarehouseId, inboundCode: fixture.inboundCode, + }), undefined, {}, merchantToken) + const inventoryCandidates = success(inventoryCandidatesResponse) + && Array.isArray(inventoryCandidatesResponse.body?.data) ? inventoryCandidatesResponse.body.data : [] + const source = inventoryCandidates.find((item) => id(item?.inventoryBalanceId) === id(inventory?.balanceId)) + || inventoryCandidates.find((item) => item?.skuCode === inventory.skuCode + && item?.inboundCode === fixture.inboundCode) + const sourcePosition = source?.positions?.find((item) => id(item?.positionId) === id(inventory?.positionId) + && Number(item?.availablePackages) >= 3) + || source?.positions?.find((item) => Number(item?.availablePackages) >= 3) + context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse, + Boolean(source) && Boolean(sourcePosition) && id(source?.inboundOrderId) !== '', + `balanceId=${id(source?.inventoryBalanceId) || 'missing'}, positionId=${id(sourcePosition?.positionId) || 'missing'}, inboundOrderId=${id(source?.inboundOrderId) || 'missing'}; ${trace(inventoryCandidatesResponse)}`, OPTIONS) + if (!source || !sourcePosition || !id(source.inboundOrderId)) { + blocker(context, 'P5本轮库存换标来源', '客户库存候选未返回本轮库存的余额、至少三箱位置和来源入库单', inventoryCandidatesResponse) + return false + } const relabelBase = '/api/platform/v1/last-leg/relabel-applications' const pdaBase = '/api/pda/v1/last-leg/relabel-tasks' const newSku = `${inventory.skuCode}-NEW-${String(context.marker).slice(-8)}`.slice(0, 128) const createBody = { - customerOrderNo: String(customerOrderNo).slice(0, 128), overseasWarehouseId: id(fixture.overseasWarehouseId), + customerOrderNo: String(customerOrderNo).slice(0, 128), operationWarehouseId: id(fixture.overseasWarehouseId), labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19), contactName: 'PDA换标验收', contactPhone: '13800000006', chargeAmount: 0.01, remark: context.marker, idempotencyKey: key(context, 'RELABEL-CREATE'), - items: [{ skuCode: inventory.skuCode, productNameZh: inventory.productName || '本轮库存商品', + items: [{ sourceInventoryBalanceId: id(source.inventoryBalanceId), + sourceInventoryPositionId: id(sourcePosition.positionId), sourceInboundOrderId: id(source.inboundOrderId), + skuCode: source.skuCode, productNameZh: source.productNameZh || inventory.productName || '本轮库存商品', totalPackageCount: 3, notifiedPackageCount: 3, shipmentNo: `SHIP-${context.marker}`.slice(0, 128), newSkuCode: newSku, labelRequirement: context.marker, cartonMark: `CM-${String(context.marker).slice(-32)}`, - fbaCartonMark: `FBA-${String(context.marker).slice(-32)}` }], + fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`, + destinationFbaWarehouseCode: source.destinationFbaWarehouseCode }], } const createdResponse = await context.requestOverseas('POST', query(relabelBase, { customerId }), createBody) @@ -469,6 +491,7 @@ async function verifyRelabel(context, token, fixture) { export async function run(context) { const handoff = context.context?.phase4OverseasPdaOperations const token = handoff?.primary?.token + const merchantToken = handoff?.merchantToken try { if (handoff?.marker !== context.marker || !nonBlank(token) || !Array.isArray(handoff.completedFixtures) || handoff.completedFixtures.length === 0) { @@ -488,13 +511,14 @@ export async function run(context) { await verifyStocktake(context, token, fixture) await verifyInventoryMove(context, token, fixture) await verifyPalletOperations(context, token, fixture) - await verifyRelabel(context, token, fixture) + await verifyRelabel(context, token, merchantToken, fixture) } finally { try { if (typeof handoff?.cleanup === 'function') await handoff.cleanup() } finally { if (handoff?.primary) delete handoff.primary.token if (handoff?.secondary) delete handoff.secondary.token + if (handoff) delete handoff.merchantToken delete context.context.phase4OverseasPdaOperations } } diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index ad3153a8b..db1c8b9b7 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -212,6 +212,8 @@ async function verifyWarehouseQueries(context, token, inboundCode, pallet, desti && Number(resolvedPallet.data?.occupiedPackages) > 0, `palletId=${id(resolvedPallet.data?.palletId) || 'missing'}; ${trace(resolvedPallet.response)}`, OPTIONS) + if (Number(resolvedPallet.data?.unitPrice) > 0) pallet.unitPrice = Number(resolvedPallet.data.unitPrice) + const locationCode = resolvedPallet.data?.locationCode || pallet.locationCode const location = await pda(context, token, 'POST', '完成入库后扫码查询库位', '/api/pda/v1/last-leg/locations/resolve', { code: locationCode }) @@ -693,12 +695,6 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe 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') { - context.createdResources.phase4OverseasPda.completedContainerFacts.push({ - containerNo, - containerBookingId: id(preferredProducer?.containerBookingId), - expectedPalletCost: [...pallets.values()].reduce((sum, item) => sum + Number(item.unitPrice || 0), 0), - palletIds: [...pallets.values()].map((item) => item.palletId), - }) const order = await captureCompletedOrder(context, containerNo, preferredProducer?.tenantId) for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) { const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode, @@ -711,6 +707,14 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe }) } } + const physicalPallets = [...new Map([...pallets.values()] + .map((item) => [id(item.palletId), item])).values()] + context.createdResources.phase4OverseasPda.completedContainerFacts.push({ + containerNo, + containerBookingId: id(preferredProducer?.containerBookingId), + expectedPalletCost: physicalPallets.reduce((sum, item) => sum + Number(item.unitPrice || 0), 0), + palletIds: physicalPallets.map((item) => item.palletId), + }) } } @@ -747,6 +751,7 @@ export async function run(context) { primary: primary ? { membershipId: primary.membershipId, username: primary.username, token: primary.token } : null, secondary: secondary ? { membershipId: secondary.membershipId, username: secondary.username, token: secondary.token } : null, overseasWarehouseId: sharedWarehouse.id, + merchantToken, completedFixtures: [], cleanup, } diff --git a/scripts/admin-test-env-modules/test-container-inbound-plan-batch-contract.test.mjs b/scripts/admin-test-env-modules/test-container-inbound-plan-batch-contract.test.mjs index 8b82ad51c..ac87612f6 100644 --- a/scripts/admin-test-env-modules/test-container-inbound-plan-batch-contract.test.mjs +++ b/scripts/admin-test-env-modules/test-container-inbound-plan-batch-contract.test.mjs @@ -75,7 +75,7 @@ test('HTTP scenario freezes normal, invalid, replay, concurrency, partial and PD test('container API uses one stable long string identity', () => { const endpoint = openapi.slice(openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`), openapi.indexOf(' /api/platform/v1/last-leg/dispatches:', openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`))) - assert.match(endpoint, /required: \[sourceContainerBookingId\]/) + assert.match(endpoint, /required: \[sourceContainerBookingId, snapshotVersion, snapshotHash\]/) assert.match(endpoint, /sourceContainerBookingId:\s*\{ type: string/) assert.match(controller, /convertContainer/) assert.match(adminApi, /sourceContainerBookingId: string/) diff --git a/scripts/java-quality-baseline.json b/scripts/java-quality-baseline.json index 4b60ec018..bc8e4580d 100644 --- a/scripts/java-quality-baseline.json +++ b/scripts/java-quality-baseline.json @@ -11,9 +11,9 @@ "services": 15 }, "openapi": { - "documents": 39, - "coveredServices": 8, - "codeFirstServices": 7 + "documents": 43, + "coveredServices": 10, + "codeFirstServices": 5 }, "reports": { "jacocoPresent": 21, diff --git a/scripts/java-quality-summary.test.mjs b/scripts/java-quality-summary.test.mjs index bf77a3e19..0c731fae6 100644 --- a/scripts/java-quality-summary.test.mjs +++ b/scripts/java-quality-summary.test.mjs @@ -65,9 +65,9 @@ test('repository freezes the current topology and non-blocking aggregate summary const summary = await buildQualitySummary(repositoryRoot) const baseline = JSON.parse(await readFile(path.join(repositoryRoot, 'scripts', 'java-quality-baseline.json'), 'utf8')) assert.deepEqual(summary.topology, { modules: 22, codeModules: 21, services: 15 }) - assert.equal(summary.openapi.documents, 40) - assert.equal(summary.openapi.coveredServices.length, 8) - assert.equal(summary.openapi.codeFirstServices.length, 7) + assert.equal(summary.openapi.documents, 43) + assert.equal(summary.openapi.coveredServices.length, 10) + assert.equal(summary.openapi.codeFirstServices.length, 5) assert.equal(baseline.mode, 'report-only') assert.deepEqual(baseline.topology, summary.topology) assert.equal(baseline.modules.length, 21) diff --git a/scripts/synchronous-dependency-baseline.mjs b/scripts/synchronous-dependency-baseline.mjs index 33823f179..0b2ec81c2 100644 --- a/scripts/synchronous-dependency-baseline.mjs +++ b/scripts/synchronous-dependency-baseline.mjs @@ -80,6 +80,9 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([ 'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '价目文件上传无幂等键,读取可重复'), feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookWarehouseClient.java', 'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取仓库/渠道主数据'), + feign('firstleg/src/main/java/com/baiyue/firstleg/client/PrintableOrderInboundClient.java', + 'firstleg-service', 'order-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY', + '首次打印按客户与订单预留稳定入仓码,重复调用复用已冻结快照'), feign('firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java', 'firstleg-service', 'print-service', ['REQUEST'], 'READ', 'SAFE_READ', '无状态 PDF 渲染,POST 为计算型读取'), feign('firstleg/src/main/java/com/baiyue/firstleg/client/TruckAppointmentFileClient.java', @@ -154,6 +157,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([ 'gateway-overseas', 'auth-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST PDA 会话校验,失败关闭'), http('lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/HttpCustomerVisitFileGateway.java', 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '回访附件上传无幂等键,归属校验可重复'), + http('lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/HttpExceptionEvidenceFileGateway.java', + 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '异常处理证据上传无幂等键,归属校验可重复'), http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java', 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'), http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java', diff --git a/scripts/synchronous-dependency-contract.test.mjs b/scripts/synchronous-dependency-contract.test.mjs index a7a2687f6..90d7cd409 100644 --- a/scripts/synchronous-dependency-contract.test.mjs +++ b/scripts/synchronous-dependency-contract.test.mjs @@ -98,15 +98,15 @@ async function discoverFeignEdges() { return clients.sort((left, right) => left.path.localeCompare(right.path)) } -test('all 55 Feign clients have reviewed context, access and idempotency metadata', async () => { +test('all 56 Feign clients have reviewed context, access and idempotency metadata', async () => { const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN') .map(({ path, source, target }) => ({ path, source, target })) .sort((left, right) => left.path.localeCompare(right.path)) - assert.equal(reviewed.length, 55) + assert.equal(reviewed.length, 56) assert.deepEqual(await discoverFeignEdges(), reviewed) }) -test('all 19 raw HTTP transports have reviewed context, access and idempotency metadata', async () => { +test('all 20 raw HTTP transports have reviewed context, access and idempotency metadata', async () => { const discovered = [] for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) { for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) { @@ -118,7 +118,7 @@ test('all 19 raw HTTP transports have reviewed context, access and idempotency m } const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP') .map(client => client.path).sort() - assert.equal(reviewed.length, 19) + assert.equal(reviewed.length, 20) assert.deepEqual(discovered.sort(), reviewed) }) @@ -226,7 +226,8 @@ test('Order and Firstleg remain a bilateral REQUEST SCC until loading-complete t assert.deepEqual(firstlegToOrder.map(client => client.path).sort(), [ 'firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java', 'firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java', - 'firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java' + 'firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java', + 'firstleg/src/main/java/com/baiyue/firstleg/client/PrintableOrderInboundClient.java' ]) assert.ok(stronglyConnectedComponents(graphEdges('REQUEST')).some(component => component.includes('firstleg-service') && component.includes('order-service')))