This commit is contained in:
2026-08-24 19:58:35 +08:00
parent 9e0f41e78b
commit c93fdc44d5
23 changed files with 965 additions and 313 deletions
@@ -36,6 +36,9 @@ export async function run(context) {
const containerBookingId = id(fixture?.containerBookingId)
if (!inboundId || !containerBookingId || !['COMPLETED', 'LOADED'].includes(fixture?.finalStatus)
|| fixture?.marker !== context.marker) {
context.addStep(MODULE, '取得同 marker 的 LOADED 国内出仓前置', null, false,
`inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, finalStatus=${fixture?.finalStatus || 'missing'}, fixtureMarker=${fixture?.marker || 'missing'}, runMarker=${context.marker}`,
OPTIONS)
context.addCoverageGap(MODULE, OPTIONS.scenario, '国内出仓主聚合真实闭环',
'本轮未取得 marker 隔离的柜次装柜 fixture,未使用历史资源', 'fixture')
return
@@ -85,6 +88,9 @@ export async function run(context) {
{ 'Idempotency-Key': confirmKey })
context.addStep(MODULE, '门禁未通过时 confirm-dispatch fail-closed', blocked, rejected(blocked),
`HTTP=${blocked.http}, code=${codeOf(blocked)}`, OPTIONS)
context.addStep(MODULE, '国内出仓八项门禁必须在同轮全部通过', gateResponse, false,
`failed=${gateItems.filter((item) => !item.passed).map((item) => `${item.code}:${item.actual || 'missing'}`).join(',') || 'gate.passed=false'}`,
OPTIONS)
for (const item of gateItems.filter((value) => !value.passed)) {
context.addCoverageGap(MODULE, OPTIONS.scenario, item.name || item.code,
`${item.code}: actual=${item.actual || 'missing'}, requirement=${item.requirement || 'missing'}`,
@@ -124,6 +130,37 @@ export async function run(context) {
reversed?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH' && reversed?.aggregate?.inboundStatus === 'LOADED'
&& reversed?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'REVERSE_DISPATCH'),
`dispatch=${reversed?.aggregate?.dispatchStatus || 'missing'}, inbound=${reversed?.aggregate?.inboundStatus || 'missing'}`, OPTIONS)
const reverseReplayResponse = await request(context, 'POST', `${BASE}/reverse-dispatch`, {
inboundId, containerBookingId, expectedVersion: Number(confirmed.aggregate.version), remark: context.marker,
}, { 'Idempotency-Key': reverseKey })
const reverseReplay = context.apiSuccess(MODULE, 'reverse-dispatch 幂等重放', reverseReplayResponse, '', OPTIONS)
context.assert(MODULE, '撤销重放保持 READY_FOR_DISPATCH 与同一版本', reverseReplayResponse,
reverseReplay?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH'
&& Number(reverseReplay?.aggregate?.version) === Number(reversed?.aggregate?.version),
`dispatch=${reverseReplay?.aggregate?.dispatchStatus || 'missing'}, version=${reverseReplay?.aggregate?.version ?? 'missing'}`, OPTIONS)
const reverseHashConflict = await request(context, 'POST', `${BASE}/reverse-dispatch`, {
inboundId, containerBookingId, expectedVersion: Number(confirmed.aggregate.version), remark: 'forged',
}, { 'Idempotency-Key': reverseKey })
context.addStep(MODULE, 'reverse-dispatch 同键异参返回 A0409', reverseHashConflict,
reverseHashConflict.http === 200 && codeOf(reverseHashConflict) === 'A0409',
`HTTP=${reverseHashConflict.http}, code=${codeOf(reverseHashConflict)}`, OPTIONS)
const staleConfirmResponse = await request(context, 'POST', `${BASE}/confirm-dispatch`, actionBody,
{ 'Idempotency-Key': `${context.marker}-outbound-confirm-stale`.slice(0, 128) })
context.addStep(MODULE, '撤销后旧版本再次确认返回 A0409', staleConfirmResponse,
staleConfirmResponse.http === 200 && codeOf(staleConfirmResponse) === 'A0409',
`HTTP=${staleConfirmResponse.http}, code=${codeOf(staleConfirmResponse)}`, OPTIONS)
const finalDetailResponse = await request(context, 'GET', `${BASE}/detail?${selector}`)
const finalDetail = context.apiSuccess(MODULE, '负向与幂等验证后回查出仓聚合', finalDetailResponse, '', OPTIONS)
context.assert(MODULE, '出仓聚合保持 READY_FOR_DISPATCH、LOADED 和确认撤销历史', finalDetailResponse,
finalDetail?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH'
&& finalDetail?.aggregate?.inboundStatus === 'LOADED'
&& Number(finalDetail?.aggregate?.version) === Number(reversed?.aggregate?.version)
&& finalDetail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'CONFIRM_DISPATCH')
&& finalDetail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'REVERSE_DISPATCH'),
`dispatch=${finalDetail?.aggregate?.dispatchStatus || 'missing'}, inbound=${finalDetail?.aggregate?.inboundStatus || 'missing'}, version=${finalDetail?.aggregate?.version ?? 'missing'}`, OPTIONS)
if (reversed?.aggregate) {
context.createdResources.phase3OutboundDispatch = {
marker: context.marker, inboundId, containerBookingId,