Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # order/AGENTS.md
This commit is contained in:
@@ -172,7 +172,7 @@ node --test scripts/admin-test-env-modules/suppliers-contract.test.mjs
|
||||
|
||||
## 国内整柜与客户运营统计
|
||||
|
||||
- `domestic-container.mjs`:整柜入库、出库、库存列表和详情;静态契约为 `domestic-container-contract.test.mjs`。
|
||||
- `domestic-container.mjs`:整柜入库、出库、库存列表和详情,并覆盖字段边界、组合筛选与分页汇总、柜号标准化、同柜追加入库、货名冲突、完成出库强制库存版本、并发版本竞争和幂等重放;静态契约为 `domestic-container-contract.test.mjs`。
|
||||
- `customer-operating-statistics.mjs`:客户详情在途柜量/TEU 运营指标,复用 `platform:firstleg:container:list` 权限。
|
||||
- 两者由总入口 `admin-test-env-acceptance.mjs` 的默认模块列表编排;接口验收会写入测试数据,静态合同可离线执行。
|
||||
|
||||
|
||||
@@ -819,11 +819,9 @@ export async function run(context) {
|
||||
const skippedInboundId = id(partialTarget.inboundId)
|
||||
const partialBatchItems = (partialPda.task.items || []).map((item) => id(item.inboundId) === skippedInboundId
|
||||
? { itemId: id(item.id), action: 'SKIP', pickedPackageCount: null,
|
||||
finalPickedPackageCount: null, releaseMode: null,
|
||||
locationIds: [], version: Number(item.version), reason: '同柜不可拆组短拣验收' }
|
||||
finalPickedPackageCount: null, version: Number(item.version), reason: '同柜不可拆组短拣验收' }
|
||||
: { itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount),
|
||||
finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [],
|
||||
version: Number(item.version), reason: null })
|
||||
finalPickedPackageCount: null, version: Number(item.version), reason: null })
|
||||
const partialBatchResponse = await context.request('POST', `${PDA}/tasks/${partialPda.taskId}/batch`, {
|
||||
warehouseId: first.warehouseId, loadingPlanId: partialChain.planId,
|
||||
containerBookingId: partialChain.containerBookingId, expectedVersion: Number(partialPda.task.version),
|
||||
@@ -863,8 +861,7 @@ export async function run(context) {
|
||||
const fullTreesBefore = await fulfillmentTrees(context, fullChain.fixtures, '足量完成前')
|
||||
const fullBatchItems = (fullPda.task?.items || []).map((item) => ({
|
||||
itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount),
|
||||
finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [],
|
||||
version: Number(item.version), reason: null,
|
||||
finalPickedPackageCount: null, version: Number(item.version), reason: null,
|
||||
}))
|
||||
const fullBatchResponse = await context.request('POST', `${PDA}/tasks/${fullPda.taskId}/batch`, {
|
||||
warehouseId: third.warehouseId, loadingPlanId: fullChain.planId,
|
||||
@@ -910,8 +907,7 @@ export async function run(context) {
|
||||
const normalTreeBefore = await fulfillmentTrees(context, normalChain.fixtures, '普通单完成前')
|
||||
const normalBatchItems = (normalPda.task?.items || []).map((item) => ({
|
||||
itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount),
|
||||
finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [],
|
||||
version: Number(item.version), reason: null,
|
||||
finalPickedPackageCount: null, version: Number(item.version), reason: null,
|
||||
}))
|
||||
const normalBatchResponse = await context.request('POST', `${PDA}/tasks/${normalPda.taskId}/batch`, {
|
||||
warehouseId: seventh.warehouseId, loadingPlanId: normalChain.planId,
|
||||
|
||||
@@ -28,8 +28,9 @@ test('覆盖国内仓、待单、完成入库、部分和全部出库及列表
|
||||
|
||||
test('在创建测试数据前覆盖无筛选整柜入库列表', () => {
|
||||
const unfilteredList = '`${BASE}/inbounds?page=1&size=20`'
|
||||
const runSource = moduleSource.slice(moduleSource.indexOf('export async function run(context)'))
|
||||
assert.match(moduleSource, /无筛选查询整柜入库单列表/)
|
||||
assert.ok(moduleSource.indexOf(unfilteredList) < moduleSource.indexOf("api(context, 'POST', `${BASE}/inbounds`"))
|
||||
assert.ok(runSource.indexOf(unfilteredList) < runSource.indexOf("api(context, 'POST', `${BASE}/inbounds`"))
|
||||
})
|
||||
|
||||
test('覆盖库存不足、版本冲突及两类幂等行为', () => {
|
||||
@@ -46,3 +47,20 @@ test('覆盖权限拒绝与 requestId traceId', () => {
|
||||
assert.match(moduleSource, /Boolean\(response\?\.requestId\).*Boolean\(response\?\.traceId\)/s)
|
||||
assert.match(moduleSource, /status: 'DISABLED'/)
|
||||
})
|
||||
|
||||
test('扩展覆盖字段边界、组合筛选、标准化、分页汇总和详情不存在', () => {
|
||||
for (const token of ['集装箱号格式非法被拒绝', '零箱入库被拒绝', '零体积入库被拒绝',
|
||||
'非法入库状态被拒绝', '空幂等键被拒绝', '超长备注被拒绝', '非法入库分页大小被拒绝',
|
||||
'非法库存状态筛选被拒绝', '同仓同柜同货名追加入库', '同仓同柜不同货名禁止合并库存',
|
||||
'筛选汇总不受分页大小影响且柜号已标准化', '入库单筛选总数与分页记录正确',
|
||||
'不存在的整柜库存详情返回未找到']) assert.match(moduleSource, new RegExp(token))
|
||||
})
|
||||
|
||||
test('扩展覆盖强制库存版本、并发竞争和成功请求幂等重放', () => {
|
||||
assert.match(moduleSource, /完成出库必须携带库存版本/)
|
||||
assert.match(moduleSource, /缺失库存版本的请求不得扣减库存/)
|
||||
assert.match(moduleSource, /Promise\.all\(raceBodies\.map/)
|
||||
assert.match(moduleSource, /同一库存版本并发出库仅一个成功/)
|
||||
assert.match(moduleSource, /并发出库只扣减一次并只新增一条流水/)
|
||||
assert.match(moduleSource, /并发成功请求重放不重复扣减/)
|
||||
})
|
||||
|
||||
@@ -20,6 +20,102 @@ async function api(context, method, path, body, token = context.token) {
|
||||
return response
|
||||
}
|
||||
|
||||
async function verifyValidationBoundaries(context, warehouseId, marker, now) {
|
||||
const valid = {
|
||||
warehouseId, cargoName: marker, containerNo: `DCV${marker}`.slice(0, 32).toUpperCase(), boxCount: 1,
|
||||
volumeCbm: 1, inboundTime: now, status: 'COMPLETED', remark: marker,
|
||||
}
|
||||
const cases = [
|
||||
['集装箱号格式非法被拒绝', { ...valid, containerNo: 'bad!', idempotencyKey: `${marker}-invalid-container` }],
|
||||
['零箱入库被拒绝', { ...valid, boxCount: 0, idempotencyKey: `${marker}-zero-boxes` }],
|
||||
['零体积入库被拒绝', { ...valid, volumeCbm: 0, idempotencyKey: `${marker}-zero-volume` }],
|
||||
['非法入库状态被拒绝', { ...valid, status: 'UNKNOWN', idempotencyKey: `${marker}-invalid-status` }],
|
||||
['空幂等键被拒绝', { ...valid, idempotencyKey: ' ' }],
|
||||
['超长备注被拒绝', { ...valid, remark: 'R'.repeat(501), idempotencyKey: `${marker}-long-remark` }],
|
||||
]
|
||||
for (const [label, body] of cases) {
|
||||
const response = await api(context, 'POST', `${BASE}/inbounds`, body)
|
||||
expectCode(context, label, response, 'A0422')
|
||||
}
|
||||
expectCode(context, '非法入库分页大小被拒绝',
|
||||
await api(context, 'GET', `${BASE}/inbounds?page=1&size=201`), 'A0422')
|
||||
expectCode(context, '非法库存状态筛选被拒绝',
|
||||
await api(context, 'GET', `${BASE}/inventories?page=1&size=20&status=UNKNOWN`), 'A0422')
|
||||
}
|
||||
|
||||
async function verifyExtendedInventoryScenarios(context, warehouseId, marker, now) {
|
||||
const containerNo = `DCX${marker}`.slice(0, 32).toUpperCase()
|
||||
const cargoName = `${marker}-extended`.slice(0, 120)
|
||||
const firstBody = { warehouseId, cargoName, containerNo, boxCount: 40, volumeCbm: 20,
|
||||
inboundTime: now, status: 'COMPLETED', remark: `${marker}-extended-1`, idempotencyKey: `${marker}-extended-1` }
|
||||
context.apiSuccess(MODULE, '扩展场景首次完成入库', await api(context, 'POST', `${BASE}/inbounds`, firstBody))
|
||||
context.apiSuccess(MODULE, '同仓同柜同货名追加入库', await api(context, 'POST', `${BASE}/inbounds`, {
|
||||
...firstBody, cargoName: ` ${cargoName} `, containerNo: containerNo.toLowerCase(), boxCount: 10, volumeCbm: 5,
|
||||
remark: `${marker}-extended-2`, idempotencyKey: `${marker}-extended-2`,
|
||||
}))
|
||||
expectCode(context, '同仓同柜不同货名禁止合并库存', await api(context, 'POST', `${BASE}/inbounds`, {
|
||||
...firstBody, cargoName: `${cargoName}-other`, boxCount: 1, volumeCbm: 1,
|
||||
idempotencyKey: `${marker}-cargo-conflict`,
|
||||
}), 'A0409')
|
||||
|
||||
const filteredResponse = await api(context, 'GET', `${BASE}/inventories?page=1&size=1&warehouseId=${warehouseId}&containerNo=${containerNo.toLowerCase()}&cargoName=${encodeURIComponent(cargoName)}&status=IN_STOCK`)
|
||||
const filtered = context.apiSuccess(MODULE, '组合条件筛选扩展库存', filteredResponse)
|
||||
const inventory = filtered?.records?.[0]
|
||||
context.assert(MODULE, '筛选汇总不受分页大小影响且柜号已标准化', filteredResponse,
|
||||
Number(filtered?.total) === 1 && filtered?.records?.length === 1 && inventory?.containerNo === containerNo
|
||||
&& Number(filtered?.summary?.inboundBoxes) === 50 && Number(filtered?.summary?.availableBoxes) === 50
|
||||
&& Number(filtered?.summary?.inboundVolumeCbm) === 25,
|
||||
`total=${filtered?.total}, records=${filtered?.records?.length}, containerNo=${inventory?.containerNo}, summary=${JSON.stringify(filtered?.summary)}`)
|
||||
if (!inventory?.id) throw new Error('扩展库存未创建成功')
|
||||
|
||||
const inboundListResponse = await api(context, 'GET', `${BASE}/inbounds?page=1&size=1&warehouseId=${warehouseId}&containerNo=${containerNo}&cargoName=${encodeURIComponent(cargoName)}&status=COMPLETED`)
|
||||
const inboundList = context.apiSuccess(MODULE, '组合条件筛选扩展入库单', inboundListResponse)
|
||||
context.assert(MODULE, '入库单筛选总数与分页记录正确', inboundListResponse,
|
||||
Number(inboundList?.total) === 2 && inboundList?.records?.length === 1
|
||||
&& inboundList.records[0]?.status === 'COMPLETED' && inboundList.records[0]?.businessType === 'INBOUND',
|
||||
`total=${inboundList?.total}, records=${inboundList?.records?.length}, status=${inboundList?.records?.[0]?.status}`)
|
||||
|
||||
const beforeMissingVersion = await api(context, 'GET', `${BASE}/inventories/${inventory.id}`)
|
||||
const beforeDetail = context.apiSuccess(MODULE, '缺失版本测试前读取库存详情', beforeMissingVersion)
|
||||
expectCode(context, '完成出库必须携带库存版本', await api(context, 'POST', `${BASE}/outbounds`, {
|
||||
warehouseId, containerNo, boxCount: 1, outboundTime: now, status: 'COMPLETED',
|
||||
idempotencyKey: `${marker}-missing-version`,
|
||||
}), 'A0422')
|
||||
const afterMissingVersion = context.apiSuccess(MODULE, '缺失版本请求后回查库存',
|
||||
await api(context, 'GET', `${BASE}/inventories/${inventory.id}`))
|
||||
context.assert(MODULE, '缺失库存版本的请求不得扣减库存', beforeMissingVersion,
|
||||
Number(afterMissingVersion?.inventory?.availableBoxes) === Number(beforeDetail?.inventory?.availableBoxes),
|
||||
`before=${beforeDetail?.inventory?.availableBoxes}, after=${afterMissingVersion?.inventory?.availableBoxes}`)
|
||||
|
||||
const currentVersion = Number(afterMissingVersion?.inventory?.version)
|
||||
const availableBeforeRace = Number(afterMissingVersion?.inventory?.availableBoxes)
|
||||
const raceBodies = [1, 2].map((sequence) => ({ warehouseId, containerNo, boxCount: 7, outboundTime: now,
|
||||
status: 'COMPLETED', inventoryVersion: currentVersion, idempotencyKey: `${marker}-race-${sequence}` }))
|
||||
const raceResponses = await Promise.all(raceBodies.map((body) => context.request('POST', `${BASE}/outbounds`, body, context.token)))
|
||||
raceResponses.forEach((response, index) => traced(context, `并发出库请求${index + 1}`, response))
|
||||
const winners = raceResponses.filter(succeeded)
|
||||
const conflicts = raceResponses.filter((response) => response?.http === 200 && String(response?.body?.code) === 'A0409')
|
||||
context.assert(MODULE, '同一库存版本并发出库仅一个成功', raceResponses[0], winners.length === 1 && conflicts.length === 1,
|
||||
`success=${winners.length}, conflict=${conflicts.length}`)
|
||||
const afterRaceResponse = await api(context, 'GET', `${BASE}/inventories/${inventory.id}`)
|
||||
const afterRace = context.apiSuccess(MODULE, '并发出库后回查库存', afterRaceResponse)
|
||||
context.assert(MODULE, '并发出库只扣减一次并只新增一条流水', afterRaceResponse,
|
||||
Number(afterRace?.inventory?.availableBoxes) === availableBeforeRace - 7
|
||||
&& afterRace?.transactions?.length === 3 && Number(afterRace?.outboundOrderCount) === 1,
|
||||
`before=${availableBeforeRace}, after=${afterRace?.inventory?.availableBoxes}, transactions=${afterRace?.transactions?.length}, outboundOrders=${afterRace?.outboundOrderCount}`)
|
||||
const winnerIndex = raceResponses.findIndex(succeeded)
|
||||
if (winnerIndex >= 0) {
|
||||
const firstWinner = raceResponses[winnerIndex]?.body?.data
|
||||
const replay = await api(context, 'POST', `${BASE}/outbounds`, raceBodies[winnerIndex])
|
||||
const replayData = context.apiSuccess(MODULE, '并发成功请求使用原幂等键重放', replay)
|
||||
context.assert(MODULE, '并发成功请求重放不重复扣减', replay,
|
||||
replayData?.replayed === true && id(replayData?.id) === id(firstWinner?.id),
|
||||
`replayed=${replayData?.replayed}, firstId=${id(firstWinner?.id)}, replayId=${id(replayData?.id)}`)
|
||||
}
|
||||
expectCode(context, '不存在的整柜库存详情返回未找到',
|
||||
await api(context, 'GET', `${BASE}/inventories/1`), 'A0404')
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const marker = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18)
|
||||
const initialInboundsResponse = await api(context, 'GET', `${BASE}/inbounds?page=1&size=20`)
|
||||
@@ -37,6 +133,8 @@ export async function run(context) {
|
||||
const containerNo = `DCA${marker}`.slice(0, 32).toUpperCase()
|
||||
const now = new Date().toISOString().slice(0, 19)
|
||||
|
||||
await verifyValidationBoundaries(context, warehouseId, marker, now)
|
||||
|
||||
const invalidWarehouse = await api(context, 'POST', `${BASE}/inbounds`, {
|
||||
warehouseId: '1', cargoName: marker, containerNo: `${containerNo}X`, boxCount: 1, volumeCbm: 1,
|
||||
inboundTime: now, status: 'COMPLETED', remark: marker, idempotencyKey: `${marker}-invalid-warehouse`,
|
||||
@@ -122,6 +220,8 @@ export async function run(context) {
|
||||
inboundRows?.records?.length === 2 && outboundRows?.records?.length === 3,
|
||||
`inbounds=${inboundRows?.records?.length}, outbounds=${outboundRows?.records?.length}`)
|
||||
|
||||
await verifyExtendedInventoryScenarios(context, warehouseId, marker, now)
|
||||
|
||||
await verifyPermissionDenial(context, marker, warehouseId, containerNo)
|
||||
context.createdResources.domesticContainer = { warehouseId, inventoryId, containerNo, permissions: PERMISSIONS }
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ async function pickAll(context, chain, label) {
|
||||
expectedVersion: Number(chain.task.version), inventorySnapshotToken: snapshot.snapshotToken,
|
||||
items: chain.task.items.map((item) => ({ itemId: id(item.id), action: 'PICK',
|
||||
pickedPackageCount: Number(item.plannedPackageCount), finalPickedPackageCount: null,
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(item.version), reason: null })),
|
||||
version: Number(item.version), reason: null })),
|
||||
}, `${label}-batch`)
|
||||
chain.task = context.apiSuccess(MODULE, `${label} 完成 PDA 拣货`, response, '', OPTIONS)
|
||||
return chain
|
||||
|
||||
@@ -240,9 +240,22 @@ export async function run(context) {
|
||||
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
|
||||
expectedVersion: Number(foreignPlan?.version), operationId: `${context.marker}-other-channel-assign`.slice(0, 128),
|
||||
})
|
||||
context.addStep(MODULE, '两个真实有效渠道之间拒绝跨渠道装柜', foreignAssignResponse,
|
||||
foreignAssignResponse.http === 200 && ['A0422', '422'].includes(codeOf(foreignAssignResponse)),
|
||||
const foreignAssigned = foreignAssignResponse.body?.data
|
||||
const foreignItem = Array.isArray(foreignAssigned?.items)
|
||||
? foreignAssigned.items.find((item) => id(item.inboundId) === inboundId
|
||||
&& id(item.inboundDetailId) === id(candidate.inboundDetailId))
|
||||
: null
|
||||
context.addStep(MODULE, '手工装柜允许两个真实有效渠道之间跨渠道分配', foreignAssignResponse,
|
||||
foreignAssignResponse.http === 200 && codeOf(foreignAssignResponse) === '200' && Boolean(foreignItem?.id),
|
||||
`primary=${channelCode}, foreign=${foreignChannel.channelCode}, HTTP=${foreignAssignResponse.http}, code=${codeOf(foreignAssignResponse)}`, OPTIONS)
|
||||
if (foreignItem?.id) {
|
||||
const foreignRemoveResponse = await request(context, 'DELETE',
|
||||
`${SMART}/plans/${foreignPlan?.id}/items/${foreignItem.id}`, {
|
||||
expectedPlanVersion: Number(foreignAssigned.version), expectedItemVersion: Number(foreignItem.version),
|
||||
operationId: `${context.marker}-other-channel-remove`.slice(0, 128),
|
||||
})
|
||||
context.apiSuccess(MODULE, '跨渠道分配验收后释放入仓码供主流程继续', foreignRemoveResponse, '', OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
|
||||
@@ -678,8 +691,7 @@ export async function run(context) {
|
||||
|
||||
const batchPath = `${PDA}/tasks/${taskId}/batch`
|
||||
const batchItems = [{ itemId: id(pickItem.id), action: 'PICK', pickedPackageCount: 1,
|
||||
finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [],
|
||||
version: Number(pickItem.version), reason: null }]
|
||||
finalPickedPackageCount: null, version: Number(pickItem.version), reason: null }]
|
||||
const staleBatchResponse = await request(context, 'POST', batchPath, {
|
||||
warehouseId, loadingPlanId: planId, containerBookingId: containerId,
|
||||
expectedVersion: Number(task.version) + 1,
|
||||
|
||||
@@ -655,7 +655,7 @@ export async function run(context) {
|
||||
inventorySnapshotToken: inventory.snapshotToken,
|
||||
items: [{
|
||||
itemId: id(canonicalItem.id), action: 'PICK', pickedPackageCount: 1,
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(canonicalItem.version),
|
||||
version: Number(canonicalItem.version),
|
||||
}],
|
||||
}
|
||||
|
||||
@@ -1277,7 +1277,7 @@ export async function run(context) {
|
||||
expectedVersion: Number(afterUnresolvedCompleteTask?.version),
|
||||
inventorySnapshotToken: partialInventory?.snapshotToken,
|
||||
items: [{ itemId: id(unresolvedTaskItem?.id), action: 'PARTIAL', finalPickedPackageCount: 1,
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(unresolvedTaskItem?.version) }],
|
||||
version: Number(unresolvedTaskItem?.version) }],
|
||||
}, { 'Idempotency-Key': partialBatchKey })
|
||||
const partialTask = context.apiSuccess(MODULE, '合法 PARTIAL 解决未完成明细', partialBatchResponse, '', OPTIONS)
|
||||
const partialTaskItem = partialTask?.items?.find?.((item) => id(item?.id) === id(unresolvedTaskItem?.id))
|
||||
@@ -1347,7 +1347,7 @@ export async function run(context) {
|
||||
expectedVersion: Number(incrementedTask?.version),
|
||||
inventorySnapshotToken: effectiveBatchPayload.inventorySnapshotToken,
|
||||
items: [{ itemId: id(incrementalTaskItem.id), action: 'PICK', pickedPackageCount: 1,
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(incrementalTaskItem.version) }],
|
||||
version: Number(incrementalTaskItem.version) }],
|
||||
}, { 'Idempotency-Key': `${context.marker}-dynamic-stale-inventory`.slice(0, 128) })
|
||||
context.addStep(MODULE, '增量后旧 inventory token 明确 A0409 并要求刷新', staleIncrementBatch,
|
||||
staleIncrementBatch.http === 200 && codeOf(staleIncrementBatch) === 'A0409'
|
||||
@@ -1424,7 +1424,7 @@ export async function run(context) {
|
||||
expectedVersion: Number(refreshedDynamicTask?.version),
|
||||
inventorySnapshotToken: refreshedDynamicInventory?.snapshotToken,
|
||||
items: [{ itemId: id(refreshedDynamicTaskItem?.id), action: 'PICK', pickedPackageCount: Number(reassignedPlanItem?.packageCount),
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(refreshedDynamicTaskItem?.version) }],
|
||||
version: Number(refreshedDynamicTaskItem?.version) }],
|
||||
}, { 'Idempotency-Key': dynamicBatchKey })
|
||||
const dynamicPickedTask = context.apiSuccess(MODULE, '刷新后完成动态新增项拣货', dynamicBatchResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '动态新增项拣完后同一任务重新进入 AWAITING_REVIEW', dynamicBatchResponse,
|
||||
|
||||
@@ -75,7 +75,7 @@ test('each remainder settles only through inventory snapshot, atomic batch and e
|
||||
assert.match(source, /expectedVersion: Number\(inventory\.taskVersion\)/)
|
||||
assert.match(source, /inventorySnapshotToken: inventory\.snapshotToken/)
|
||||
assert.match(source, /action: 'PICK', pickedPackageCount: 1/)
|
||||
assert.match(source, /version: Number\(claimedItem\.version\), releaseMode: 'MANUAL', locationIds: \[\]/)
|
||||
assert.doesNotMatch(source, /releaseMode|locationIds|reservationIds/)
|
||||
assert.match(source, /'Idempotency-Key': batchKey/)
|
||||
assert.match(source, /differentReplay/)
|
||||
assert.match(source, /workflowStatus === 'AWAITING_REVIEW'/)
|
||||
|
||||
@@ -392,7 +392,7 @@ async function loadOneFragment(context, state) {
|
||||
expectedVersion: Number(inventory.taskVersion), inventorySnapshotToken: inventory.snapshotToken,
|
||||
items: [{
|
||||
itemId: id(claimedItem.id), action: 'PICK', pickedPackageCount: 1,
|
||||
version: Number(claimedItem.version), releaseMode: 'MANUAL', locationIds: [],
|
||||
version: Number(claimedItem.version),
|
||||
}],
|
||||
}
|
||||
const staleBatch = await request(context, 'POST', batchPath, {
|
||||
|
||||
@@ -371,12 +371,23 @@ test('channel isolation reports BLOCKED_FIXTURE instead of inventing a second ch
|
||||
assert.match(value, /codeOf\(foreignAssignResponse\) === 'A0422'/)
|
||||
})
|
||||
|
||||
test('container loading enforces channel isolation and stable PDA identity across container number changes', () => {
|
||||
test('manual cross-channel loading fixture releases the inbound before the primary flow', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assertOrder(value, [
|
||||
'新增真实异渠道模拟柜',
|
||||
'手工装柜允许两个真实有效渠道之间跨渠道分配',
|
||||
'跨渠道分配验收后释放入仓码供主流程继续',
|
||||
'人工分配本轮候选货物',
|
||||
])
|
||||
})
|
||||
|
||||
test('container loading permits manual cross-channel allocation and keeps stable PDA identity across container number changes', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assertOrder(value, [
|
||||
'新增内部模拟柜',
|
||||
'新增真实异渠道模拟柜',
|
||||
'两个真实有效渠道之间拒绝跨渠道装柜',
|
||||
'手工装柜允许两个真实有效渠道之间跨渠道分配',
|
||||
'跨渠道分配验收后释放入仓码供主流程继续',
|
||||
'人工分配本轮候选货物',
|
||||
'分配同步建立 bridge',
|
||||
'无真实柜号时 PDA 查询 A0404',
|
||||
|
||||
Reference in New Issue
Block a user