Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs

179 lines
11 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
const read = (path) => readFileSync(resolve(here, path), 'utf8')
const firstlegOpenApi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml')
const pdaOpenApi = read('../../firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml')
const entry = read('../admin-test-env-acceptance.mjs')
const loading = read('./phase3-loading.mjs')
const convergence = read('./phase3-pda-convergence.mjs')
const customs = read('./phase3-customs.mjs')
const finalize = read('./phase3-loading-finalize.mjs')
const loadingPickService = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java')
const loadingDeliveryService = read('../../firstleg/src/main/java/com/baiyue/firstleg/loading/service/LoadingDeliveryService.java')
const loadingDeliveryServiceTest = read('../../firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java')
function ordered(source, fragments) {
let cursor = -1
for (const fragment of fragments) {
const next = source.indexOf(fragment, cursor + 1)
assert.ok(next > cursor, `${fragment} must appear in frozen order`)
cursor = next
}
}
test('OpenAPI freezes LOADING incremental plan capabilities and optimistic versions', () => {
for (const endpoint of [
'/api/platform/v1/first-leg/smart-loading/plans/{planId}/assignments:',
'/api/platform/v1/first-leg/smart-loading/plans/{planId}/items/{itemId}:',
]) assert.ok(firstlegOpenApi.includes(endpoint), `${endpoint} missing`)
for (const field of [
'inboundId', 'inboundDetailId', 'packageCount', 'expectedVersion', 'operationId',
'expectedPlanVersion', 'expectedItemVersion', 'canAssign', 'canEdit', 'blockedReason',
'finalCompletionStatus', 'pdaTaskId', 'pdaTaskStatus', 'pdaTaskVersion',
'pickedPackageCount', 'minPackageCount', 'canAdjust', 'canRemove',
]) assert.match(firstlegOpenApi, new RegExp(`\\b${field}\\b`))
assert.match(firstlegOpenApi, /enum: \[NOT_STARTED, IN_PROGRESS, COMPLETED\]/)
})
test('default fixture publishes a second marker-owned inbound for the same-plan increment', () => {
assert.match(entry, /const incrementalMarker = `\$\{originalMarker\}-LOAD\$\{suffix\}-INC`/)
assert.match(entry, /orders-phase3-loading-incremental-fixture/)
assert.match(entry, /inbound-measurement-phase3-loading-incremental-fixture/)
assert.match(entry, /phase3LoadingIncrementalPackageCount = 2/)
assert.match(entry, /phase3LoadingIncrementalDetailCount = 4/)
assert.match(entry, /phase3LoadingIncremental: incrementalFixture/)
assert.match(entry, /marker: originalMarker/)
assert.doesNotMatch(entry, /phase3LoadingIncremental[\s\S]{0,300}(history|fallback|latest)/i)
})
test('HTTP chain refreshes after stale inventory and keeps one stable PDA identity', () => {
ordered(loading, [
'PDA 原子批次完成拣货并等待人工确认',
'五类装柜证据完整且铅封号持久化',
'LOADING 阶段向同一 PDA task 增加入库单',
'动态新增计划明细按 inboundDetailId 唯一定位',
'增量保持稳定四元身份、LOADING 不回退并撤回最终完成门禁',
'增量后旧 inventory token 明确 A0409 并要求刷新',
'增量旧 token 冲突后 plan/task/items 零写入',
'未拣动态增量明细允许删除',
'未拣动态增量明细允许调整数量',
'调整数量递增 plan/task/item 版本并保持稳定任务',
'重新增量后刷新 PDA task detail',
'重新增量后刷新 inventory snapshot',
'动态新增项拣完后同一任务重新进入 AWAITING_REVIEW',
'已拣项减量低于 minPackageCount 返回 A0409',
'已拣项删除返回 A0409',
'已拣项删除和减量失败后 plan/task/items 零写入',
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
])
assert.match(loading, /String\(staleIncrementBatch\.body\?\.message \|\| ''\)\.includes\('库存已变化,请刷新后重试'\)/)
assert.match(loading, /incrementalPlanItems = \(incrementedPlan\?\.items \|\| \[\]\)\.filter[\s\S]*id\(item\?\.inboundDetailId\) === id\(dynamicCandidate\.inboundDetailId\)/)
assert.match(loading, /reassignedPlanItems = \(reassignedPlan\?\.items \|\| \[\]\)\.filter[\s\S]*id\(item\?\.inboundDetailId\) === id\(dynamicCandidate\.inboundDetailId\)/)
assert.match(loading, /重新加入动态明细仍按 inboundDetailId 唯一定位/)
assert.doesNotMatch(loading, /incrementedPlan\?\.items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
assert.doesNotMatch(loading, /reassignedPlan\?\.items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
assert.match(loading, /id\(incrementedTask\?\.loadingPlanId\) === loadingPlanId/)
assert.match(loading, /id\(incrementedTask\?\.containerBookingId\) === containerBookingId/)
assert.match(loading, /const taskDetailPath = `\$\{pdaBase\}\/tasks\/\$\{canonicalTaskId\}\?warehouseId=/)
assert.match(loading, /loadingPlanStatus: 'LOADING', finalStatus: 'AWAITING_REVIEW'/)
assert.match(loading, /expectedBatchOperationIds = \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
assert.match(loading, /expectedBatchOperationIds, incrementalInboundId/)
assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/)
assert.doesNotMatch(loading, /if \(Number\(dynamicCandidate\?\.availablePackageCount\) < 2\) return/)
assert.doesNotMatch(loading, /complete-before-downstream/)
assert.doesNotMatch(loading, /\/loading\/complete\?tenantId=/)
})
test('downstream finalizer uses the frozen truck-coupled complete contract', () => {
assert.match(convergence, /loadingPlan\?\.status === 'LOADING'/)
assert.match(convergence, /!operations\.some\(\(item\) => item\.actionType === 'COMPLETE_LOADING'\)/)
assert.match(customs, /\['AWAITING_REVIEW', 'LOADED'\]/)
assert.match(customs, /ensureReleasedOceanBooking/)
assert.doesNotMatch(customs, /truck\?\.finalStatus !== 'COMPLETED'/)
ordered(finalize, [
'最终装柜前保持 LOADING 且版本有效',
'const taskPath =',
'结算递增 inboundVersion 后 final complete 必须使用最新版本',
'入库单终态 LOADED、版本递增且审计历史存在',
])
assert.match(finalize, /const taskDetailPath = `\$\{taskPath\}\?warehouseId=/)
assert.doesNotMatch(finalize, /BLOCKED_CONTRACT/)
assert.doesNotMatch(finalize, /truck\?\.finalStatus === 'COMPLETED'/)
assert.doesNotMatch(finalize, /\/loading\/complete\?tenantId=/)
})
test('PDA complete OpenAPI freezes optional truck coupling and atomic rollback', () => {
assert.match(pdaOpenApi, /\/api\/platform\/v1\/first-leg\/pda-loading\/tasks\/\{taskId\}\/complete:/)
assert.match(pdaOpenApi, /拖车状态不是前置门禁/)
assert.match(pdaOpenApi, /没有有效拖车则直接继续/)
assert.match(pdaOpenApi, /恰有一条有效拖车时由服务端加锁推进为COMPLETED并写历史/)
assert.match(pdaOpenApi, /多条有效拖车或并发修改返回A0409且全部Firstleg本地状态回滚/)
assert.match(pdaOpenApi, /同operationId重放返回冻结结果,不重复推进拖车或写历史/)
})
test('PDA final completion refreshes post-settlement versions for full and partial split', () => {
ordered(loadingPickService, [
'settlementService.settle(taskId, operatorId, operatorName)',
'loadingDeliveryService.completeAfterPdaSettlement',
])
assert.doesNotMatch(loadingPickService,
/new LoadingRequests\.CompleteLoadingRequest\(entry\.getValue\(\)\.version\(\), remark\)/)
ordered(loadingDeliveryService, [
'CompletionTarget target = repository.completionTarget(tenantId, inboundId)',
'Snapshot current = repository.findInboundForUpdate(tenantId, completionInboundId)',
'new CompleteLoadingRequest(current.version(), remark)',
])
for (const regression of [
'pdaFinalizeUsesSourceVersionIncrementedByFullSettlement',
'pdaFinalizeLocksAndUsesLoadedSliceVersionAfterPartialSettlement',
'pdaFinalizeKeepsOptimisticConflictForRealConcurrentChange',
]) assert.match(loadingDeliveryServiceTest, new RegExp(regression))
assert.match(loadingDeliveryServiceTest, /verify\(repository, never\(\)\)\.completeLoadedChild\(10, 20, 7, 6\)/)
assert.match(loadingDeliveryServiceTest, /verify\(histories, never\(\)\)\.insert/)
})
test('HTTP finalizer preserves failure evidence and verifies the settled slice', () => {
ordered(finalize, [
'读取结算前计划即时版本',
'读取结算前 PDA 操作历史基线',
'结算递增 inboundVersion 后 final complete 必须使用最新版本',
'final complete 失败后回查 PDA 任务',
'final complete 失败后回查计划',
'final complete 失败后回查入库单',
'结算或版本冲突失败后 task/plan/inbound/operation 全部零写入',
'结算后回查柜次实际履约分片',
'结算后每个实装来源分片唯一且柜次身份稳定',
])
assert.match(finalize, /requestId=\$\{completeResponse\.requestId \|\| 'missing'\}/)
assert.match(finalize, /traceId=\$\{completeResponse\.traceId \|\| 'missing'\}/)
assert.match(finalize, /settlementMode = remainderSlice \? 'PARTIAL' : 'FULL'/)
})
test('HTTP finalizer races distinct completion keys and persists exactly one terminal audit', () => {
ordered(finalize, [
'读取结算前 PDA 操作历史基线',
'const completeAttempts = [1, 2].map',
'const concurrentCompleteResponses = await Promise.all',
'并发完成后回查 PDA 操作审计',
'两个不同幂等键并发 complete 仅一成功且只写一条完成审计',
'订舱与五类证据等保留门禁齐备后完成装柜',
'LOADED 完成请求 Idempotency-Key 重放保持终态',
'入库单终态 LOADED、版本递增且审计历史存在',
])
assert.match(finalize, /pda-final-complete-concurrent-\$\{index\}/)
assert.match(finalize, /p3-complete-concurrent-key-\$\{index\}/)
assert.match(finalize, /successfulCompleteIndexes\.length === 1/)
assert.match(finalize, /conflictedCompleteIndexes\.length === 1/)
assert.match(finalize, /codeOf\(response\) === 'A0409'/)
assert.match(finalize, /Boolean\(conflictedCompleteResponse\?\.requestId\)/)
assert.match(finalize, /Boolean\(conflictedCompleteResponse\?\.traceId\)/)
assert.match(finalize, /operationsAfterConcurrent\.length === operationsBefore\.length \+ 1/)
assert.match(finalize, /concurrentCompleteOperations\.length === 1/)
assert.match(finalize, /concurrentCompleteOperations\[0\]\?\.operationId === completeBody\.operationId/)
})