Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
@@ -288,6 +288,7 @@ function usage() {
|
||||
--wait-split-package-count <n> 单票指定总箱数,1-1000;省略时使用既有轮换
|
||||
--wait-split-sequence <n> 单票全局序号,正整数,默认1
|
||||
--wait-split-random-value <n> 单票随机选择值,正整数,默认使用全局序号
|
||||
--verify-pallet-location-guide <true> 验证整票库位块、图片槽绑定与删除后 RESERVED_EMPTY
|
||||
--route-mode <mode> 线路选择:fixed 或 random,默认fixed
|
||||
--manual-split-fixture-report <path> 人工分柜准备报告路径,仅只读核验模块使用
|
||||
--manual-split-plan-id <id> 人工操作产生的智能装柜计划 ID,仅只读核验模块使用
|
||||
@@ -348,6 +349,7 @@ export function parseArguments(argv) {
|
||||
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
|
||||
waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
|
||||
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
|
||||
verifyPalletLocationGuide: values.has('verify-pallet-location-guide'),
|
||||
routeMode: values.get('route-mode') || 'fixed',
|
||||
quiet: values.get('quiet') === 'true',
|
||||
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
|
||||
|
||||
@@ -128,6 +128,12 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s
|
||||
node scripts/admin-test-env-acceptance.mjs --modules merchant-qualification-fixture,orders,inbound-measurement,phase3-container-loading,phase3-loading,phase3-container-overview,phase3-pda-convergence,phase3-evidence-gate,phase3-customs,phase3-loading-finalize
|
||||
```
|
||||
|
||||
整票库位块专项可在只跑 `orders,inbound-measurement` 时追加
|
||||
`--verify-pallet-location-guide true`。它在测量提交后核对
|
||||
`palletLocationGuide` 计数、每槽 `blockId/slotNo/reservationId/locationCode/slotStatus`、
|
||||
称重结果按 `weightResultId` 显式绑定槽位,并真实验证删除图片后原 reservation/location
|
||||
保留且转为 `RESERVED_EMPTY`。脚本只观测自然出现的共享 A0;不会为制造普通位不足批量停用库位。
|
||||
|
||||
只验证真实集装箱号晚补门禁时,使用正式可选参数 `LATE_CONTAINER_GATE_ONLY=1` 并仍从总入口执行 `phase3-loading`。该模式使用本轮 marker 独立造数,固定验证无号 push、订舱关联、整份预装确认、PDA claim/batch、无号 complete `A0409`、LOADING 阶段首次补号和补号后 complete 受理;随后允许后台命令停在 `CABINET_EVIDENCE/A0409`,以证明柜号门禁已经消失且下一门禁来自未上传的五类柜级证据。该模式不继续完整报关或 LOADED 链,也不得与完整链同时解释为同一终态验收。
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -69,6 +69,16 @@ test('demo package keeps pallet pages bounded and historical snapshots read-only
|
||||
assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
|
||||
})
|
||||
|
||||
test('opt-in HTTP acceptance verifies whole-ticket slot identity and delete-to-empty semantics', () => {
|
||||
assert.match(source, /verifyPalletLocationGuide/)
|
||||
assert.match(source, /expectedWeightResultIds\.every/)
|
||||
assert.match(source, /asId\(row\.reservationId\) === asId\(slot\.reservationId\)/)
|
||||
assert.match(source, /\['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'\]/)
|
||||
assert.match(source, /deleteSucceeded/)
|
||||
assert.match(source, /retainedSlot\?\.slotStatus === 'RESERVED_EMPTY'/)
|
||||
assert.match(source, /不批量停用普通库位制造容量不足/)
|
||||
})
|
||||
|
||||
test('approval freezes measurement and requires quantity weight and volume conservation', () => {
|
||||
assert.match(source, /审核及合托完成后箱码尺寸与图片快照不可再覆盖/)
|
||||
assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/)
|
||||
|
||||
@@ -271,7 +271,10 @@ async function createConfirmedWeightEvidence(context, {
|
||||
staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409',
|
||||
`HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`,
|
||||
OPTIONS)
|
||||
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
|
||||
confirmedResults.push({
|
||||
id: asId(created.id), palletId, netWeightKg, grossWeightKg,
|
||||
version: Number(confirmed?.version),
|
||||
})
|
||||
}
|
||||
|
||||
const workspaceResponse = await context.request('GET',
|
||||
@@ -292,7 +295,41 @@ async function createConfirmedWeightEvidence(context, {
|
||||
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
||||
`confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
||||
OPTIONS)
|
||||
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg } : null
|
||||
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg, confirmedResults } : null
|
||||
}
|
||||
|
||||
function verifyPalletLocationGuide(context, response, workspace, expectedWeightResultIds, name) {
|
||||
const guide = workspace?.palletLocationGuide
|
||||
const slots = Array.isArray(guide?.slots) ? guide.slots : []
|
||||
const weights = Array.isArray(workspace?.weightResults) ? workspace.weightResults : []
|
||||
const boundSlots = slots.filter((slot) => asId(slot?.boundWeightResultId))
|
||||
const emptySlots = slots.filter((slot) => slot?.slotStatus === 'RESERVED_EMPTY')
|
||||
const identityComplete = asId(guide?.blockId)
|
||||
&& slots.length === Number(guide?.reservedSlotCount)
|
||||
&& Number(guide?.actualPalletCount) === boundSlots.length
|
||||
&& Number(guide?.reservedEmptySlotCount) === emptySlots.length
|
||||
&& slots.every((slot) => Number.isInteger(Number(slot?.slotNo)) && Number(slot.slotNo) > 0
|
||||
&& asId(slot?.reservationId) && asId(slot?.locationId)
|
||||
&& String(slot?.locationCode || '').trim() && typeof slot?.sharedOverflow === 'boolean'
|
||||
&& ['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'].includes(String(slot?.slotStatus || '')))
|
||||
context.assert(MODULE, `${name}整票库位块计数与槽身份完整`, response, Boolean(identityComplete),
|
||||
`blockId=${asId(guide?.blockId) || 'missing'}, reserved=${guide?.reservedSlotCount ?? 'missing'}, actual=${guide?.actualPalletCount ?? 'missing'}, empty=${guide?.reservedEmptySlotCount ?? 'missing'}, slots=${slots.length}`,
|
||||
OPTIONS)
|
||||
|
||||
const bindingsStable = expectedWeightResultIds.every((resultId) => {
|
||||
const row = weights.find((item) => asId(item?.id) === resultId)
|
||||
const slot = slots.find((item) => asId(item?.boundWeightResultId) === resultId)
|
||||
return row && slot && asId(row.blockId) === asId(guide?.blockId)
|
||||
&& Number(row.slotNo) === Number(slot.slotNo)
|
||||
&& asId(row.reservationId) === asId(slot.reservationId)
|
||||
&& asId(row.locationId) === asId(slot.locationId)
|
||||
&& String(row.locationCode || '') === String(slot.locationCode || '')
|
||||
&& Boolean(row.sharedOverflow) === Boolean(slot.sharedOverflow)
|
||||
})
|
||||
context.assert(MODULE, `${name}称重图片按 resultId 显式绑定槽位`, response, bindingsStable,
|
||||
`expectedResultIds=${expectedWeightResultIds.join('|') || 'none'}, boundResultIds=${boundSlots.map((slot) => asId(slot.boundWeightResultId)).join('|') || 'none'}`,
|
||||
OPTIONS)
|
||||
return { guide, slots, weights, identityComplete: Boolean(identityComplete), bindingsStable }
|
||||
}
|
||||
|
||||
function findIsolatedInbound(context) {
|
||||
@@ -708,6 +745,62 @@ export async function run(context) {
|
||||
submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING',
|
||||
`processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS)
|
||||
|
||||
if (context.options?.verifyPalletLocationGuide) {
|
||||
const guideResponse = await context.request('GET', measurementPath)
|
||||
const guideWorkspace = context.apiSuccess(MODULE, '提交后读取整票库位块与称重图片槽绑定', guideResponse,
|
||||
`inboundId=${inboundId}`, OPTIONS)
|
||||
const beforeDelete = verifyPalletLocationGuide(context, guideResponse, guideWorkspace,
|
||||
weightEvidence.confirmedResults.map((item) => item.id), '删除前')
|
||||
const overflowSlots = beforeDelete.slots.filter((slot) => slot?.sharedOverflow === true)
|
||||
context.addStep(MODULE, '正式数据 A0 共享溢出观测', guideResponse, true,
|
||||
overflowSlots.length > 0
|
||||
? `sharedOverflow=true, slots=${overflowSlots.map((slot) => `${slot.slotNo}:${slot.reservationId}:${slot.locationCode}`).join('|')}`
|
||||
: '本票未触发 A0;为保护共享测试仓,不批量停用普通库位制造容量不足',
|
||||
{ scenario: SCENARIO })
|
||||
|
||||
const deletedWeight = weightEvidence.confirmedResults[0]
|
||||
const deleteResponse = await context.request('DELETE',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${deletedWeight.id}?${query(tenantId)}&version=${deletedWeight.version}`)
|
||||
const deleteSucceeded = deleteResponse.http === 200 && String(deleteResponse.body?.code) === '200'
|
||||
context.assert(MODULE, '删除称重图片后保留原 reservation/location 并释放为空槽', deleteResponse,
|
||||
deleteSucceeded,
|
||||
`HTTP=${deleteResponse.http}, code=${deleteResponse.body?.code ?? 'missing'}, message=${deleteResponse.body?.message ?? 'missing'}, requestId=${deleteResponse.requestId || 'missing'}, traceId=${deleteResponse.traceId || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (deleteSucceeded) {
|
||||
const afterResponse = await context.request('GET', measurementPath)
|
||||
const afterWorkspace = context.apiSuccess(MODULE, '删除后回查整票库位块', afterResponse,
|
||||
`deletedWeightResultId=${deletedWeight.id}`, OPTIONS)
|
||||
const afterDelete = verifyPalletLocationGuide(context, afterResponse, afterWorkspace,
|
||||
weightEvidence.confirmedResults.slice(1).map((item) => item.id), '删除后')
|
||||
const previousSlot = beforeDelete.slots.find((slot) => asId(slot?.boundWeightResultId) === deletedWeight.id)
|
||||
const retainedSlot = afterDelete.slots.find((slot) =>
|
||||
asId(slot?.reservationId) === asId(previousSlot?.reservationId)
|
||||
&& asId(slot?.locationId) === asId(previousSlot?.locationId))
|
||||
context.assert(MODULE, '删除只解绑图片且原槽变为 RESERVED_EMPTY', afterResponse,
|
||||
Boolean(previousSlot) && retainedSlot?.slotStatus === 'RESERVED_EMPTY'
|
||||
&& !asId(retainedSlot?.boundWeightResultId)
|
||||
&& Number(afterDelete.guide?.reservedSlotCount) === Number(beforeDelete.guide?.reservedSlotCount),
|
||||
`reservationId=${asId(retainedSlot?.reservationId) || 'missing'}, locationId=${asId(retainedSlot?.locationId) || 'missing'}, slotStatus=${retainedSlot?.slotStatus || 'missing'}`,
|
||||
OPTIONS)
|
||||
context.createdResources.inboundMeasurementLocationGuide = {
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(afterDelete.guide?.blockId),
|
||||
deletedWeightResultId: deletedWeight.id, deleteSucceeded: true,
|
||||
sharedOverflowObserved: afterDelete.slots.some((slot) => slot?.sharedOverflow === true),
|
||||
}
|
||||
finishScenario(context, start)
|
||||
return
|
||||
}
|
||||
context.createdResources.inboundMeasurementLocationGuide = {
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId),
|
||||
deletedWeightResultId: deletedWeight.id, deleteSucceeded: false,
|
||||
deleteCode: deleteResponse.body?.code || null,
|
||||
deleteMessage: deleteResponse.body?.message || null,
|
||||
deleteRequestId: deleteResponse.requestId || null,
|
||||
deleteTraceId: deleteResponse.traceId || null,
|
||||
sharedOverflowObserved: overflowSlots.length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
const rejectedScanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-03', measuredAt: new Date().toISOString().slice(0, 19),
|
||||
})
|
||||
|
||||
@@ -124,8 +124,10 @@ function inventorySignature(snapshot) {
|
||||
inboundId: id(inbound?.inboundId),
|
||||
availablePackageCount: Number(inbound?.availablePackageCount),
|
||||
resources: (inbound?.resources || []).map((resource) => ({
|
||||
reservationId: id(resource?.reservationId), occupancyId: id(resource?.occupancyId),
|
||||
palletId: id(resource?.palletId), locationId: id(resource?.locationId),
|
||||
blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
|
||||
reservationId: id(resource?.reservationId), weightResultId: id(resource?.weightResultId),
|
||||
palletSequence: Number(resource?.palletSequence), palletId: id(resource?.palletId),
|
||||
locationId: id(resource?.locationId), slotStatus: resource?.slotStatus || null,
|
||||
sharedOverflow: resource?.sharedOverflow === true,
|
||||
availablePackageCount: Number(resource?.availablePackageCount),
|
||||
})),
|
||||
@@ -622,8 +624,9 @@ export async function run(context) {
|
||||
&& id(inventory?.warehouseId) === warehouseId
|
||||
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
|
||||
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
|
||||
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.reservationId)
|
||||
&& id(resource?.occupancyId) && id(resource?.palletId) && id(resource?.locationId)
|
||||
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.blockId)
|
||||
&& Number(resource?.slotNo) >= 1 && id(resource?.reservationId) && id(resource?.locationId)
|
||||
&& ['RESERVED_EMPTY', 'BOUND'].includes(resource?.slotStatus)
|
||||
&& Number(resource?.availablePackageCount) >= 0)
|
||||
&& Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || [])
|
||||
.reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0)
|
||||
|
||||
Reference in New Issue
Block a user