Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
4 changed files with 81 additions and 7 deletions
Showing only changes of commit cb78bc6b41 - Show all commits
@@ -142,3 +142,26 @@ test('HTTP finalizer preserves failure evidence and verifies the settled slice',
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/)
})
@@ -129,13 +129,50 @@ export async function run(context) {
packageCount: Number(item?.packageCount), picked: Number(item?.pickedPackageCount) })),
})
const operationsBeforeSignature = JSON.stringify(operationsBefore || [])
const completeBody = {
expectedTaskVersion: Number(taskBefore.version),
operationId: `${context.marker}-pda-final-complete`.slice(0, 128), remark: context.marker,
}
const completeKey = `${context.marker}-p3-complete-after-downstream`.slice(0, 128)
const completeResponse = await request(context, 'POST', `${taskPath}/complete`, completeBody,
{ 'Idempotency-Key': completeKey })
const completeAttempts = [1, 2].map((index) => ({
body: {
expectedTaskVersion: Number(taskBefore.version),
operationId: `${context.marker}-pda-final-complete-concurrent-${index}`.slice(0, 128),
remark: context.marker,
},
key: `${context.marker}-p3-complete-concurrent-key-${index}`.slice(0, 128),
}))
const concurrentCompleteResponses = await Promise.all(completeAttempts.map((attempt) =>
request(context, 'POST', `${taskPath}/complete`, attempt.body,
{ 'Idempotency-Key': attempt.key })))
const successfulCompleteIndexes = concurrentCompleteResponses.flatMap((response, index) =>
response.http === 200 && codeOf(response) === '200' ? [index] : [])
const conflictedCompleteIndexes = concurrentCompleteResponses.flatMap((response, index) =>
response.http === 200 && codeOf(response) === 'A0409' ? [index] : [])
const conflictedCompleteResponse = conflictedCompleteIndexes.length === 1
? concurrentCompleteResponses[conflictedCompleteIndexes[0]] : null
const winningCompleteIndex = successfulCompleteIndexes[0] ?? -1
const completeResponse = winningCompleteIndex >= 0
? concurrentCompleteResponses[winningCompleteIndex] : concurrentCompleteResponses[0]
const completeBody = winningCompleteIndex >= 0
? completeAttempts[winningCompleteIndex].body : completeAttempts[0].body
const completeKey = winningCompleteIndex >= 0
? completeAttempts[winningCompleteIndex].key : completeAttempts[0].key
const operationsAfterConcurrentResponse = await context.request('GET', `${taskPath}/operations`)
const operationsAfterConcurrent = context.apiSuccess(MODULE,
'并发完成后回查 PDA 操作审计', operationsAfterConcurrentResponse, '', OPTIONS)
const baselineCompleteOperationIds = new Set((operationsBefore || [])
.filter((item) => item?.actionType === 'COMPLETE_LOADING').map((item) => String(item?.operationId || '')))
const concurrentCompleteOperations = (operationsAfterConcurrent || []).filter((item) =>
item?.actionType === 'COMPLETE_LOADING'
&& completeAttempts.some((attempt) => attempt.body.operationId === item?.operationId)
&& !baselineCompleteOperationIds.has(String(item?.operationId || '')))
const concurrentCompleteVerified = successfulCompleteIndexes.length === 1
&& conflictedCompleteIndexes.length === 1
&& Boolean(conflictedCompleteResponse?.requestId) && Boolean(conflictedCompleteResponse?.traceId)
&& Array.isArray(operationsAfterConcurrent) && Array.isArray(operationsBefore)
&& operationsAfterConcurrent.length === operationsBefore.length + 1
&& concurrentCompleteOperations.length === 1
&& concurrentCompleteOperations[0]?.operationId === completeBody.operationId
&& id(concurrentCompleteOperations[0]?.operatorId) && Boolean(concurrentCompleteOperations[0]?.createdAt)
context.assert(MODULE, '两个不同幂等键并发 complete 仅一成功且只写一条完成审计',
completeResponse, concurrentCompleteVerified,
`results=${concurrentCompleteResponses.map((response) => `${response.http}/${codeOf(response)}`).join(',')}, success=${successfulCompleteIndexes.length}, conflict=${conflictedCompleteIndexes.length}, completeAudits=${concurrentCompleteOperations.length}, winner=${completeBody.operationId}`, OPTIONS)
if (completeResponse.http !== 200 || codeOf(completeResponse) !== '200') {
context.addStep(MODULE, '结算递增 inboundVersion 后 final complete 必须使用最新版本', completeResponse, false,
`expected=success after settlement refresh; HTTP=${completeResponse.http}, code=${codeOf(completeResponse)}, message=${completeResponse.body?.message || 'missing'}, taskId=${taskId}, inboundId=${inboundId}, taskVersion=${taskBefore?.version ?? 'missing'}, inboundVersion=${version}, requestId=${completeResponse.requestId || 'missing'}, traceId=${completeResponse.traceId || 'missing'}`, OPTIONS)
@@ -166,6 +203,7 @@ export async function run(context) {
`taskUnchanged=${taskAfterSignature === taskBeforeSignature}, planUnchanged=${planAfterSignature === planBeforeSignature}, inboundStatus=${inboundAfterFailure?.summary?.processStatus || 'missing'}, inboundVersion=${inboundAfterFailure?.summary?.version ?? 'missing'}, operationsUnchanged=${JSON.stringify(operationsAfterFailure || []) === operationsBeforeSignature}`, OPTIONS)
return
}
if (!concurrentCompleteVerified) return
const completed = context.apiSuccess(MODULE, '订舱与五类证据等保留门禁齐备后完成装柜', completeResponse, '', OPTIONS)
context.assert(MODULE, '统一硬门禁通过后 task 与 workflow 同事务完成且身份不漂移', completeResponse,
completed?.taskStatus === 'COMPLETED' && completed?.workflowStatus === 'COMPLETED'
@@ -1133,6 +1133,15 @@ export async function run(context) {
&& Number(partialTaskItem?.pickedPackageCount) === 1
&& partialTask?.workflowStatus === 'AWAITING_REVIEW' && partialTask?.taskStatus === 'LOADING',
`itemStatus=${partialTaskItem?.itemStatus || 'missing'}, planned=${partialTaskItem?.plannedPackageCount ?? 'missing'}, picked=${partialTaskItem?.pickedPackageCount ?? 'missing'}, workflow=${partialTask?.workflowStatus || 'missing'}`, OPTIONS)
const exceptionOverviewResponse = await context.request('GET',
`/api/platform/v1/containers/${encodeURIComponent(containerBookingId)}/overview`)
const exceptionOverview = context.apiSuccess(MODULE,
'回查柜次 PDA 异常原因与已解决进度', exceptionOverviewResponse, '', OPTIONS)
context.assert(MODULE, 'SKIP 异常原因持久化且 PARTIAL 后无未解决明细', exceptionOverviewResponse,
exceptionOverview?.pda?.exceptionReasons?.includes?.(skipReason)
&& Number(exceptionOverview?.pda?.unresolvedItemCount) === 0
&& exceptionOverview?.pda?.workflowStatus === 'AWAITING_REVIEW',
`exceptionReasons=${JSON.stringify(exceptionOverview?.pda?.exceptionReasons || [])}, unresolved=${exceptionOverview?.pda?.unresolvedItemCount ?? 'missing'}, workflow=${exceptionOverview?.pda?.workflowStatus || 'missing'}`, OPTIONS)
}
if (dynamicCandidate?.inboundDetailId) {
const planBeforeIncrementResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
@@ -471,6 +471,8 @@ test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstrea
'未解决异常完成失败后 task/items/operation 全部零写入',
'合法 PARTIAL 解决未完成明细',
'部分拣货数量守恒并恢复最终可确认状态',
'回查柜次 PDA 异常原因与已解决进度',
'SKIP 异常原因持久化且 PARTIAL 后无未解决明细',
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
])
assert.match(loading, /action: 'SKIP', version: Number\(skipTaskItem\?\.version\),\s*reason: skipReason/)
@@ -481,6 +483,8 @@ test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstrea
assert.match(loading, /String\(unresolvedComplete\.body\?\.message \|\| ''\)\.includes\('尚未处理'\)/)
assert.match(loading, /taskItemsSignature\(afterUnresolvedCompleteTask\) === taskItemsSignature\(unresolvedTask\)/)
assert.match(loading, /operationsSignature\(operationsAfterUnresolvedComplete\) === operationsSignature\(operationsBeforeUnresolvedComplete\)/)
assert.match(loading, /exceptionOverview\?\.pda\?\.exceptionReasons\?\.includes\?\.\(skipReason\)/)
assert.match(loading, /Number\(exceptionOverview\?\.pda\?\.unresolvedItemCount\) === 0/)
assert.doesNotMatch(loading, /addCoverageGap\([^)]*(SKIP|PARTIAL|未解决异常)/)
})