808 lines
47 KiB
JavaScript
808 lines
47 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
|
|
import { selectCustomsFlow } from './phase3-customs.mjs'
|
|
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
|
import { stableLoadedCartonMarks } from './phase3-loading.mjs'
|
|
import { selectContainerLoadingFixture, selectPlatformChannelIsolationPair } from './phase3-container-loading.mjs'
|
|
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
|
import { AcceptanceRun, normalizeModuleOrder, parseArguments } from '../admin-test-env-acceptance.mjs'
|
|
|
|
function source(name) {
|
|
return readFileSync(new URL(name, import.meta.url), 'utf8')
|
|
}
|
|
|
|
function assertOrder(value, snippets) {
|
|
let cursor = -1
|
|
for (const snippet of snippets) {
|
|
const next = value.indexOf(snippet)
|
|
assert.ok(next > cursor, `${snippet} must appear after the previous production step`)
|
|
cursor = next
|
|
}
|
|
}
|
|
|
|
test('pairs customs source fixture with the matching loading container', () => {
|
|
const resources = {
|
|
orders: { tenantId: 10001, orderId: 101, inboundId: 201 },
|
|
orchestrationFixtures: {
|
|
phase3Container: { tenantId: 10001, orderId: 102, inboundId: 202 },
|
|
},
|
|
phase3Loading: { tenantId: 10001, inboundId: 201, containerBookingId: 301 },
|
|
phase3ContainerLoading: { tenantId: 10001, inboundId: 202, containerBookingId: 302 },
|
|
}
|
|
const selected = selectCustomsFlow(resources)
|
|
assert.equal(selected.fixture.orderId, 101)
|
|
assert.equal(selected.container.containerBookingId, 301)
|
|
})
|
|
|
|
test('falls back to the matching container fixture without crossing source identities', () => {
|
|
const resources = {
|
|
orchestrationFixtures: { phase3Container: { tenantId: 10001, orderId: 102, inboundId: 202 } },
|
|
phase3Loading: { tenantId: 10001, inboundId: 201, containerBookingId: 301 },
|
|
phase3ContainerLoading: { tenantId: 10001, inboundId: 202, containerBookingId: 302 },
|
|
}
|
|
const selected = selectCustomsFlow(resources)
|
|
assert.equal(selected.fixture.orderId, 102)
|
|
assert.equal(selected.container.containerBookingId, 302)
|
|
})
|
|
|
|
test('records LOADING and a cleanup reason when finalize returns S0500', () => {
|
|
const result = buildFinalizeResource(
|
|
{ inboundStatus: 'LOADING', inboundVersion: 5, finalStatus: 'COMPLETED' },
|
|
{ summary: { processStatus: 'LOADING', version: 5 } },
|
|
{ http: 200, body: { code: 'S0500' } },
|
|
)
|
|
assert.equal(result.finalStatus, 'LOADING')
|
|
assert.equal(result.finalizeSucceeded, false)
|
|
assert.match(result.notCleanedReason, /S0500/)
|
|
})
|
|
|
|
test('records LOADED and clears the reason only after a real LOADED reread', () => {
|
|
const result = buildFinalizeResource(
|
|
{ inboundStatus: 'LOADING', inboundVersion: 5, finalStatus: 'COMPLETED' },
|
|
{ summary: { processStatus: 'LOADED', version: 6 } },
|
|
{ http: 200, body: { code: '200' } },
|
|
)
|
|
assert.equal(result.finalStatus, 'LOADED')
|
|
assert.equal(result.finalizeSucceeded, true)
|
|
assert.equal(result.notCleanedReason, null)
|
|
})
|
|
|
|
test('finalize acceptance covers stable pallet generation and both print modes', () => {
|
|
const value = source('./phase3-loading-finalize.mjs')
|
|
assert.match(value, /pallet-labels`/)
|
|
assert.match(value, /`\$\{base\}\/prepare\?warehouseId=/)
|
|
assert.match(value, /柜号-FBA内部编号-仓内连续下标/)
|
|
assert.match(value, /重复生成散货托盘码返回稳定清单/)
|
|
assert.match(value, /destinationWarehouseCode: null, count: null/)
|
|
assert.match(value, /按指定 FBA 仓补打 1 张托盘码 PDF/)
|
|
assert.match(value, /pdfMagic === '%PDF'/)
|
|
assert.match(value, /palletLabelManifest,/)
|
|
})
|
|
|
|
test('overseas conversion proves source-code preservation and native code generation boundaries', () => {
|
|
const value = source('./phase4-overseas-pda.mjs')
|
|
assert.match(value, /头程 FBA 散货托盘码跨区原样用于实体托盘/)
|
|
assert.match(value, /标准海外仓订单由海外仓生成本地实体托盘码/)
|
|
assert.match(value, /actualPalletCodes\.every\(\(code\) => code\.startsWith\('PLT-'\)\)/)
|
|
})
|
|
|
|
test('container loading follows push, claim, real pick, completed sequence', () => {
|
|
const value = source('./phase3-container-loading.mjs')
|
|
assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/)
|
|
assertOrder(value, [
|
|
"pushed?.status === 'EDITING'",
|
|
"claimedPlan?.status === 'LOADING'",
|
|
"const pickResponse = await request",
|
|
"const completeResponse = await request",
|
|
"finalPlan?.status === 'COMPLETED'",
|
|
])
|
|
assert.match(value, /picked\?\.workflowStatus === 'AWAITING_REVIEW'/)
|
|
assert.match(value, /`\$\{PDA\}\/tasks\/\$\{taskId\}\/complete`/)
|
|
assert.match(value, /expectedTaskVersion: Number\(picked\?\.version\)/)
|
|
assert.match(value, /operationId: `\$\{context\.marker\}-pda-complete`\.slice\(0, 128\)/)
|
|
})
|
|
|
|
test('bridge loading confirms preload before creating and claiming the PDA task', () => {
|
|
const value = source('./phase3-loading.mjs')
|
|
assertOrder(value, [
|
|
'分配后自动建立预装关联且柜次计划可读',
|
|
'无号推送后预装清单正式可见',
|
|
'无号 push 成功后 PDA 查询明确 A0404',
|
|
'push exact replay 对计划版本历史关联和任务零漂移',
|
|
'无号且未关联订舱仍无 PDA 任务',
|
|
'三期订舱 RELEASED 冻结运输快照',
|
|
'模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号',
|
|
'订舱 RELEASED 但预装未 CONFIRMED 仍无 PDA 任务',
|
|
'计划级确认前读取即时版本与就绪门禁',
|
|
'计划级原子确认整份预装并触发唯一 PDA 任务',
|
|
'计划确认后 task 待领取且 claim 前仍 WAIT_SPLIT/EDITING',
|
|
'读取柜次级 PDA 唯一事实任务',
|
|
"claimedPlan?.status === 'LOADING'",
|
|
"const canonicalPick = await request",
|
|
"pickedPlan?.status === 'LOADING'",
|
|
"pickedPlan?.finalCompletionStatus === 'IN_PROGRESS'",
|
|
])
|
|
assert.match(value, /const confirmPath = `\/api\/platform\/v1\/first-leg\/loading-plans\/\$\{loadingPlanId\}\/confirm`/)
|
|
assert.match(value, /confirmed\?\.confirmationStatus === 'CONFIRMED'/)
|
|
assert.match(value, /confirmed\?\.readiness\?\.readyForPda === true/)
|
|
assert.match(value, /canonicalTaskId === confirmedTaskId/)
|
|
assert.match(value, /计划级确认同 key 异参返回冲突/)
|
|
assert.match(value, /已确认计划使用新 key 重复确认返回冲突/)
|
|
assert.doesNotMatch(value, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
|
|
assert.doesNotMatch(value.slice(value.indexOf('分配后自动建立预装关联且柜次计划可读'), value.indexOf('const linkResponse')),
|
|
/\/transport/)
|
|
assert.match(value, /missingContainerTaskResponse\.http === 200 && codeOf\(missingContainerTaskResponse\) === 'A0404'/)
|
|
assert.doesNotMatch(value, /无真实柜号拒绝推送柜次计划|PDA complete后补确认/)
|
|
assert.match(value, /billOfLadingNo:[\s\S]*vesselName:[\s\S]*voyageNo:[\s\S]*estimatedSailingAt:[\s\S]*estimatedArrivalAt:/)
|
|
assert.doesNotMatch(value, /P3-SHIPPING|P3-AGENT/)
|
|
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
|
|
assert.match(value, /const frozenTransportMatches = frozenTransportValid[\s\S]*transportFields\.every/)
|
|
assert.match(value, /RELEASED 订舱回显完整冻结运输快照且无真实柜号[\s\S]*if \(!bookingId \|\| !requirement\?\.id \|\| !frozenTransportMatches\) return/)
|
|
assert.doesNotMatch(value, /new Date\(Date\.now\(\)/)
|
|
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/)
|
|
assert.match(value, /独立柜未创建拖车单也不阻塞 PDA 装柜完成命令/)
|
|
})
|
|
|
|
test('PDA completion production and HTTP chain never call or gate on truck appointments', () => {
|
|
const service = source('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java')
|
|
const serviceTest = source('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java')
|
|
const loading = source('./phase3-loading.mjs')
|
|
assert.doesNotMatch(service, /TruckAppointmentService|completeLoadingFromPda|completeTruckAfterCommit/)
|
|
assert.match(serviceTest, /completionWorkerIsIndependentOfEveryTruckAppointmentState/)
|
|
for (const state of ['NO_APPOINTMENT', 'DRAFT', 'IN_PROGRESS', 'COMPLETED']) {
|
|
assert.match(serviceTest, new RegExp(state))
|
|
}
|
|
assert.match(serviceTest, /failedSameOperationIdCanResumeWithoutCreatingAnotherCommand[\s\S]*assertNoTruckCoupling/)
|
|
assert.match(serviceTest, /secondCustomerOwnerReadyFailureLeavesEveryOwnerUnsettledAndUncompleted[\s\S]*assertNoTruckCoupling/)
|
|
assert.match(serviceTest, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements[\s\S]*assertNoTruckCoupling/)
|
|
assert.doesNotMatch(loading, /\/truck-appointments|completeLoadingFromPda/)
|
|
})
|
|
|
|
test('automatic preload bridge repeated reads preserve the complete pre-PDA snapshot', () => {
|
|
for (const file of ['./phase3-loading.mjs', './phase3-container-loading.mjs']) {
|
|
const value = source(file)
|
|
assert.match(value, /const pushRequest = Object\.freeze\(\{ path: `\$\{(?:smartLoadingBase|SMART)\}\/plans\/\$\{(?:loadingPlanId|planId)\}` \}\)/)
|
|
assert.equal((value.match(/request\(context, 'GET', pushRequest\.path\)/g) || []).length >= 4, true)
|
|
assert.match(value, /push exact replay[\s\S]*零漂移/)
|
|
assert.match(value, /自动关联计划重复读取稳定[\s\S]*自动关联计划再次读取稳定/)
|
|
assert.match(value, /重放后回查柜次计划[\s\S]*重放后回查预装历史[\s\S]*重放后回查 bridge/)
|
|
assert.match(value, /codeOf\(rejectedTaskResponse\) === 'A0404'/)
|
|
assert.match(value, /projectPushPreloadStability/)
|
|
assert.match(value, /diffPushPreloadStability/)
|
|
assert.match(value, /首次 push 后读取事务完成态柜次计划/)
|
|
assert.match(value, /事务完成态计划版本与 bridge 一致且保持 EDITING/)
|
|
assert.match(value, /plan: postPushPlan, detail:/)
|
|
assert.match(value, /changed=\$\{(?:pushReplayDiff|replayDiff|rejectedDiff)\.join/)
|
|
assert.doesNotMatch(value, /\/plans\/[^'`]*\/push-preload/)
|
|
assert.doesNotMatch(value, /baselineBytes=|replayBytes=|rejectedBytes=/)
|
|
}
|
|
})
|
|
|
|
test('push stability projection sorts unordered facts and reports persisted field drift', () => {
|
|
const baseline = projectPushPreloadStability({
|
|
plan: { id: '10', bookingId: '20', status: 'EDITING', version: 2,
|
|
items: [{ id: '2', inboundId: '42', packageCount: 1, version: 1 },
|
|
{ id: '1', inboundId: '41', packageCount: 1, version: 1 }] },
|
|
detail: { summary: { planId: '10', containerBookingId: '20', status: 'EDITING', version: 2,
|
|
readiness: { preloadPushed: true, preloadLinkCount: 2, confirmationStatus: 'PENDING',
|
|
blockingReasons: ['CONTAINER_NO_MISSING', 'OCEAN_BOOKING_NOT_LINKED'] } },
|
|
items: [{ itemId: '2', inboundId: '42', packageCount: 1 }, { itemId: '1', inboundId: '41', packageCount: 1 }],
|
|
histories: [{ historyId: '2', actionType: 'PUSH' }, { historyId: '1', actionType: 'CREATE' }] },
|
|
links: [{ id: '2', inboundId: '42', loadingPlanId: '10', allocatedPackageCount: 1 },
|
|
{ id: '1', inboundId: '41', loadingPlanId: '10', allocatedPackageCount: 1 }],
|
|
task: { http: 200, code: 'A0404' },
|
|
})
|
|
const reordered = projectPushPreloadStability({
|
|
plan: { id: '10', bookingId: '20', status: 'EDITING', version: 2,
|
|
items: [{ id: '1', inboundId: '41', packageCount: 1, version: 1 },
|
|
{ id: '2', inboundId: '42', packageCount: 1, version: 1 }], updatedAt: 'ignored' },
|
|
detail: { summary: { planId: '10', containerBookingId: '20', status: 'EDITING', version: 2,
|
|
readiness: { preloadPushed: true, preloadLinkCount: 2, confirmationStatus: 'PENDING',
|
|
blockingReasons: ['OCEAN_BOOKING_NOT_LINKED', 'CONTAINER_NO_MISSING'] } },
|
|
items: [{ itemId: '1', inboundId: '41', packageCount: 1 }, { itemId: '2', inboundId: '42', packageCount: 1 }],
|
|
histories: [{ historyId: '1', actionType: 'CREATE' }, { historyId: '2', actionType: 'PUSH' }] },
|
|
links: [{ id: '1', inboundId: '41', loadingPlanId: '10', allocatedPackageCount: 1 },
|
|
{ id: '2', inboundId: '42', loadingPlanId: '10', allocatedPackageCount: 1 }],
|
|
task: { http: 200, code: 'A0404' },
|
|
})
|
|
assert.deepEqual(diffPushPreloadStability(baseline, reordered), [])
|
|
const drifted = structuredClone(reordered)
|
|
drifted.plan.version = 3
|
|
drifted.links[0].preloadVersion = 2
|
|
assert.deepEqual(diffPushPreloadStability(baseline, drifted), [
|
|
'links[0].preloadVersion:null->2', 'plan.version:2->3',
|
|
])
|
|
})
|
|
|
|
test('assignment transaction establishes preload and bridge before PDA synchronization', () => {
|
|
const service = source('../../firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java')
|
|
const serviceTest = source('../../firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java')
|
|
const assign = service.slice(service.indexOf('public Plan assign('), service.indexOf('public Plan assignInbound('))
|
|
assert.match(service, /@Transactional[\s\S]{0,300}public Plan assign\(/)
|
|
assert.match(assign, /splitPreloadService\.ensure(?:Pending|ForRunning)[\s\S]*touch\(plan[\s\S]*history\([\s\S]*loadingModelBridge\.bindIncrementalAllocation[\s\S]*loadingPickService\.sync/)
|
|
assert.match(serviceTest, /addsItemToLoadingPlanWithoutChangingStableTaskOrPlanStatus/)
|
|
assert.match(serviceTest, /inOrder\(loadingPickService, splitPreloadService, repository, loadingModelBridge\)/)
|
|
assert.match(serviceTest, /order\.verify\(loadingModelBridge\)\.bindIncrementalAllocation/)
|
|
})
|
|
|
|
test('finalize rechecks the same unified order detail and list as LOADED', () => {
|
|
const value = source('./phase3-loading-finalize.mjs')
|
|
assertOrder(value, [
|
|
'入库单终态 LOADED、版本递增且审计历史存在',
|
|
'LOADED 后回查同一统一订单详情',
|
|
'统一订单详情投影同一订单为 LOADED',
|
|
'LOADED 后回查同一统一订单列表',
|
|
'统一订单列表投影同一订单为 LOADED',
|
|
])
|
|
assert.match(value, /summary\?\.mainStatus === 'LOADED'/)
|
|
assert.match(value, /unifiedRow\?\.mainStatus === 'LOADED'/)
|
|
assert.match(source('./phase3-loading.mjs'), /tenantId, orderId: id\(source\?\.orderId\), inboundId/)
|
|
})
|
|
|
|
test('PDA inventory snapshot gates batch and records whole-ticket block identity', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assertOrder(loading, [
|
|
'读取 PDA 提交前实时库存快照',
|
|
'未发生库存变化时重复读取实时库存快照',
|
|
'PDA 库存快照按入仓单返回唯一整票库位块身份',
|
|
'客户身份无权读取平台 PDA 实时库存快照',
|
|
'const canonicalPick = await request',
|
|
'const canonicalBatchReplay = await request',
|
|
])
|
|
assert.match(loading, /inventorySnapshotToken: inventory\.snapshotToken/)
|
|
assert.match(loading, /warehouseId, loadingPlanId, containerBookingId,/)
|
|
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)/)
|
|
assert.match(loading, /inventorySnapshotVerified: inventorySnapshotVerified && atomicBatchReplayVerified/)
|
|
assert.match(loading, /inventoryBlockIdentityVerified, inventoryBlockSnapshots/)
|
|
assert.match(loading, /partialBlockSnapshot/)
|
|
assert.doesNotMatch(loading, /occupancyId|temporary-occupancies|\/rebind/)
|
|
|
|
const convergence = source('./phase3-pda-convergence.mjs')
|
|
assert.doesNotMatch(convergence, /未公开实时库存查询或库存版本快照端点/)
|
|
assert.match(convergence, /fixture\?\.inventorySnapshotVerified !== true/)
|
|
assert.match(convergence, /fixture\?\.inventoryBlockIdentityVerified !== true/)
|
|
})
|
|
|
|
test('container loading arranges a simulation before linking a released booking requirement', () => {
|
|
const value = source('./phase3-container-loading.mjs')
|
|
assert.match(value, /candidates\.find\(\(candidate\) => candidate\.marker === context\.marker\)/)
|
|
assertOrder(value, [
|
|
"const simulationResponse = await request(context, 'POST', CONTAINERS",
|
|
"const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=",
|
|
"const planResponse = await request(context, 'GET', `${SMART}/plans/by-booking/",
|
|
"const assignResponse = await request",
|
|
'分配后自动建立预装关联且计划可读',
|
|
"const bookingCreate = await request",
|
|
"const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`",
|
|
])
|
|
assert.match(value, /publishPlatformLogisticsChannels\(context, warehouseId, MODULE\)/)
|
|
const draft = value.slice(value.indexOf('const bookingDraft = {'), value.indexOf('context.assert(MODULE, \'订舱渠道与仓库快照完整\''))
|
|
assert.doesNotMatch(draft, /warehouseName\s*:/)
|
|
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
|
|
assert.match(draft, new RegExp(`${field}:`))
|
|
}
|
|
assert.doesNotMatch(value, /loading-booking-(?:submit|confirm)|ocean-bookings\/.*\/(?:submit|confirm)/)
|
|
assert.match(value, /confirmedBooking\.containerRequirements\.length === 2/)
|
|
assert.match(value, /oceanBookingId: bookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/)
|
|
assert.match(value, /function responseEvidence\(response\)[\s\S]*HTTP=\$\{response\?\.http \?\? 'missing'\}[\s\S]*code=\$\{codeOf\(response\)\}[\s\S]*requestId=\$\{response\?\.requestId \|\| 'missing'\}[\s\S]*traceId=\$\{response\?\.traceId \|\| 'missing'\}/)
|
|
assert.match(value, /关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份/)
|
|
assert.match(value, /订舱 allocation 原子创建并指向稳定模拟柜身份/)
|
|
assert.match(value, /已消费订舱需求拒绝再次关联/)
|
|
assertOrder(value, [
|
|
"const linkedContainer = context.apiSuccess(MODULE, '首次关联 RELEASED 订舱需求并补真实柜号', linkResponse,",
|
|
'if (!linkedContainer) return',
|
|
"const afterLinkPlanResponse = await context.request('GET'",
|
|
"const duplicateSimulationResponse = await request(context, 'POST', CONTAINERS",
|
|
])
|
|
assert.doesNotMatch(value, /realContainerBookingId|MERGED/)
|
|
assert.match(value, /关联前后内部柜次、计划和排货明细身份稳定/)
|
|
assert.match(value, /bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=/)
|
|
assert.match(value, /tasks\?loadingPlanId=\$\{encodeURIComponent\(planId\)\}&warehouseId=/)
|
|
assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/)
|
|
assert.doesNotMatch(value.slice(value.indexOf('分配后自动建立预装关联且计划可读'), value.indexOf('const linkResponse')),
|
|
/\/transport/)
|
|
assert.match(value, /订舱放舱为 RELEASED 且不预生成真实货柜/)
|
|
assert.match(value, /分配后自动建立预装关联且计划可读/)
|
|
assert.match(value, /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/)
|
|
assert.match(value, /计划级原子确认整份预装并触发唯一 PDA 任务/)
|
|
assert.match(value, /confirmationStatus === 'CONFIRMED'/)
|
|
assert.match(value, /readiness\?\.readyForPda === true/)
|
|
assert.match(value, /taskId === confirmedTaskId/)
|
|
assert.match(value, /计划级确认同 key 异参返回冲突/)
|
|
assert.match(value, /已确认计划使用新 key 重复确认返回冲突/)
|
|
assert.doesNotMatch(value, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
|
|
assert.match(value, /POST', CONTAINERS/)
|
|
})
|
|
|
|
test('container loading selects the current marker instead of a stale CAB fixture', () => {
|
|
const selected = selectContainerLoadingFixture({
|
|
marker: 'ADMIN-RUN-AUTO1',
|
|
createdResources: {
|
|
orchestrationFixtures: {
|
|
phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' },
|
|
},
|
|
inboundMeasurement: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-INBOUND' },
|
|
orders: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-ORDER-INBOUND' },
|
|
},
|
|
})
|
|
assert.equal(selected.inboundId, 'FINANCE-INBOUND')
|
|
assert.equal(selected.marker, 'ADMIN-RUN-AUTO1')
|
|
})
|
|
|
|
test('container loading keeps an unmatched candidate so marker failure stays observable', () => {
|
|
const selected = selectContainerLoadingFixture({
|
|
marker: 'ADMIN-RUN-AUTO1',
|
|
createdResources: {
|
|
orchestrationFixtures: {
|
|
phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' },
|
|
},
|
|
},
|
|
})
|
|
assert.equal(selected.marker, 'ADMIN-RUN-CAB')
|
|
})
|
|
|
|
test('channel isolation accepts only two active channels from the current platform warehouse catalog', () => {
|
|
const selected = selectPlatformChannelIsolationPair({
|
|
marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '41',
|
|
sourcePath: '/api/platform/v1/frozen-channel-contract?originWarehouseId=41',
|
|
channels: [
|
|
{ channelCode: 'SEA-A', channelName: '海运 A', status: 'ACTIVE' },
|
|
{ channelCode: 'SEA-B', channelName: '海运 B', status: 'ACTIVE' },
|
|
{ channelCode: 'SEA-C', channelName: '海运 C', status: 'DISABLED' },
|
|
],
|
|
}, 'ADMIN-RUN-AUTO1', '41', 'SEA-A')
|
|
assert.deepEqual(selected.pair, {
|
|
primary: { channelCode: 'SEA-A', channelName: '海运 A' },
|
|
foreign: { channelCode: 'SEA-B', channelName: '海运 B' },
|
|
})
|
|
assert.equal(selected.reason, null)
|
|
})
|
|
|
|
test('channel isolation reports BLOCKED_FIXTURE instead of inventing a second channel', () => {
|
|
for (const catalog of [
|
|
{ marker: 'OLD-RUN', originWarehouseId: '41', sourcePath: '/api/platform/v1/channels', channels: [] },
|
|
{ marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '42', sourcePath: '/api/platform/v1/channels', channels: [] },
|
|
{ marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '41', sourcePath: '/api/merchant/v1/first-leg/pricing/channels', channels: [] },
|
|
{ marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '41', sourcePath: '/api/platform/v1/channels',
|
|
channels: [{ channelCode: 'SEA-A', channelName: '海运 A', status: 'ACTIVE' }] },
|
|
]) {
|
|
const selected = selectPlatformChannelIsolationPair(catalog, 'ADMIN-RUN-AUTO1', '41', 'SEA-A')
|
|
assert.equal(selected.pair, null)
|
|
assert.match(selected.reason, /^BLOCKED_FIXTURE:/)
|
|
}
|
|
const value = source('./phase3-container-loading.mjs')
|
|
assert.doesNotMatch(value, /foreignChannelCode|\$\{channelCode\}-OTHER|异渠道-\$\{suffix\}/)
|
|
assert.match(value, /context\.createdResources\?\.platformLogisticsChannels/)
|
|
})
|
|
|
|
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, [
|
|
'新增内部模拟柜',
|
|
'新增真实异渠道模拟柜',
|
|
'手工装柜允许两个真实有效渠道之间跨渠道分配',
|
|
'跨渠道分配验收后释放入仓码供主流程继续',
|
|
'人工分配本轮候选货物',
|
|
'分配后自动建立预装关联且计划可读',
|
|
'无号 push 成功后 PDA 查询 A0404',
|
|
'无号且未关联订舱不生成 PDA 任务',
|
|
'订舱放舱为 RELEASED 且不预生成真实货柜',
|
|
'首次关联 RELEASED 订舱需求并补真实柜号',
|
|
'订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务',
|
|
'计划级确认前读取即时版本与就绪门禁',
|
|
'计划级原子确认整份预装并触发唯一 PDA 任务',
|
|
'计划确认后 task 待领取且 claim 前仍 WAIT_SPLIT/EDITING',
|
|
'换柜号前读取 PDA 稳定库存快照',
|
|
'PDA 任务创建后替换真实柜号',
|
|
'PDA 任务创建后清空真实柜号失败',
|
|
'换柜号后按稳定计划身份回查 PDA 任务',
|
|
'换柜号不改变任务计划柜次柜次号和库存快照身份',
|
|
])
|
|
assert.match(value, /containerDetail\?\.channelCode === channelCode/)
|
|
assert.match(value, /id\(replacedTask\?\.cabinetNo\) === stableIdentity\.cabinetNo/)
|
|
assert.match(value, /afterReplacementSnapshot\?\.snapshotToken === beforeReplacementSnapshot\?\.snapshotToken/)
|
|
assert.match(value, /pdaIdentityStable: true/)
|
|
assert.match(value, /resolveOceanBookingAgent\(context, MODULE, 'MATSON', OPTIONS\)/)
|
|
assert.match(value, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name, carrierCode: 'MATSON'/)
|
|
assert.match(value, /bookingAgentId: id\(bookingAgent\.id\), bookingAgentName: bookingAgent\.name/)
|
|
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
|
|
assert.match(value, /if \(!frozenTransportMatches\) return/)
|
|
assert.match(value, /const releaseVersion = Number\(bookingDocument\?\.bookingVersion\)/)
|
|
assert.match(value, /expectedVersion: releaseVersion, reason: context\.marker/)
|
|
assertOrder(value, ['上传有效订舱确认文件', '订舱确认文件已关联且版本递增',
|
|
'订舱放舱为 RELEASED 且不预生成真实货柜'])
|
|
assert.doesNotMatch(value, /const sailingAt = new Date\(Date\.now\(\)/)
|
|
const batch = value.slice(value.indexOf('const staleBatchResponse'), value.indexOf('const completePath'))
|
|
assert.doesNotMatch(batch, /containerNo:/)
|
|
})
|
|
|
|
test('preload and loading versions use immediate baselines and stable automatic bridge reads', () => {
|
|
const container = source('./phase3-container-loading.mjs')
|
|
assert.match(container, /const pushRequest = Object\.freeze\(\{ path: `\$\{SMART\}\/plans\/\$\{planId\}` \}\)/)
|
|
assert.equal((container.match(/request\(context, 'GET', pushRequest\.path\)/g) || []).length >= 4, true)
|
|
assert.match(container, /自动关联保持 EDITING 且无需额外推送版本/)
|
|
assert.match(container, /const beforeClaimPlanVersion = Number\(beforeClaimPlan\?\.version\)/)
|
|
assert.match(container, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
|
|
|
|
const loading = source('./phase3-loading.mjs')
|
|
assert.match(loading, /const pushRequest = Object\.freeze\(\{ path: `\$\{smartLoadingBase\}\/plans\/\$\{loadingPlanId\}` \}\)/)
|
|
assert.equal((loading.match(/request\(context, 'GET', pushRequest\.path\)/g) || []).length >= 4, true)
|
|
assert.match(loading, /自动关联保持 EDITING、身份稳定且无需额外推送版本/)
|
|
assert.match(loading, /push exact replay 对计划版本历史关联和任务零漂移/)
|
|
assert.match(loading, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
|
|
assert.match(loading, /MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'/)
|
|
assert.match(loading, /category=BOOKING&page=1&size=200/)
|
|
assert.match(loading, /ensureMatsonSupplier\(context, supplierRecords\)/)
|
|
assert.match(loading, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name/)
|
|
assert.match(loading, /bookingAgentId: id\(supplier\.id\), bookingAgentName: supplier\.name/)
|
|
assert.doesNotMatch(loading, /shippingCompanyId: id\(supplier\.id\), shippingCompanyName: supplier\.name/)
|
|
assert.match(loading, /cartonMarks: stableLoadedCartonMarks\(stagedTask\?\.items\)/)
|
|
assert.match(source('./phase3-loading-finalize.mjs'), /cartonMarks: Array\.isArray\(fixture\.cartonMarks\)/)
|
|
})
|
|
|
|
test('loaded carton fixture mirrors partial settlement and keeps the lowest carton indexes', () => {
|
|
const marks = stableLoadedCartonMarks([
|
|
{ loadingPlanItemId: '30', inboundId: '200', pickedPackageCount: 1,
|
|
boxCodes: ['NYEXX2609020358#8-5'] },
|
|
{ loadingPlanItemId: '20', inboundId: '200', pickedPackageCount: 2,
|
|
boxCodes: ['NYEXX2609020358#8-2', 'NYEXX2609020358#8-1', 'NYEXX2609020358#8-3'] },
|
|
{ loadingPlanItemId: '10', inboundId: '100', pickedPackageCount: 1,
|
|
boxCodes: ['NYEXX2609020357#4-4', 'NYEXX2609020357#4-1'] },
|
|
])
|
|
assert.deepEqual(marks, [
|
|
'NYEXX2609020357#4-1',
|
|
'NYEXX2609020358#8-1',
|
|
'NYEXX2609020358#8-2',
|
|
'NYEXX2609020358#8-3',
|
|
])
|
|
})
|
|
|
|
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, [
|
|
'最后一箱拣货后 PDA 任务等待人工确认装柜完成',
|
|
'上传并绑定柜次 ${evidenceType} 真实证据文件',
|
|
'${evidenceType} 有效证据归属稳定且锁码门禁完整',
|
|
'PDA 完成前回查五类有效装柜证据与锁码',
|
|
'PDA 提交人工确认装柜完成',
|
|
'等待 PDA 装柜完成异步收敛',
|
|
])
|
|
assert.match(container, /const evidenceTypes = \['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'\]/)
|
|
assert.match(container, /form\.append\('evidenceType', evidenceType\)/)
|
|
assert.match(container, /new Blob\(\[png\], \{ type: 'image\/png' \}\)/)
|
|
assert.match(container, /item\.evidenceType === 'SEAL' && item\.sealNo === sealNo/)
|
|
assert.match(container, /if \(!evidenceTypes\.every[\s\S]*\) return/)
|
|
assert.match(container, /requestBinary\('GET',[\s\S]*\/evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/content/)
|
|
assert.match(container, /回查完成后的 PDA 操作审计/)
|
|
assert.match(container, /item\.actionType === 'BATCH'/)
|
|
assert.match(container, /item\.actionType === 'COMPLETE_LOADING'/)
|
|
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 = dropshipWholePallet[\s\S]*\? \[batchKey\][\s\S]*: \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
|
|
assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/)
|
|
assert.match(convergence, /expectedBatchCount = fixture\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP' \? 1 : 4/)
|
|
assert.match(convergence, /expectedBatchOperationIds\.length === expectedBatchCount/)
|
|
assert.doesNotMatch(convergence, /expectedBatchCount = fixture\?\.dynamicIncrementVerified/)
|
|
})
|
|
|
|
test('phase3 loading proves mixed-line rollback across task, all items and operations', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assertOrder(loading, [
|
|
'非法混合批次前读取 PDA 全量任务基线',
|
|
'PDA 原子批次任一非法明细时整批拒绝',
|
|
'回查失败批次后的 PDA 任务快照',
|
|
'非法混合批次后回查 PDA 操作历史',
|
|
'合法明细加非法明细失败后任务、全部明细和操作历史零写入',
|
|
])
|
|
assert.match(loading, /taskItemsSignature\(unchangedAfterInvalid\) === taskItemsSignature\(beforeInvalidTask\)/)
|
|
assert.match(loading, /operationsSignature\(afterInvalidOperations\) === operationsSignature\(beforeInvalidOperations\)/)
|
|
assert.doesNotMatch(loading, /tasks\?containerNo=/)
|
|
assert.equal((loading.match(/tasks\?loadingPlanId=/g) || []).length >= 6, true)
|
|
})
|
|
|
|
test('phase3 loading proves stale inventory zero-write and defers final completion audit', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assert.match(loading, /计划详情由服务端返回权威总箱数和剩余箱数/)
|
|
assert.match(loading, /authoritativeRemaining === authoritativeTotal - assignedPackageCount/)
|
|
assert.match(loading, /调整箱数超过服务端权威总箱数返回A0422/)
|
|
assert.match(loading, /超权威总箱数失败后计划和明细零写入/)
|
|
assert.match(loading, /taskItemsSignature\(taskAfterStale\) === taskItemsSignature\(incrementedTask\)/)
|
|
assert.match(loading, /动态新增计划明细按 inboundDetailId 唯一定位/)
|
|
assert.match(loading, /重新加入动态明细仍按 inboundDetailId 唯一定位/)
|
|
assert.equal((loading.match(/id\(item\?\.inboundDetailId\) === id\(dynamicCandidate\.inboundDetailId\)/g) || []).length, 2)
|
|
assert.doesNotMatch(loading, /items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
|
|
assertOrder(loading, [
|
|
'回查下游门禁前原子 Batch 审计',
|
|
'最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING',
|
|
'LOADING 阶段向同一 PDA task 增加入库单',
|
|
'增量后旧 inventory token 明确 A0409 并要求刷新',
|
|
'增量旧 token 冲突后 plan/task/items 零写入',
|
|
'重新增量后刷新 inventory snapshot',
|
|
'刷新后完成动态新增项拣货',
|
|
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
|
|
])
|
|
assert.doesNotMatch(loading, /complete-before-downstream/)
|
|
})
|
|
|
|
test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstream completion', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
const measurement = source('./inbound-measurement.mjs')
|
|
assertOrder(loading, [
|
|
'本轮增量 fixture 提供独立 SKIP 与 PARTIAL 合法货物明细',
|
|
'加入本轮待跳拣异常明细',
|
|
'SKIP 携带异常原因并原子解决待拣明细',
|
|
'合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW',
|
|
'加入本轮部分拣货异常明细',
|
|
'未解决异常完成前读取 PDA 操作审计基线',
|
|
'存在未解决异常明细时禁止确认装柜完成',
|
|
'未解决异常完成失败后 task/items/operation 全部零写入',
|
|
'合法 PARTIAL 解决未完成明细',
|
|
'部分拣货数量守恒并恢复最终可确认状态',
|
|
'回查柜次 PDA 异常原因与已解决进度',
|
|
'SKIP 异常原因持久化且 PARTIAL 后无未解决明细',
|
|
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
|
|
])
|
|
assert.match(loading, /action: 'SKIP', version: Number\(skipTaskItem\?\.version\),\s*reason: skipReason/)
|
|
assert.match(loading, /skipReason = `\$\{context\.marker\}-箱唛破损待复核`/)
|
|
assert.match(loading, /itemStatus === 'SKIPPED'[\s\S]*pickedPackageCount\) === 0/)
|
|
assert.match(loading, /action: 'PARTIAL', finalPickedPackageCount: 1/)
|
|
assert.match(loading, /plannedPackageCount\) === 2[\s\S]*pickedPackageCount\) === 1/)
|
|
assert.match(loading, /String\(unresolvedComplete\.body\?\.message \|\| ''\)\.includes\('尚未处理'\)/)
|
|
assert.match(loading, /taskItemsSignature\(afterUnresolvedCompleteTask\) === taskItemsSignature\(unresolvedTask\)/)
|
|
assert.match(loading, /operationsSignature\(operationsAfterUnresolvedComplete\) === operationsSignature\(operationsBeforeUnresolvedComplete\)/)
|
|
assert.match(loading, /exceptionOverview\?\.pda\?\.exceptionReasons\?\.includes\?\.\(skipReason\)/)
|
|
assert.match(loading, /Number\(exceptionOverview\?\.pda\?\.unresolvedItemCount\) === 0/)
|
|
assert.doesNotMatch(loading, /addCoverageGap\([^)]*(SKIP|PARTIAL|未解决异常)/)
|
|
assert.match(entry, /this\.context\.phase3LoadingIncrementalDetailCount = 4/)
|
|
assert.match(measurement, /增量 fixture 形成动态真实独立货物测量明细/)
|
|
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 paginates A0 candidates and refuses mass location mutations', () => {
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
const orders = source('./orders.mjs')
|
|
assertOrder(entry, [
|
|
'disableAvailableLocationsForA0(warehouseId, requiredPalletCount)',
|
|
'this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow',
|
|
"executeModule('inbound-measurement-phase3-loading-fixture'",
|
|
'restoreAvailableLocationsAfterA0(warehouseId, isolation)',
|
|
])
|
|
assert.match(entry, /page=\$\{pageNumber\}&size=\$\{A0_LOCATION_PAGE_SIZE\}/)
|
|
assert.match(entry, /A0_MAX_LOCATION_MUTATIONS = 8/)
|
|
assert.match(entry, /A0 大仓保护:不批量停用普通库位/)
|
|
assert.match(entry, /remainingNormal\.length < isolation\.requiredSlots/)
|
|
assert.match(entry, /\/locations\/\$\{encodeURIComponent\(location\.id\)\}\/status/)
|
|
assert.match(entry, /\{ status: 'DISABLED', version: Number\(location\.version\) \}/)
|
|
assert.match(entry, /\{ status: 'AVAILABLE', version: location\.version \}/)
|
|
assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/)
|
|
})
|
|
|
|
test('customs reuses the loading chain released booking without pre-completing truck', () => {
|
|
const value = source('./phase3-customs.mjs')
|
|
assertOrder(value, [
|
|
'selectCustomsFlow(context.createdResources)',
|
|
'if (!await ensureReleasedOceanBooking(context, container)) return',
|
|
'const slicesPath =',
|
|
])
|
|
assert.match(value, /const oceanBookingId = id\(container\?\.oceanBookingId\)/)
|
|
assert.match(value, /ocean-bookings\/\$\{encodeURIComponent\(oceanBookingId\)\}/)
|
|
assert.match(value, /detail\?\.containers\?\.some/)
|
|
assert.doesNotMatch(value, /oceanBookings\.run\(context\)/)
|
|
assert.doesNotMatch(value, /serviceCenter\.run\(context\)/)
|
|
assert.doesNotMatch(value, /truck\?\.finalStatus !== 'COMPLETED'/)
|
|
assert.match(value, /request\('POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)
|
|
assert.match(value, /category: 'CUSTOMS'/)
|
|
assert.match(value, /context\.createdResources\.phase3CustomsSupplier/)
|
|
assert.match(value, /const batchPayload = \{ customsAgentId,/)
|
|
assert.doesNotMatch(value, /customsAgentName: `代理-/)
|
|
})
|
|
|
|
test('downstream container consumers prefer the phase3Loading fixture', () => {
|
|
const ocean = source('./ocean-bookings.mjs')
|
|
const customs = source('./phase3-customs.mjs')
|
|
const serviceCenter = source('./service-center-flow.mjs')
|
|
assert.match(customs, /createdResources\?\.phase3Loading,\s*\n\s*createdResources\?\.phase3ContainerLoading/)
|
|
assert.match(serviceCenter, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/)
|
|
assert.match(ocean, /完整 DRAFT 放舱为 RELEASED 且冻结五项运输事实/)
|
|
assert.match(ocean, /finalStatus: 'RELEASED', containerBookingId: null, loadingPlanId: null/)
|
|
assert.match(source('./phase3-loading.mjs'), /oceanBookingId: bookingId, loadingPlanId, containerBookingId/)
|
|
assert.match(serviceCenter, /category=TRUCKING&status=ACTIVE/)
|
|
assert.match(serviceCenter, /oceanBookingId: ocean\.id/)
|
|
const servicePayload = serviceCenter.slice(serviceCenter.indexOf('const payload = {'),
|
|
serviceCenter.indexOf('const response =', serviceCenter.indexOf('const payload = {')))
|
|
assert.doesNotMatch(servicePayload, /containerBookingId:|loadingPlanId:/)
|
|
assert.match(serviceCenter, /carrierId: id\(carrier\.id\), carrierName: carrier\.name/)
|
|
assert.doesNotMatch(serviceCenter, /customerId:|customerName:/)
|
|
assert.doesNotMatch(serviceCenter, /链路承运商-/)
|
|
assert.doesNotMatch(serviceCenter, /customerName: `服务中心链路客户-/)
|
|
const schedules = source('./phase3-port-schedules.mjs')
|
|
assert.match(schedules, /ensureOceanBooking\(context, suffix, master, flow\?\.oceanBookingId\)/)
|
|
assert.match(schedules, /const containerBookingId = id\(flow\?\.containerBookingId\) \|\| null/)
|
|
assert.doesNotMatch(schedules, /\['COMPLETED', 'LOADED'\]\.includes\(flow\?\.finalStatus\)/)
|
|
assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containers \|\| \[\]\)/)
|
|
assert.match(schedules, /ocean\.allocatedContainerBookingIds\?\.includes\(containerBookingId\)/)
|
|
assert.match(schedules, /if \(containerBookingId && !knownAllocation\)/)
|
|
})
|
|
|
|
test('default acceptance order finalizes LOADED before customs reads fulfillment slices', () => {
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
assertOrder(entry, [
|
|
"'phase3-loading'",
|
|
"'phase3-loading-model-bridge'",
|
|
"'phase3-pda-convergence'",
|
|
"'phase3-evidence-gate'",
|
|
"'phase3-loading-finalize'",
|
|
"'phase3-customs'",
|
|
])
|
|
})
|
|
|
|
test('container overview always follows phase3 loading in default and custom module selections', () => {
|
|
const reordered = normalizeModuleOrder(['phase3-container-overview', 'orders', 'phase3-loading', 'finance'])
|
|
assert.deepEqual(reordered, ['merchant-qualification-fixture', 'orders', 'inbound-measurement',
|
|
'phase3-container-loading', 'phase3-loading', 'phase3-container-overview', 'finance'])
|
|
assert.deepEqual(parseArguments(['--modules', 'phase3-container-overview,phase3-loading']).modules,
|
|
['merchant-qualification-fixture', 'orders', 'inbound-measurement', 'phase3-container-loading',
|
|
'phase3-loading', 'phase3-container-overview'])
|
|
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
assert.match(entry, /const DEFAULT_MODULES = normalizeModuleOrder\(CONFIGURED_MODULES\)/)
|
|
assert.match(entry, /const modules = normalizeModuleOrder\(/)
|
|
assert.match(entry, /return \{[\s\S]{0,500}modules,/)
|
|
})
|
|
|
|
test('overview does not fall back to a container fixture when loading evidence was requested', () => {
|
|
const overview = source('./phase3-container-overview.mjs')
|
|
assert.match(overview, /loadingRequested[\s\S]*\? context\.createdResources\?\.phase3Loading/)
|
|
assert.match(overview, /source=\$\{loadingRequested \? 'phase3Loading-required'/)
|
|
})
|
|
|
|
test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', async () => {
|
|
const run = new AcceptanceRun({ gateway: 'http://127.0.0.1', modules: [], username: '', password: '', output: '' })
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
assert.match(entry, /module === 'phase3-loading'/)
|
|
assert.match(entry, /preparePhase3LoadingFixture\('', requireSharedOverflow\)/)
|
|
assert.match(entry, /orders-phase3-loading-fixture/)
|
|
assert.match(entry, /inbound-measurement-phase3-loading-fixture/)
|
|
assert.match(entry, /marker: originalMarker/)
|
|
assert.match(entry, /sourceMarker: fixtureMarker/)
|
|
assert.match(entry, /phase3Loading: loadingFixture,\s*\n\s*phase3LoadingIncremental: incrementalFixture,\s*\n\s*inboundMeasurement: loadingFixture/)
|
|
assert.equal(run.marker.includes('-LOAD'), false)
|
|
})
|
|
|
|
test('container fixture isolates order idempotency while publishing the run marker', () => {
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
assert.match(entry, /const fixtureMarker = `\$\{originalMarker\}-CAB`/)
|
|
assert.match(entry, /marker: originalMarker,\s*\n\s*sourceMarker: fixtureMarker/)
|
|
assert.match(entry, /fixture\.finalStatus !== 'APPROVED'/)
|
|
assert.match(entry, /禁止回退使用 WAIT_PRICING 订单/)
|
|
})
|
|
|
|
test('measurement fixture polls asynchronous completion before exporting loading identity', () => {
|
|
const measurement = source('./inbound-measurement.mjs')
|
|
assert.match(measurement, /waitForCompletedMeasurement/)
|
|
assert.match(measurement, /expectedStatuses\.includes\(measurement\?\.processStatus\)/)
|
|
assert.match(measurement, /setTimeout\(resolve, 1_000\)/)
|
|
})
|
|
|
|
test('customs and finalize consume the same loading inbound identity', () => {
|
|
const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 }
|
|
const loading = {
|
|
marker: 'RUN-1', tenantId: 10001, inboundId: 201,
|
|
containerBookingId: 301, loadingPlanId: 401, finalStatus: 'COMPLETED',
|
|
}
|
|
const selected = selectCustomsFlow({
|
|
orchestrationFixtures: {
|
|
phase3Loading: loadingFixture,
|
|
inboundMeasurement: loadingFixture,
|
|
},
|
|
phase3Loading: loading,
|
|
})
|
|
assert.equal(selected.fixture.marker, loading.marker)
|
|
assert.equal(selected.fixture.inboundId, loading.inboundId)
|
|
assert.equal(selected.container.inboundId, loading.inboundId)
|
|
|
|
const finalized = buildFinalizeResource(loading, {
|
|
summary: { processStatus: 'LOADED', version: 6 },
|
|
}, { http: 200, body: { code: '200' } })
|
|
assert.equal(finalized.marker, loading.marker)
|
|
assert.equal(finalized.inboundId, loading.inboundId)
|
|
assert.equal(finalized.finalStatus, 'LOADED')
|
|
})
|
|
|
|
test('loading evidence acceptance covers idempotency, reversible missing-type gate and scope isolation', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assertOrder(loading, [
|
|
'装柜证据 task 上传同键同载荷重放',
|
|
'装柜证据 task 上传同键异载荷返回冲突',
|
|
'装柜证据作废旧版本返回冲突',
|
|
'临时作废唯一 PHOTO_STEP_4 证据',
|
|
'已作废 task 证据重复作废返回冲突',
|
|
'已作废 task 证据异参重试仍返回冲突',
|
|
'已作废证据使用新请求重复作废返回冲突',
|
|
'作废唯一证据类型后不提前占用异步完成命令',
|
|
'补传恢复 PHOTO_STEP_4 有效证据',
|
|
'客户身份无权访问平台装柜证据',
|
|
'错误仓库作用域不可读取本轮 task 装柜证据',
|
|
'错误 taskId 不可读取本轮装柜证据',
|
|
])
|
|
assert.match(loading, /missingTypeVoidedEvidenceId/)
|
|
assert.match(loading, /replacementEvidenceFileId/)
|
|
assert.match(source('./phase3-loading-finalize.mjs'), /waitForCompletion\(context, taskDetailPath/)
|
|
})
|
|
|
|
test('loaded boundary acceptance proves evidence and transport immutability with zero drift', () => {
|
|
const finalize = source('./phase3-loading-finalize.mjs')
|
|
assertOrder(finalize, [
|
|
'LOADED 后使用新键重复 complete 返回冲突',
|
|
'LOADED complete 同键异参返回冲突',
|
|
'LOADED 后新增装柜证据被拒绝',
|
|
'LOADED 后作废装柜证据被拒绝',
|
|
'LOADED 后运输信息修改被拒绝',
|
|
'回查 LOADED 高风险边界后的入库快照',
|
|
'回查 LOADED 高风险边界后的证据快照',
|
|
'回查 LOADED 高风险边界后的计划快照',
|
|
'LOADED 后拒绝与幂等请求保持状态、版本、证据、计划和审计零漂移',
|
|
])
|
|
assert.match(finalize, /evidenceSignature\(afterEvidence\) === immutableEvidenceSignature/)
|
|
assert.match(finalize, /afterHistorySignature === historySignature/)
|
|
assert.match(finalize, /afterPlanSignature === immutablePlanSignature/)
|
|
})
|
|
|
|
test('finalize publishes a current-marker producer correlation for cross-region verification', () => {
|
|
const finalize = source('./phase3-loading-finalize.mjs')
|
|
assert.match(finalize, /const producer = \{/)
|
|
assert.match(finalize, /createdResources\.phase3LoadingCompletedProducer = producer/)
|
|
assert.match(finalize, /marker: context\.context\?\.phase4ProducerMatrixMarker \|\| context\.marker/)
|
|
assert.match(finalize, /eventType: 'FIRST_LEG_LOADING_PLAN_COMPLETED'/)
|
|
assert.match(finalize, /eventKey: `FIRSTLEG_LOADING_COMPLETED:\$\{loadingPlanId\}:\$\{tenantId\}:2`/)
|
|
assert.match(finalize, /sourceStatus: 'LOADED'/)
|
|
assert.match(finalize, /PRODUCED_IN_COMPLETE_TRANSACTION_PENDING_BRIDGE_CONSUMER_HTTP/)
|
|
})
|