This commit is contained in:
2026-08-11 11:41:41 +08:00
parent 9f455b6c0b
commit 9b3ff06a90
17 changed files with 429 additions and 76 deletions
@@ -1,5 +1,7 @@
const MODULE = 'phase3-loading'
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
@@ -227,20 +229,102 @@ export async function run(context) {
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
// P3-Q11 uses task-level assignment as the single-operator guard; no item claim API exists.
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
const batchPayload = {
warehouseId, containerNo,
expectedVersion: Number(claimed?.version),
items: [{
itemId: id(canonicalItem.id), action: 'PICK', pickedPackageCount: 1,
releaseMode: 'MANUAL', locationIds: [], version: Number(canonicalItem.version),
}],
}
const canonicalPick = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/pick`, {
warehouseId, pickedPackageCount: 1, locationIds: [], version: Number(canonicalItem.version),
operationId: `${context.marker}-bridge-pick`.slice(0, 128),
const staleBatch = await request(context, 'POST', batchPath,
{ ...batchPayload, expectedVersion: Number(batchPayload.expectedVersion) + 1 },
{ 'Idempotency-Key': `${context.marker}-bridge-batch-stale`.slice(0, 128) })
context.addStep(MODULE, 'PDA 原子批次旧任务版本返回冲突', staleBatch,
staleBatch.http === 200 && codeOf(staleBatch) === 'A0409',
`HTTP=${staleBatch.http}, code=${codeOf(staleBatch)}`, OPTIONS)
const invalidBatch = await request(context, 'POST', batchPath, {
...batchPayload,
items: [...batchPayload.items, {
itemId: '999999999999999999', action: 'SKIP', version: 0, reason: 'forged-item-must-rollback',
}],
}, { 'Idempotency-Key': `${context.marker}-bridge-batch-rollback`.slice(0, 128) })
context.addStep(MODULE, 'PDA 原子批次任一非法明细时整批拒绝', invalidBatch,
invalidBatch.http === 200 && codeOf(invalidBatch) === 'A0409',
`HTTP=${invalidBatch.http}, code=${codeOf(invalidBatch)}`, OPTIONS)
const unchangedAfterInvalidResponse = await context.request('GET',
`${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const unchangedAfterInvalid = context.apiSuccess(MODULE, '回查失败批次后的 PDA 任务快照', unchangedAfterInvalidResponse, '', OPTIONS)
const unchangedItem = unchangedAfterInvalid?.items?.find?.((item) => id(item.id) === id(canonicalItem.id))
context.assert(MODULE, '失败批次整批回滚且无明细部分写入', unchangedAfterInvalidResponse,
unchangedAfterInvalid?.taskStatus === 'LOADING'
&& Number(unchangedAfterInvalid?.version) === Number(batchPayload.expectedVersion)
&& unchangedItem?.itemStatus === 'PENDING' && Number(unchangedItem?.pickedPackageCount) === 0
&& Number(unchangedItem?.version) === Number(canonicalItem.version),
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, itemStatus=${unchangedItem?.itemStatus || 'missing'}, picked=${unchangedItem?.pickedPackageCount ?? 'missing'}`, OPTIONS)
const merchantLogin = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantToken = merchantLogin.http === 200 && codeOf(merchantLogin) === '200'
? merchantLogin.body?.data?.accessToken : null
context.addStep(MODULE, '取得客户侧隔离令牌用于 PDA 批次越权验证', merchantLogin,
Boolean(merchantToken), `username=${MERCHANT_USERNAME}`, OPTIONS)
const customerIsolatedBatch = merchantToken
? await request(context, 'POST', batchPath, batchPayload, {
Authorization: `Bearer ${merchantToken}`,
'Idempotency-Key': `${context.marker}-bridge-batch-customer`.slice(0, 128),
})
const pickedCanonical = context.apiSuccess(MODULE, '执行柜次 PDA 明细拣货', canonicalPick, '', OPTIONS)
: merchantLogin
context.addStep(MODULE, '客户身份无权提交平台 PDA 原子批次', customerIsolatedBatch,
customerIsolatedBatch.http === 403 && codeOf(customerIsolatedBatch) === 'A0403'
&& Boolean(customerIsolatedBatch.requestId) && Boolean(customerIsolatedBatch.traceId),
`HTTP=${customerIsolatedBatch.http}, code=${codeOf(customerIsolatedBatch)}, requestId=${customerIsolatedBatch.requestId || 'missing'}, traceId=${customerIsolatedBatch.traceId || 'missing'}`, OPTIONS)
const currentAssigneeId = Number(claimed?.assignment?.assigneeId)
const reassigned = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/assign`, {
assigneeId: currentAssigneeId + 1000000, assigneeName: `${context.marker}-other-picker`,
expectedAssignmentVersion: Number(claimed?.assignment?.version),
operationId: `${context.marker}-bridge-assign-other`.slice(0, 128),
})
const otherAssignment = context.apiSuccess(MODULE, '将 PDA 任务改派给另一作业员', reassigned, '', OPTIONS)
const operatorIsolatedBatch = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': `${context.marker}-bridge-batch-other-operator`.slice(0, 128) })
context.addStep(MODULE, '非任务作业员提交 PDA 原子批次返回冲突', operatorIsolatedBatch,
operatorIsolatedBatch.http === 200 && codeOf(operatorIsolatedBatch) === 'A0409',
`HTTP=${operatorIsolatedBatch.http}, code=${codeOf(operatorIsolatedBatch)}`, OPTIONS)
const restoredAssignmentResponse = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/assign`, {
assigneeId: currentAssigneeId, assigneeName: claimed?.assignment?.assigneeName || 'admin',
expectedAssignmentVersion: Number(otherAssignment?.assignment?.version),
operationId: `${context.marker}-bridge-assign-restore`.slice(0, 128),
})
const restoredAssignment = context.apiSuccess(MODULE, '恢复 PDA 任务到当前作业员', restoredAssignmentResponse, '', OPTIONS)
context.assert(MODULE, '作业员隔离验证后任务归属恢复', restoredAssignmentResponse,
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
const canonicalPick = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': batchKey })
const pickedCanonical = context.apiSuccess(MODULE, '原子提交柜次 PDA 整批拣货', canonicalPick, '', OPTIONS)
const directPdaCompletion = pickedCanonical?.taskStatus === 'COMPLETED'
&& pickedCanonical?.workflowStatus === 'COMPLETED'
context.assert(MODULE, '柜次 PDA 明细完成后按已部署契约推进任务终态', canonicalPick,
directPdaCompletion,
context.assert(MODULE, 'PDA 原子批次成功并推进任务终态', canonicalPick,
directPdaCompletion && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
`taskStatus=${pickedCanonical?.taskStatus || 'missing'}, workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
const canonicalBatchReplay = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': batchKey })
context.assert(MODULE, 'PDA 原子批次同参同键重放返回同一终态', canonicalBatchReplay,
canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
&& id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId
&& canonicalBatchReplay.body?.data?.taskStatus === 'COMPLETED'
&& Number(canonicalBatchReplay.body?.data?.version) === Number(pickedCanonical?.version),
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
// The deployed settlement contract completes a one-item task during pick. A
// review call is therefore a negative compatibility assertion, never a second
// positive action that can poison the marker fixture with A0409.