test: verify complete PDA batch history
This commit is contained in:
@@ -45,6 +45,7 @@ test('default fixture publishes a second marker-owned inbound for the same-plan
|
||||
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)
|
||||
@@ -74,6 +75,10 @@ test('HTTP chain refreshes after stale inventory and keeps one stable PDA identi
|
||||
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=/)
|
||||
})
|
||||
|
||||
@@ -566,6 +566,10 @@ export async function run(context) {
|
||||
|
||||
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
|
||||
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
|
||||
const skipBatchKey = `${context.marker}-exception-skip-batch`.slice(0, 128)
|
||||
const partialBatchKey = `${context.marker}-exception-partial-batch`.slice(0, 128)
|
||||
const dynamicBatchKey = `${context.marker}-dynamic-pick`.slice(0, 128)
|
||||
const expectedBatchOperationIds = [batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey]
|
||||
const batchPayload = {
|
||||
warehouseId, loadingPlanId, containerBookingId,
|
||||
expectedVersion: Number(claimed?.version),
|
||||
@@ -1021,6 +1025,8 @@ export async function run(context) {
|
||||
|
||||
const incrementalFixture = context.createdResources?.orchestrationFixtures?.phase3LoadingIncremental
|
||||
const incrementalInboundId = id(incrementalFixture?.inboundId)
|
||||
let skipBatchVerified = false
|
||||
let partialBatchVerified = false
|
||||
let dynamicIncrementVerified = false
|
||||
if (!incrementalInboundId || incrementalFixture?.marker !== context.marker || incrementalInboundId === inboundId) {
|
||||
context.addStep(MODULE, '取得本轮独立动态增量入库单', null, false,
|
||||
@@ -1068,10 +1074,10 @@ export async function run(context) {
|
||||
expectedVersion: Number(skipTask?.version), inventorySnapshotToken: skipInventory?.snapshotToken,
|
||||
items: [{ itemId: id(skipTaskItem?.id), action: 'SKIP', version: Number(skipTaskItem?.version),
|
||||
reason: skipReason }],
|
||||
}, { 'Idempotency-Key': `${context.marker}-exception-skip-batch`.slice(0, 128) })
|
||||
}, { 'Idempotency-Key': skipBatchKey })
|
||||
const skippedTask = context.apiSuccess(MODULE, 'SKIP 携带异常原因并原子解决待拣明细', skipBatchResponse, '', OPTIONS)
|
||||
const skippedTaskItem = skippedTask?.items?.find?.((item) => id(item?.id) === id(skipTaskItem?.id))
|
||||
context.assert(MODULE, '合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW', skipBatchResponse,
|
||||
skipBatchVerified = context.assert(MODULE, '合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW', skipBatchResponse,
|
||||
Boolean(skipReason.trim()) && skippedTaskItem?.itemStatus === 'SKIPPED'
|
||||
&& Number(skippedTaskItem?.pickedPackageCount) === 0
|
||||
&& skippedTask?.workflowStatus === 'AWAITING_REVIEW' && skippedTask?.taskStatus === 'LOADING',
|
||||
@@ -1125,10 +1131,10 @@ export async function run(context) {
|
||||
inventorySnapshotToken: partialInventory?.snapshotToken,
|
||||
items: [{ itemId: id(unresolvedTaskItem?.id), action: 'PARTIAL', finalPickedPackageCount: 1,
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(unresolvedTaskItem?.version) }],
|
||||
}, { 'Idempotency-Key': `${context.marker}-exception-partial-batch`.slice(0, 128) })
|
||||
}, { 'Idempotency-Key': partialBatchKey })
|
||||
const partialTask = context.apiSuccess(MODULE, '合法 PARTIAL 解决未完成明细', partialBatchResponse, '', OPTIONS)
|
||||
const partialTaskItem = partialTask?.items?.find?.((item) => id(item?.id) === id(unresolvedTaskItem?.id))
|
||||
context.assert(MODULE, '部分拣货数量守恒并恢复最终可确认状态', partialBatchResponse,
|
||||
partialBatchVerified = context.assert(MODULE, '部分拣货数量守恒并恢复最终可确认状态', partialBatchResponse,
|
||||
partialTaskItem?.itemStatus === 'PARTIAL' && Number(partialTaskItem?.plannedPackageCount) === 2
|
||||
&& Number(partialTaskItem?.pickedPackageCount) === 1
|
||||
&& partialTask?.workflowStatus === 'AWAITING_REVIEW' && partialTask?.taskStatus === 'LOADING',
|
||||
@@ -1226,7 +1232,6 @@ export async function run(context) {
|
||||
context.assert(MODULE, '动态增量 fixture 至少保留两件可用数量供调整与最小已拣门禁', dynamicCandidatesResponse,
|
||||
Number(dynamicCandidate?.availablePackageCount) >= 2,
|
||||
`availablePackageCount=${dynamicCandidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
|
||||
if (Number(dynamicCandidate?.availablePackageCount) < 2) return
|
||||
if (Number(dynamicCandidate?.availablePackageCount) >= 2 && reassignedPlanItem?.id) {
|
||||
const adjustResponse = await request(context, 'PATCH',
|
||||
`${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(reassignedPlanItem.id))}`, {
|
||||
@@ -1259,7 +1264,7 @@ export async function run(context) {
|
||||
inventorySnapshotToken: refreshedDynamicInventory?.snapshotToken,
|
||||
items: [{ itemId: id(refreshedDynamicTaskItem?.id), action: 'PICK', pickedPackageCount: Number(reassignedPlanItem?.packageCount),
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(refreshedDynamicTaskItem?.version) }],
|
||||
}, { 'Idempotency-Key': `${context.marker}-dynamic-pick`.slice(0, 128) })
|
||||
}, { 'Idempotency-Key': dynamicBatchKey })
|
||||
const dynamicPickedTask = context.apiSuccess(MODULE, '刷新后完成动态新增项拣货', dynamicBatchResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '动态新增项拣完后同一任务重新进入 AWAITING_REVIEW', dynamicBatchResponse,
|
||||
id(dynamicPickedTask?.id) === canonicalTaskId && dynamicPickedTask?.workflowStatus === 'AWAITING_REVIEW'
|
||||
@@ -1341,7 +1346,8 @@ export async function run(context) {
|
||||
atomicBatchVerified: awaitingReview && atomicBatchReplayVerified,
|
||||
inventorySnapshotVerified: inventorySnapshotVerified && atomicBatchReplayVerified,
|
||||
inventoryMutationVerified, inventoryMutationCleanup,
|
||||
dynamicIncrementVerified, incrementalInboundId,
|
||||
skipBatchVerified, partialBatchVerified, dynamicIncrementVerified,
|
||||
expectedBatchOperationIds, incrementalInboundId,
|
||||
pdaTaskVersion: Number(stagedTask?.version),
|
||||
marker: context.marker, inboundStatus: 'LOADING', loadingPlanStatus: 'LOADING', finalStatus: 'AWAITING_REVIEW',
|
||||
}
|
||||
|
||||
@@ -82,7 +82,9 @@ export async function run(context) {
|
||||
const batchOperations = Array.isArray(operations)
|
||||
? operations.filter((item) => item.actionType === 'BATCH'
|
||||
&& Boolean(item.operatorId) && Boolean(item.createdAt)) : []
|
||||
const expectedBatchCount = fixture?.dynamicIncrementVerified === true ? 2 : 1
|
||||
const expectedBatchOperationIds = Array.isArray(fixture?.expectedBatchOperationIds)
|
||||
? fixture.expectedBatchOperationIds.map(id).filter(Boolean) : []
|
||||
const actualBatchOperationIds = batchOperations.map((item) => id(item.operationId)).filter(Boolean)
|
||||
context.assert(MODULE, '最终完成前保留领取操作且不伪造复核或完成历史', operationsResponse,
|
||||
Array.isArray(operations) && operations.some((item) => item.actionType === 'CLAIM'
|
||||
&& Boolean(item.operatorId) && Boolean(item.createdAt))
|
||||
@@ -91,9 +93,13 @@ export async function run(context) {
|
||||
`actions=${Array.isArray(operations) ? operations.map((item) => item.actionType).join(',') : 'missing'}`, OPTIONS)
|
||||
|
||||
context.assert(MODULE, 'PDA 原子批次完成审计包含 BATCH 操作', operationsResponse,
|
||||
batchOperations.length === expectedBatchCount
|
||||
&& new Set(batchOperations.map((item) => item.operationId)).size === expectedBatchCount,
|
||||
`expectedBatchCount=${expectedBatchCount}, actualBatchCount=${batchOperations.length}, operationIds=${batchOperations.map((item) => item.operationId || 'missing').join(',')}`, OPTIONS)
|
||||
expectedBatchOperationIds.length === 4
|
||||
&& new Set(expectedBatchOperationIds).size === 4
|
||||
&& actualBatchOperationIds.length === 4
|
||||
&& new Set(actualBatchOperationIds).size === 4
|
||||
&& expectedBatchOperationIds.every((operationId) => actualBatchOperationIds.includes(operationId))
|
||||
&& actualBatchOperationIds.every((operationId) => expectedBatchOperationIds.includes(operationId)),
|
||||
`expectedBatchCount=${expectedBatchOperationIds.length}, actualBatchCount=${actualBatchOperationIds.length}, expectedIds=${expectedBatchOperationIds.join(',') || 'missing'}, actualIds=${actualBatchOperationIds.join(',') || 'missing'}`, OPTIONS)
|
||||
const forged = await request(context, 'POST',
|
||||
`/api/platform/v1/inbounds/${inboundId}/loading/picking/start?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(projection?.version), expectedCount: 999, pickedCount: 999, remark: 'forged' },
|
||||
@@ -127,6 +133,10 @@ export async function run(context) {
|
||||
context.addStep(MODULE, 'LOADING 增量重开、刷新续拣与已拣不可删减', null, false,
|
||||
'phase3-loading 未发布 dynamicIncrementVerified=true;不能跳过增量重开、旧 token A0409、刷新续拣和已拣项保护', OPTIONS)
|
||||
}
|
||||
if (fixture?.skipBatchVerified !== true || fixture?.partialBatchVerified !== true) {
|
||||
context.addStep(MODULE, 'SKIP 与 PARTIAL 独立原子批次均完成', null, false,
|
||||
`skipBatchVerified=${fixture?.skipBatchVerified === true}, partialBatchVerified=${fixture?.partialBatchVerified === true};不能按普通 PICK 批次代替异常处理审计`, OPTIONS)
|
||||
}
|
||||
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA Android/HBuilderX 真机完整闭环',
|
||||
'原子 batch 真实 HTTP 契约已自动化;扫码、弱网恢复、在线草稿和设备交互仍需 Android/HBuilderX 真机验收',
|
||||
'device')
|
||||
|
||||
@@ -25,8 +25,12 @@ test('AWAITING_REVIEW derives zero client actions and never needs a repeated bat
|
||||
assert.match(convergence, /AWAITING_REVIEW 已全拣任务 actions 为空且不再提交 batch/)
|
||||
assert.match(convergence, /AWAITING_REVIEW 可进入装柜取证只读入口/)
|
||||
assert.match(convergence, /filter\(\(item\) => item\.actionType === 'BATCH'/)
|
||||
assert.match(convergence, /expectedBatchCount = fixture\?\.dynamicIncrementVerified === true \? 2 : 1/)
|
||||
assert.match(convergence, /new Set\(batchOperations\.map\(\(item\) => item\.operationId\)\)\.size === expectedBatchCount/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.length === 4/)
|
||||
assert.match(convergence, /actualBatchOperationIds\.length === 4/)
|
||||
assert.match(convergence, /new Set\(expectedBatchOperationIds\)\.size === 4/)
|
||||
assert.match(convergence, /new Set\(actualBatchOperationIds\)\.size === 4/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.every[\s\S]*actualBatchOperationIds\.every/)
|
||||
assert.doesNotMatch(convergence, /dynamicIncrementVerified === true \? 2 : 1/)
|
||||
assert.match(convergence, /LOADING 增量重开、刷新续拣与已拣不可删减/)
|
||||
assert.doesNotMatch(convergence, /pendingActions[\s\S]{0,240}request\(context, 'POST'/)
|
||||
})
|
||||
|
||||
@@ -406,6 +406,7 @@ test('preload and loading versions use immediate operation baselines and exact p
|
||||
})
|
||||
|
||||
test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => {
|
||||
const loading = source('./phase3-loading.mjs')
|
||||
const container = source('./phase3-container-loading.mjs')
|
||||
const convergence = source('./phase3-pda-convergence.mjs')
|
||||
assertOrder(container, [
|
||||
@@ -426,6 +427,13 @@ test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING au
|
||||
assert.match(container, /Boolean\(item\.operatorId\) && Boolean\(item\.createdAt\)/)
|
||||
assert.match(convergence, /PDA 原子批次完成审计包含 BATCH 操作/)
|
||||
assert.match(convergence, /item\.actionType === 'BATCH'/)
|
||||
for (const key of ['bridge-batch', 'exception-skip-batch', 'exception-partial-batch', 'dynamic-pick']) {
|
||||
assert.match(loading, new RegExp(key))
|
||||
}
|
||||
assert.match(loading, /expectedBatchOperationIds = \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
|
||||
assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.length === 4/)
|
||||
assert.doesNotMatch(convergence, /expectedBatchCount = fixture\?\.dynamicIncrementVerified/)
|
||||
})
|
||||
|
||||
test('phase3 loading proves mixed-line rollback across task, all items and operations', () => {
|
||||
@@ -493,6 +501,9 @@ test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstrea
|
||||
assert.match(measurement, /detailBoxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-\$\{cartonIndex\}`/)
|
||||
assert.match(measurement, /items: draftItems/)
|
||||
assert.match(measurement, /expectedPackages === originalPackageCount/)
|
||||
assert.match(loading, /skipBatchVerified = context\.assert/)
|
||||
assert.match(loading, /partialBatchVerified = context\.assert/)
|
||||
assert.doesNotMatch(loading, /if \(Number\(dynamicCandidate\?\.availablePackageCount\) < 2\) return/)
|
||||
})
|
||||
|
||||
test('phase3 loading fixture forces A0 through reversible public location status APIs', () => {
|
||||
|
||||
Reference in New Issue
Block a user