550 lines
30 KiB
JavaScript
550 lines
30 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 { 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 starts through a real PDA batch before preload confirmation', () => {
|
|
const value = source('./phase3-loading.mjs')
|
|
assertOrder(value, [
|
|
'原子关联入库预装与柜次装柜计划',
|
|
'无真实柜号拒绝推送柜次计划',
|
|
'无真实柜号推送失败后不生成 PDA 任务',
|
|
'绑定真实柜号后开放三期 PDA 推送门禁',
|
|
'订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务',
|
|
'三期订舱携带已冻结运输快照并放舱',
|
|
'三期订舱 RELEASED 后保持模拟柜关联身份',
|
|
'真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务',
|
|
"pushedLoadingPlan?.status === 'EDITING'",
|
|
"claimedPlan?.status === 'LOADING'",
|
|
"const canonicalPick = await request",
|
|
"pickedPlan?.status === 'COMPLETED'",
|
|
"const confirming = await request",
|
|
])
|
|
assert.match(value, /confirmed\?\.mainStatus === 'LOADING' && confirmed\?\.preloadPlan\?\.status === 'CONFIRMING'/)
|
|
assert.match(value, /confirmed\?\.preloadPlan\?\.status === 'CONFIRMED'/)
|
|
assert.doesNotMatch(value, /关联柜次计划进入 LOADING/)
|
|
assert.match(value, /confirmed\?\.mainStatus === 'LOADING'/)
|
|
assert.match(value, /confirmed\?\.preloadPlan\?\.status === 'CONFIRMING'/)
|
|
assert.match(value, /Number\(confirmed\?\.preloadPlan\?\.version\) === Number\(confirmingData\.preloadPlan\.version\)/)
|
|
assert.match(value, /const missingContainerPushRejected = failure\(missingContainerPushResponse\)[\s\S]*if \(!missingContainerPushRejected\) return/)
|
|
assert.match(value, /const missingContainerTaskAbsent =[\s\S]*if \(!missingContainerTaskAbsent\) return/)
|
|
assert.match(value, /version: transportVersion/)
|
|
assert.match(value, /Number\.isInteger\(transportVersion\)/)
|
|
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/)
|
|
assertOrder(value, [
|
|
'CONFIRMED 订舱回显完整且冻结五项运输快照',
|
|
'if (!frozenTransportMatches) return',
|
|
'三期订舱携带已冻结运输快照并放舱',
|
|
])
|
|
assert.match(value, /\.\.\.frozenTransport, reason: context\.marker/)
|
|
assert.doesNotMatch(value, /new Date\(Date\.now\(\)/)
|
|
assert.match(value, /bookingReleased\?\.oceanBooking\?\.status === 'RELEASED'/)
|
|
})
|
|
|
|
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 && directPdaCompletion && 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 confirmed real booking', () => {
|
|
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, /channelCode, warehouseId, warehouseName/)
|
|
assert.match(value, /publishPlatformLogisticsChannels\(context, warehouseId, MODULE\)/)
|
|
assert.match(value, /warehouseId, warehouseName/)
|
|
const draft = value.slice(value.indexOf('const bookingDraft = {'), value.indexOf('context.assert(MODULE, \'订舱渠道与仓库快照完整\''))
|
|
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, /containerAllocations\.length === 0/)
|
|
assert.match(value, /version: Number\(linkedContainer\.version\), reason: context\.marker/)
|
|
assert.doesNotMatch(value, /version: Number\(containerDetail\?\.version \?\? 0\), reason: context\.marker/)
|
|
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, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜首次关联真实订舱', emptyLink,\s+responseEvidence\(emptyLink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyUnlink = await request/)
|
|
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜允许解除关联', emptyUnlink,\s+responseEvidence\(emptyUnlink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyRelink = await request/)
|
|
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜解绑后允许重新关联', emptyRelink,\s+responseEvidence\(emptyRelink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyFinalUnlink = await request/)
|
|
assert.match(value, /const emptyReleased = context\.apiSuccess\(MODULE, '空模拟柜最终解绑释放真实订舱容量', emptyFinalUnlink,\s+responseEvidence\(emptyFinalUnlink\), OPTIONS\)\s+if \(!emptyReleased\) return\s+const beforeLinkItems/)
|
|
assertOrder(value, [
|
|
"const linkedContainer = context.apiSuccess(MODULE, '首次关联真实已确认订舱柜', linkResponse,",
|
|
'if (!linkedContainer) return',
|
|
"const afterLinkPlanResponse = await context.request('GET'",
|
|
"const duplicateSimulationResponse = await request(context, 'POST', CONTAINERS",
|
|
])
|
|
assert.equal(value.match(/responseEvidence\((?:emptyLink|emptyUnlink|emptyRelink|emptyFinalUnlink|linkResponse)\)/g)?.length, 5)
|
|
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.ok(value.includes("'PATCH', `${CONTAINERS}/${containerId}/transport"))
|
|
assert.match(value, /在放舱前绑定本轮真实箱号/)
|
|
assert.match(value, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/)
|
|
assert.match(value, /无真实柜号拒绝推送预装计划/)
|
|
assert.match(value, /柜号与 RELEASED 订舱就绪后推送预装计划/)
|
|
assert.match(value, /const confirmedTaskAbsent =[\s\S]*if \(!confirmedTaskAbsent\) return/)
|
|
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, [
|
|
'新增内部模拟柜',
|
|
'新增真实异渠道模拟柜',
|
|
'两个真实有效渠道之间拒绝跨渠道装柜',
|
|
'人工分配本轮候选货物',
|
|
'首次关联真实已确认订舱柜',
|
|
'无真实柜号拒绝推送预装计划',
|
|
'无真实柜号推送失败后不生成 PDA 任务',
|
|
'在放舱前绑定本轮真实箱号',
|
|
'订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务',
|
|
'订舱携带已冻结运输快照放舱并满足 PDA 门禁',
|
|
'柜号与 RELEASED 订舱就绪后推送预装计划',
|
|
'换柜号前读取 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, /shippingCompanyId: null, shippingCompanyName: null/)
|
|
assert.match(value, /bookingAgentId: null, bookingAgentName: null/)
|
|
assert.doesNotMatch(value, /shippingCompanyName: `验收船公司-/)
|
|
assert.doesNotMatch(value, /bookingAgentName: `验收代理-/)
|
|
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
|
|
assert.match(value, /if \(!frozenTransportMatches\) return/)
|
|
assert.match(value, /expectedVersion: Number\(confirmedBooking\?\.oceanBooking\?\.version\),\s*\.\.\.frozenTransport, reason: context\.marker/s)
|
|
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, /Number\.isInteger\(smartPreloadVersion\) && smartPreloadVersion >= 0/)
|
|
assert.match(loading, /Number\(link\?\.preloadVersion\) > bindVersions\.expectedPreloadVersion/)
|
|
assert.match(loading, /Number\(link\?\.loadingPlanVersion\) > bindVersions\.expectedLoadingPlanVersion/)
|
|
assert.match(loading, /Number\(confirmingData\?\.preloadPlan\?\.version\) > Number\(link\.preloadVersion\)/)
|
|
assert.doesNotMatch(loading, /Number\((?:smartData|link|confirmingData|confirmed)[^\n]*version\) === [0-9]/)
|
|
})
|
|
|
|
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')
|
|
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 for all items and completion audit', () => {
|
|
const loading = source('./phase3-loading.mjs')
|
|
assert.match(loading, /taskItemsSignature\(afterStaleTask\) === taskItemsSignature\(beforeMutationTask\)/)
|
|
assertOrder(loading, [
|
|
'旧 token 冲突对任务、明细、已拣数量和操作历史零写入',
|
|
'库存变化后刷新 PDA 实时库存快照',
|
|
'原子提交柜次 PDA 整批拣货',
|
|
'PDA 原子批次同参同键重放保持待人工确认',
|
|
'人工确认柜次 PDA 装柜完成',
|
|
'回查原子 Batch 完成审计',
|
|
'人工完成后保留 CLAIM/BATCH/COMPLETE_LOADING 审计',
|
|
])
|
|
})
|
|
|
|
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 provisions released booking and completed truck for the selected loading container', () => {
|
|
const value = source('./phase3-customs.mjs')
|
|
assertOrder(value, [
|
|
'selectCustomsFlow(context.createdResources)',
|
|
'if (!await ensureServiceCenterFlow(context, container)) return',
|
|
'const slicesPath =',
|
|
])
|
|
assert.match(value, /oceanBookings\.run\(context\)/)
|
|
assert.match(value, /serviceCenter\.run\(context\)/)
|
|
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 producers prefer the phase3Loading container fixture', () => {
|
|
const ocean = source('./ocean-bookings.mjs')
|
|
const customs = source('./phase3-customs.mjs')
|
|
const serviceCenter = source('./service-center-flow.mjs')
|
|
assert.match(ocean, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/)
|
|
assert.match(customs, /createdResources\?\.phase3Loading,\s*\n\s*createdResources\?\.phase3ContainerLoading/)
|
|
assert.match(serviceCenter, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/)
|
|
assert.match(ocean, /request\(context, 'POST', `\$\{path\}\/release`/)
|
|
assert.match(ocean, /flowOceanBookingId = id\(containerFlow\?\.oceanBookingId\)/)
|
|
assert.match(ocean, /ocean-flow-release/)
|
|
assert.match(ocean, /existing\?\.marker === context\.marker && existing\?\.finalStatus === 'RELEASED'/)
|
|
assert.match(ocean, /const reconciled = await releaseLoadingBooking\(context, containerFlow, suffix\)/)
|
|
assert.match(ocean, /containerBookingId: id\(containerFlow\.containerBookingId\), loadingPlanId: id\(containerFlow\.loadingPlanId\)/)
|
|
assert.match(source('./phase3-loading.mjs'), /oceanBookingId: bookingId, loadingPlanId, containerBookingId/)
|
|
assert.doesNotMatch(ocean, /`\$\{context\.marker\}-\$\{endpoint\}`/)
|
|
assert.match(ocean, /`\$\{context\.marker\}-ocean-\$\{endpoint\}`/)
|
|
assert.match(serviceCenter, /if \(!allocationMatches\) return/)
|
|
assert.match(serviceCenter, /const customerName = String\(oceanDetail\?\.oceanBooking\?\.customerName \|\| ''\)\.trim\(\)/)
|
|
assert.match(serviceCenter, /customerId, customerName, carrierId: null/)
|
|
assert.match(serviceCenter, /carrierId: null, carrierName: null/)
|
|
assert.doesNotMatch(serviceCenter, /链路承运商-/)
|
|
assert.doesNotMatch(serviceCenter, /customerName: `服务中心链路客户-/)
|
|
const schedules = source('./phase3-port-schedules.mjs')
|
|
assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containerAllocations \|\| \[\]\)/)
|
|
assert.match(schedules, /ocean\.allocatedContainerBookingIds\?\.includes\(containerBookingId\)/)
|
|
assert.match(schedules, /if \(containerBookingId && !knownAllocation\)/)
|
|
})
|
|
|
|
test('default acceptance order verifies loading evidence before finalizing LOADED', () => {
|
|
const entry = source('../admin-test-env-acceptance.mjs')
|
|
assertOrder(entry, [
|
|
"'phase3-loading'",
|
|
"'phase3-loading-model-bridge'",
|
|
"'phase3-pda-convergence'",
|
|
"'phase3-evidence-gate'",
|
|
"'phase3-customs'",
|
|
"'phase3-loading-finalize'",
|
|
])
|
|
})
|
|
|
|
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*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/)
|
|
})
|