641 lines
36 KiB
JavaScript
641 lines
36 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 { 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('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('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)
|
|
assert.match(value, /const pushRequest = Object\.freeze\([\s\S]*body: Object\.freeze\(\{ expectedVersion:/)
|
|
assert.match(value, /push exact replay[\s\S]*零漂移/)
|
|
assert.match(value, /同 key 异参[\s\S]*新 key 重复 push/)
|
|
assert.match(value, /非法 push 后回查柜次计划[\s\S]*非法 push 后回查预装历史[\s\S]*非法 push 后回查 bridge/)
|
|
assert.match(value, /两类非法 push 后计划版本历史关联(?:和)?任务仍?零漂移/)
|
|
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, /expectedVersion: Number\(postPushPlan\.version\)/)
|
|
assert.match(value, /changed=\$\{(?:pushReplayDiff|replayDiff|rejectedDiff)\.join/)
|
|
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('push orchestration is transactional and a bridge failure cannot reach PDA creation', () => {
|
|
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 push = service.slice(service.indexOf('public Plan pushPreload'), service.indexOf('private void requirePushReplay'))
|
|
assert.match(service, /@Transactional[\s\S]*public Plan pushPreload/)
|
|
assert.match(push, /repository\.push[\s\S]*history\([\s\S]*loadingModelBridge\.bindAllocatedBeforeLoading/)
|
|
assert.doesNotMatch(push, /loadingPickService\.beginIfReady/)
|
|
assert.match(serviceTest, /bindingFailureStopsBeforePdaAndPropagatesForTransactionRollback/)
|
|
assert.match(serviceTest, /verify\(loadingPickService, never\(\)\)\.beginIfReady/)
|
|
})
|
|
|
|
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 the only safe mutation gap', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assertOrder(loading, [
|
|
'读取 PDA 提交前实时库存快照',
|
|
'未发生库存变化时重复读取实时库存快照',
|
|
'客户身份无权读取平台 PDA 实时库存快照',
|
|
'const overflowResource =',
|
|
'通过公开 A0 换绑接口制造真实库存变化',
|
|
'真实库存变化后旧 snapshot token 提交返回冲突',
|
|
'旧 token 冲突对任务、明细、已拣数量和操作历史零写入',
|
|
'库存变化后刷新 PDA 实时库存快照',
|
|
'const canonicalPick = await request',
|
|
'const canonicalBatchReplay = await request',
|
|
])
|
|
assert.match(loading, /inventorySnapshotToken: refreshedInventory\.snapshotToken/)
|
|
assert.match(loading, /warehouseId, loadingPlanId, containerBookingId,/)
|
|
assert.match(loading, /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, /inventoryMutationVerified, inventoryMutationCleanup/)
|
|
assert.match(loading, /当前唯一平台代理库存变更入口只允许 A0 临时占用换绑/)
|
|
|
|
const convergence = source('./phase3-pda-convergence.mjs')
|
|
assert.doesNotMatch(convergence, /未公开实时库存查询或库存版本快照端点/)
|
|
assert.match(convergence, /fixture\?\.inventorySnapshotVerified !== true/)
|
|
assert.match(convergence, /fixture\?\.inventoryMutationVerified !== 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, 'POST', `${SMART}/plans`",
|
|
"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('container loading enforces channel isolation and 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, /category=BOOKING&status=ACTIVE/)
|
|
assert.match(value, /shippingCompanyId: id\(bookingAgent\.id\), shippingCompanyName: bookingAgent\.name/)
|
|
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, /expectedVersion: Number\(bookingCreatedDraft\?\.oceanBooking\?\.version\), reason: context\.marker/)
|
|
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 operation baselines and exact push replay', () => {
|
|
const container = source('./phase3-container-loading.mjs')
|
|
assert.match(container, /const pushRequest = Object\.freeze\(\{[\s\S]*path:[\s\S]*body: Object\.freeze[\s\S]*headers: Object\.freeze/)
|
|
assert.equal((container.match(/request\(context, 'POST', pushRequest\.path, pushRequest\.body, pushRequest\.headers\)/g) || []).length, 2)
|
|
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\(\{[\s\S]*body: Object\.freeze[\s\S]*headers: Object\.freeze/)
|
|
assert.equal((loading.match(/request\(context, 'POST', pushRequest\.path, pushRequest\.body, pushRequest\.headers\)/g) || []).length, 2)
|
|
assert.match(loading, /push exact replay 对计划版本历史关联和任务零漂移/)
|
|
assert.match(loading, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
|
|
})
|
|
|
|
test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => {
|
|
const container = source('./phase3-container-loading.mjs')
|
|
const convergence = source('./phase3-pda-convergence.mjs')
|
|
assertOrder(container, [
|
|
'最后一箱拣货后 PDA 任务等待人工确认装柜完成',
|
|
'上传柜次 ${evidenceType} 真实证据文件',
|
|
'绑定柜次 ${evidenceType} 装柜证据',
|
|
'PDA 完成前回查五类有效装柜证据与锁码',
|
|
'PDA 人工确认装柜完成',
|
|
])
|
|
assert.match(container, /const evidenceTypes = \['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'\]/)
|
|
assert.match(container, /businessType === `LOADING_\$\{evidenceType\}`/)
|
|
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]*files\/loading-evidence/)
|
|
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'/)
|
|
})
|
|
|
|
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 >= 7, true)
|
|
})
|
|
|
|
test('phase3 loading proves stale inventory zero-write and defers final completion audit', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assert.match(loading, /taskItemsSignature\(afterStaleTask\) === taskItemsSignature\(beforeMutationTask\)/)
|
|
assertOrder(loading, [
|
|
'旧 token 冲突对任务、明细、已拣数量和操作历史零写入',
|
|
'库存变化后刷新 PDA 实时库存快照',
|
|
'原子提交柜次 PDA 整批拣货',
|
|
'PDA 原子批次同参同键重放保持待人工确认',
|
|
'回查下游门禁前原子 Batch 审计',
|
|
'最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING',
|
|
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
|
|
])
|
|
assert.doesNotMatch(loading, /complete-before-downstream/)
|
|
})
|
|
|
|
test('phase3 loading fixture forces A0 through reversible public location status APIs', () => {
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
const measurement = source('./inbound-measurement.mjs')
|
|
const orders = source('./orders.mjs')
|
|
assertOrder(entry, [
|
|
'disableAvailableLocationsForA0(warehouseId)',
|
|
'this.context.phase3LoadingRequireSharedOverflow = true',
|
|
"executeModule('inbound-measurement-phase3-loading-fixture'",
|
|
'restoreAvailableLocationsAfterA0(warehouseId, disabledLocations)',
|
|
])
|
|
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(measurement, /phase3LoadingRequireSharedOverflow !== true/)
|
|
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, /if \(!allocationMatches\) return/)
|
|
assert.match(serviceCenter, /const customerName = String\(oceanDetail\?\.oceanBooking\?\.customerName \|\| ''\)\.trim\(\)/)
|
|
assert.match(serviceCenter, /category=TRUCKING&status=ACTIVE/)
|
|
assert.match(serviceCenter, /customerId, customerName: null, carrierId: id\(carrier\.id\), carrierName: carrier\.name/)
|
|
assert.doesNotMatch(serviceCenter, /链路承运商-/)
|
|
assert.doesNotMatch(serviceCenter, /customerName: `服务中心链路客户-/)
|
|
const schedules = source('./phase3-port-schedules.mjs')
|
|
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, ['orders', 'phase3-loading', 'phase3-container-overview', 'finance'])
|
|
assert.deepEqual(parseArguments(['--modules', 'phase3-container-overview,phase3-loading']).modules,
|
|
['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, /modules: normalizeModuleOrder\(/)
|
|
})
|
|
|
|
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\(\)/)
|
|
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, /measurement\?\.processStatus === 'WAIT_SPLIT'/)
|
|
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, [
|
|
'装柜证据绑定同键同参幂等重放',
|
|
'装柜证据绑定同键异参返回冲突',
|
|
'装柜证据作废旧版本返回冲突',
|
|
'临时作废唯一 PHOTO_STEP_4 证据',
|
|
'证据作废同键同参重放保持同一 VOID 结果',
|
|
'证据作废同键异参返回冲突',
|
|
'已作废证据使用新请求重复作废返回冲突',
|
|
'作废唯一证据类型后 LOADED 门禁拒绝',
|
|
'补传恢复 PHOTO_STEP_4 有效证据',
|
|
'客户身份无权访问平台装柜证据',
|
|
'错误客户作用域不可读取本轮装柜证据',
|
|
'错误入库单不可读取本轮装柜证据',
|
|
])
|
|
assert.match(loading, /missingTypeVoidedEvidenceId/)
|
|
assert.match(loading, /replacementEvidenceFileId/)
|
|
})
|
|
|
|
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/)
|
|
})
|