diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 53eafd43b..d0dcb8bd5 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -237,33 +237,50 @@ export async function run(context) { const batchHeaders = { 'Idempotency-Key': `${context.marker}-pda-batch`.slice(0, 128) } const pickResponse = await request(context, 'POST', batchPath, batchPayload, batchHeaders) const picked = context.apiSuccess(MODULE, '单次调用 PDA 原子 batch 完成拣货', pickResponse, '', OPTIONS) - const directPdaCompletion = picked?.taskStatus === 'COMPLETED' - && picked?.workflowStatus === 'COMPLETED' - context.assert(MODULE, '最后一箱拣货后 PDA 任务按已部署契约直接完成', pickResponse, - directPdaCompletion + const awaitingReview = picked?.taskStatus === 'LOADING' + && picked?.workflowStatus === 'AWAITING_REVIEW' + context.assert(MODULE, '最后一箱拣货后 PDA 任务等待人工确认装柜完成', pickResponse, + awaitingReview && picked?.items?.every?.((item) => Number(item.pickedPackageCount) === Number(item.plannedPackageCount)), `taskStatus=${picked?.taskStatus || 'missing'}, workflow=${picked?.workflowStatus || 'missing'}`, OPTIONS) const pickReplayResponse = await request(context, 'POST', batchPath, batchPayload, batchHeaders) const pickReplay = context.apiSuccess(MODULE, 'PDA batch Idempotency-Key 幂等重放', pickReplayResponse, '', OPTIONS) - context.assert(MODULE, 'PDA batch 重放保持完成任务和拣货数', pickReplayResponse, - pickReplay?.taskStatus === 'COMPLETED' && pickReplay?.workflowStatus === 'COMPLETED' + context.assert(MODULE, 'PDA batch 重放保持待确认任务和拣货数', pickReplayResponse, + pickReplay?.taskStatus === 'LOADING' && pickReplay?.workflowStatus === 'AWAITING_REVIEW' && pickReplay?.items?.every?.((item) => Number(item.pickedPackageCount) === Number(item.plannedPackageCount)), `taskStatus=${pickReplay?.taskStatus || 'missing'}`, OPTIONS) + const completePath = `${PDA}/tasks/${taskId}/complete` + const completePayload = { + expectedTaskVersion: Number(picked?.version), + operationId: `${context.marker}-pda-complete`.slice(0, 128), + remark: context.marker, + } + const completeResponse = await request(context, 'POST', completePath, completePayload) + const completed = context.apiSuccess(MODULE, 'PDA 人工确认装柜完成', completeResponse, '', OPTIONS) + const directPdaCompletion = completed?.taskStatus === 'COMPLETED' + && completed?.workflowStatus === 'COMPLETED' + context.assert(MODULE, '人工确认后 PDA 任务进入 COMPLETED', completeResponse, + directPdaCompletion + && completed?.items?.every?.((item) => Number(item.pickedPackageCount) === Number(item.plannedPackageCount)), + `taskStatus=${completed?.taskStatus || 'missing'}, workflow=${completed?.workflowStatus || 'missing'}`, OPTIONS) + const operationsResponse = await request(context, 'GET', `${PDA}/tasks/${taskId}/operations`) const operations = context.apiSuccess(MODULE, '回查完成后的 PDA 操作审计', operationsResponse, '', OPTIONS) - context.assert(MODULE, 'PDA 完成审计保留领取和原子批次操作者记录', operationsResponse, + context.assert(MODULE, 'PDA 完成审计保留领取、原子批次和人工完成记录', operationsResponse, Array.isArray(operations) && operations.some((item) => item.actionType === 'CLAIM' && Boolean(item.operatorId) && Boolean(item.createdAt)) && operations.some((item) => item.actionType === 'BATCH' && Boolean(item.operatorId) && Boolean(item.createdAt)) + && operations.some((item) => item.actionType === 'COMPLETE_LOADING' + && Boolean(item.operatorId) && Boolean(item.createdAt)) && !operations.some((item) => item.actionType === 'REVIEW'), `actions=${Array.isArray(operations) ? operations.map((item) => item.actionType).join(',') : 'missing'}`, OPTIONS) const reviewResponse = await request(context, 'POST', `${PDA}/tasks/${taskId}/review`, { - expectedTaskVersion: Number(picked?.version), operationId: `${context.marker}-pda-review`.slice(0, 128), + expectedTaskVersion: Number(completed?.version), operationId: `${context.marker}-pda-review`.slice(0, 128), remark: context.marker, }) context.addStep(MODULE, 'PDA 已完成后拒绝重复复核', reviewResponse, 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 6c90a63a6..d768a6d9f 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 @@ -73,8 +73,13 @@ test('container loading follows push, claim, real pick, completed sequence', () "pushed?.status === 'EDITING'", "claimedPlan?.status === 'LOADING'", "const pickResponse = await request", + "const completeResponse = await request", "finalPlan?.status === 'COMPLETED'", ]) + assert.match(value, /picked\?\.workflowStatus === 'AWAITING_REVIEW'/) + assert.match(value, /`\$\{PDA\}\/tasks\/\$\{taskId\}\/complete`/) + assert.match(value, /expectedTaskVersion: Number\(picked\?\.version\)/) + assert.match(value, /operationId: `\$\{context\.marker\}-pda-complete`\.slice\(0, 128\)/) }) test('bridge loading starts through a real PDA batch before preload confirmation', () => { @@ -119,11 +124,12 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap' assert.match(convergence, /fixture\?\.inventoryMutationVerified !== true/) }) -test('PDA completion acceptance verifies immutable BATCH audit with operator metadata', () => { +test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => { const container = source('./phase3-container-loading.mjs') const convergence = source('./phase3-pda-convergence.mjs') assert.match(container, /回查完成后的 PDA 操作审计/) assert.match(container, /item\.actionType === 'BATCH'/) + assert.match(container, /item\.actionType === 'COMPLETE_LOADING'/) assert.match(container, /Boolean\(item\.operatorId\) && Boolean\(item\.createdAt\)/) assert.match(convergence, /PDA 原子批次完成审计包含 BATCH 操作/) assert.match(convergence, /item\.actionType === 'BATCH'/)