Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
@@ -120,6 +120,14 @@ node scripts/admin-test-env-acceptance.mjs --gateway <development-gateway> --ove
|
||||
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
|
||||
```
|
||||
|
||||
只验证真实集装箱号晚补门禁时,使用正式可选参数 `LATE_CONTAINER_GATE_ONLY=1` 并仍从总入口执行 `phase3-loading`。该模式使用本轮 marker 独立造数,固定验证无号 push、订舱关联、整份预装确认、PDA claim/batch、无号 complete `A0409`、LOADING 阶段首次补号和补号后 complete 受理;随后允许后台命令停在 `CABINET_EVIDENCE/A0409`,以证明柜号门禁已经消失且下一门禁来自未上传的五类柜级证据。该模式不继续完整报关或 LOADED 链,也不得与完整链同时解释为同一终态验收。
|
||||
|
||||
```powershell
|
||||
$env:LATE_CONTAINER_GATE_ONLY='1'
|
||||
node --env-file="$env:USERPROFILE\.codex\chongchong-global-test-acceptance.env" scripts/admin-test-env-acceptance.mjs --modules phase3-loading
|
||||
Remove-Item Env:LATE_CONTAINER_GATE_ONLY
|
||||
```
|
||||
|
||||
该命令会写测试环境,必须另行获得当前对话授权。离线入口合同为 `../full-regression-entry-contract.test.mjs`,生产者与状态合同为 `phase3-producer-chain-contract.test.mjs`。
|
||||
|
||||
## 消息中心模块
|
||||
|
||||
@@ -8,6 +8,7 @@ const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: t
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'
|
||||
const LATE_CONTAINER_GATE_ONLY = process.env.LATE_CONTAINER_GATE_ONLY === '1'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
export function markerSuffix(marker) {
|
||||
@@ -496,19 +497,24 @@ export async function run(context) {
|
||||
const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}`)
|
||||
const latestContainer = context.apiSuccess(MODULE, '合并前读取模拟柜最新版本', latestContainerResponse, '', OPTIONS)
|
||||
const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), containerNo,
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id),
|
||||
...(LATE_CONTAINER_GATE_ONLY ? {} : { containerNo }),
|
||||
expectedVersion: Number(latestContainer?.version),
|
||||
})
|
||||
const linked = context.apiSuccess(MODULE, '模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号', linkResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '订舱关联保持柜次计划与真实柜号身份', linkResponse,
|
||||
const linked = context.apiSuccess(MODULE, LATE_CONTAINER_GATE_ONLY
|
||||
? '无号模拟柜原地关联三期 RELEASED 订舱需求'
|
||||
: '模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号', linkResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '订舱关联保持柜次计划与运输身份', linkResponse,
|
||||
id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo
|
||||
&& linked?.containerNo === containerNo && id(linked?.oceanBookingId) === bookingId,
|
||||
&& (LATE_CONTAINER_GATE_ONLY ? linked?.containerNo == null : linked?.containerNo === containerNo)
|
||||
&& id(linked?.oceanBookingId) === bookingId,
|
||||
`containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS)
|
||||
if (id(linked?.id) !== containerBookingId) return
|
||||
|
||||
context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份和运输快照', releaseResponse,
|
||||
bookingConfirmed?.oceanBooking?.status === 'RELEASED'
|
||||
&& id(linked?.oceanBookingId) === bookingId && linked?.containerNo === containerNo,
|
||||
&& id(linked?.oceanBookingId) === bookingId
|
||||
&& (LATE_CONTAINER_GATE_ONLY ? linked?.containerNo == null : linked?.containerNo === containerNo),
|
||||
`status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}`, OPTIONS)
|
||||
|
||||
const unconfirmedTaskResponse = await context.request('GET',
|
||||
@@ -610,7 +616,8 @@ export async function run(context) {
|
||||
&& id(inventory?.loadingPlanId) === loadingPlanId
|
||||
&& id(inventory?.containerBookingId) === containerBookingId
|
||||
&& Number(inventory?.taskVersion) === Number(claimed?.version)
|
||||
&& id(inventory?.cabinetNo) && inventory?.currentContainerNo === containerNo
|
||||
&& id(inventory?.cabinetNo)
|
||||
&& (LATE_CONTAINER_GATE_ONLY ? inventory?.currentContainerNo == null : inventory?.currentContainerNo === containerNo)
|
||||
&& id(inventory?.warehouseId) === warehouseId
|
||||
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
|
||||
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
|
||||
@@ -869,6 +876,73 @@ export async function run(context) {
|
||||
&& id(pickedPlan?.pdaTaskId) === canonicalTaskId && pickedPlan?.pdaTaskStatus === 'LOADING',
|
||||
`status=${pickedPlan?.status || 'missing'}, version=${pickedPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
if (LATE_CONTAINER_GATE_ONLY) {
|
||||
const missingOperationId = `${context.marker}-late-container-missing-complete`.slice(0, 128)
|
||||
const missingComplete = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`, {
|
||||
expectedTaskVersion: Number(pickedCanonical?.version), operationId: missingOperationId,
|
||||
remark: `${context.marker}-missing-container`,
|
||||
})
|
||||
context.addStep(MODULE, '无号拣货完成后 PDA complete 精确返回 A0409', missingComplete,
|
||||
missingComplete.http === 200 && codeOf(missingComplete) === 'A0409'
|
||||
&& String(missingComplete.body?.message || '').includes('真实集装箱号'),
|
||||
`${responseEvidence(missingComplete)}, message=${missingComplete.body?.message || 'missing'}`, OPTIONS)
|
||||
|
||||
const transport = await request(context, 'PATCH',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
|
||||
containerNo, voyageNo: bookingDraft.voyageNo, expectedVersion: Number(pickedPlan?.version),
|
||||
operationReason: null,
|
||||
}, { 'Idempotency-Key': `${context.marker}-late-container-fill`.slice(0, 128) })
|
||||
const filled = context.apiSuccess(MODULE, 'LOADING 阶段通过预装运输信息首次补录真实柜号', transport,
|
||||
responseEvidence(transport), OPTIONS)
|
||||
context.assert(MODULE, '首次补号保持计划与柜次身份且柜号规范化落盘', transport,
|
||||
id(filled?.summary?.planId) === loadingPlanId
|
||||
&& id(filled?.summary?.containerBookingId) === containerBookingId
|
||||
&& filled?.summary?.status === 'LOADING' && filled?.summary?.containerNo === containerNo,
|
||||
`planId=${id(filled?.summary?.planId) || 'missing'}, containerBookingId=${id(filled?.summary?.containerBookingId) || 'missing'}, containerNo=${filled?.summary?.containerNo || 'missing'}`, OPTIONS)
|
||||
|
||||
const afterFillTaskResponse = await context.request('GET', taskDetailPath)
|
||||
const afterFillTask = context.apiSuccess(MODULE, '首次补号后回读原 PDA 任务', afterFillTaskResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '补号不替换 task/plan/cabinet 稳定身份', afterFillTaskResponse,
|
||||
id(afterFillTask?.id) === canonicalTaskId && id(afterFillTask?.loadingPlanId) === loadingPlanId
|
||||
&& id(afterFillTask?.containerBookingId) === containerBookingId
|
||||
&& afterFillTask?.currentContainerNo === containerNo,
|
||||
`taskId=${id(afterFillTask?.id) || 'missing'}, currentContainerNo=${afterFillTask?.currentContainerNo || 'missing'}`, OPTIONS)
|
||||
|
||||
const acceptedOperationId = `${context.marker}-late-container-accepted-complete`.slice(0, 128)
|
||||
const acceptedComplete = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`, {
|
||||
expectedTaskVersion: Number(afterFillTask?.version), operationId: acceptedOperationId,
|
||||
remark: `${context.marker}-container-filled`,
|
||||
})
|
||||
context.assert(MODULE, '补号后 complete 已越过柜号门禁并受理异步命令', acceptedComplete,
|
||||
acceptedComplete.http === 200 && codeOf(acceptedComplete) === '200'
|
||||
&& ['PROCESSING', 'FAILED', 'COMPLETED'].includes(acceptedComplete.body?.data?.workflowStatus),
|
||||
`${responseEvidence(acceptedComplete)}, workflow=${acceptedComplete.body?.data?.workflowStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
let terminalTask = acceptedComplete.body?.data
|
||||
let terminalResponse = acceptedComplete
|
||||
for (let attempt = 0; attempt < 10 && terminalTask?.workflowStatus === 'PROCESSING'; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
terminalResponse = await context.request('GET', taskDetailPath)
|
||||
if (terminalResponse.http === 200 && codeOf(terminalResponse) === '200') terminalTask = terminalResponse.body?.data
|
||||
}
|
||||
const nextStage = terminalTask?.completionCommand?.failureStage || null
|
||||
const nextCode = terminalTask?.completionCommand?.failureCode || null
|
||||
const nextMessage = String(terminalTask?.completionCommand?.failureMessage || '')
|
||||
context.assert(MODULE, '补号后已越过柜号门禁并允许进入柜级证据门禁', terminalResponse,
|
||||
!nextMessage.includes('集装箱号')
|
||||
&& (terminalTask?.workflowStatus !== 'PROCESSING'
|
||||
|| (nextStage === 'CABINET_EVIDENCE' && nextCode === 'A0409')),
|
||||
`workflow=${terminalTask?.workflowStatus || 'missing'}, stage=${terminalTask?.completionCommand?.failureStage || 'none'}, code=${terminalTask?.completionCommand?.failureCode || 'none'}, message=${terminalTask?.completionCommand?.failureMessage || 'none'}`, OPTIONS)
|
||||
context.createdResources.phase3LateContainerGate = {
|
||||
marker: context.marker, tenantId, inboundId, containerBookingId, cabinetNo: simulation.cabinetNo,
|
||||
oceanBookingId: bookingId, loadingPlanId, taskId: canonicalTaskId, containerNo,
|
||||
missingComplete: responseEvidence(missingComplete), fill: responseEvidence(transport),
|
||||
acceptedComplete: responseEvidence(acceptedComplete), finalWorkflow: terminalTask?.workflowStatus || null,
|
||||
nextFailureStage: nextStage, nextFailureCode: nextCode,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS)
|
||||
context.assert(MODULE, 'PDA 最终完成前预装清单仍为 CONFIRMED 且归属稳定', preload,
|
||||
|
||||
@@ -123,6 +123,25 @@ test('bridge loading confirms preload before creating and claiming the PDA task'
|
||||
assert.match(value, /bookingConfirmed\?\.oceanBooking\?\.status === 'RELEASED'/)
|
||||
})
|
||||
|
||||
test('late container gate mode proves the final gate without requiring evidence completion', () => {
|
||||
const value = source('./phase3-loading.mjs')
|
||||
assert.match(value, /LATE_CONTAINER_GATE_ONLY = process\.env\.LATE_CONTAINER_GATE_ONLY === '1'/)
|
||||
assertOrder(value, [
|
||||
'无号模拟柜原地关联三期 RELEASED 订舱需求',
|
||||
'计划级原子确认整份预装并触发唯一 PDA 任务',
|
||||
'领取柜次 PDA 任务',
|
||||
'原子提交柜次 PDA 整批拣货',
|
||||
'无号拣货完成后 PDA complete 精确返回 A0409',
|
||||
'LOADING 阶段通过预装运输信息首次补录真实柜号',
|
||||
'补号后 complete 已越过柜号门禁并受理异步命令',
|
||||
'补号后已越过柜号门禁并允许进入柜级证据门禁',
|
||||
])
|
||||
assert.match(value, /String\(missingComplete\.body\?\.message \|\| ''\)\.includes\('真实集装箱号'\)/)
|
||||
assert.match(value, /nextStage === 'CABINET_EVIDENCE' && nextCode === 'A0409'/)
|
||||
assert.match(value, /!nextMessage\.includes\('集装箱号'\)/)
|
||||
assert.match(value, /context\.createdResources\.phase3LateContainerGate/)
|
||||
})
|
||||
|
||||
test('push replay and both conflict modes preserve the complete pre-PDA snapshot', () => {
|
||||
for (const file of ['./phase3-loading.mjs', './phase3-container-loading.mjs']) {
|
||||
const value = source(file)
|
||||
@@ -220,7 +239,8 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap'
|
||||
])
|
||||
assert.match(loading, /inventorySnapshotToken: refreshedInventory\.snapshotToken/)
|
||||
assert.match(loading, /warehouseId, loadingPlanId, containerBookingId,/)
|
||||
assert.match(loading, /inventory\?\.currentContainerNo === containerNo/)
|
||||
assert.match(loading,
|
||||
/LATE_CONTAINER_GATE_ONLY \? inventory\?\.currentContainerNo == null : inventory\?\.currentContainerNo === containerNo/)
|
||||
const batchPayload = loading.slice(loading.indexOf('const batchPayload ='), loading.indexOf('const staleBatch ='))
|
||||
assert.doesNotMatch(batchPayload, /containerNo[,\s]/)
|
||||
assert.doesNotMatch(loading, /\/loading\/picking\/(?:start|complete)/)
|
||||
|
||||
Reference in New Issue
Block a user