test(acceptance): align automatic preload workflows

This commit is contained in:
2026-09-02 13:03:03 +08:00
parent 275d4df35d
commit f7f8ff53c0
7 changed files with 65 additions and 68 deletions
+25 -9
View File
@@ -335,7 +335,8 @@ export function parseArguments(argv) {
channelCode: values.get('channel-code') || '', channelCode: values.get('channel-code') || '',
orderProfileMode: values.get('order-profile-mode') || 'fixed', orderProfileMode: values.get('order-profile-mode') || 'fixed',
waitSplitCount: Number(values.get('wait-split-count') || process.env.ADMIN_WAIT_SPLIT_COUNT waitSplitCount: Number(values.get('wait-split-count') || process.env.ADMIN_WAIT_SPLIT_COUNT
|| (modules.includes('manual-order-splitting-fixture') ? 6 : 1)), || (modules.includes('coload-groups') ? 8
: modules.includes('manual-order-splitting-fixture') ? 6 : 1)),
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0), waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
waitSplitSequence: Number(values.get('wait-split-sequence') || 1), waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1), waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
@@ -826,7 +827,7 @@ export class AcceptanceRun {
this.context.phase3LoadingIncrementalPackageCount = variant.packageCountPerSku this.context.phase3LoadingIncrementalPackageCount = variant.packageCountPerSku
const profile = this.options.orderProfileMode === 'randomized' const profile = this.options.orderProfileMode === 'randomized'
? this.randomizedOrderProfile(sequence) ? this.randomizedOrderProfile(sequence)
: { destinationType: 'FBA', fulfillmentType: 'STANDARD', pickupMethod: 'PICKUP', : { destinationType: 'FBA', fulfillmentType: 'STANDARD', pickupMethod: 'SELF_EXPRESS',
taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false } taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false }
this.context.orderProfile = profile this.context.orderProfile = profile
this.context.orderDestinationType = profile.destinationType this.context.orderDestinationType = profile.destinationType
@@ -1058,15 +1059,30 @@ export class AcceptanceRun {
&& /[A-Za-z]/.test(createData.temporaryPassword) && /\d/.test(createData.temporaryPassword), && /[A-Za-z]/.test(createData.temporaryPassword) && /\d/.test(createData.temporaryPassword),
`generated=${typeof createData.temporaryPassword === 'string'}, length=${createData.temporaryPassword?.length || 0}`, `generated=${typeof createData.temporaryPassword === 'string'}, length=${createData.temporaryPassword?.length || 0}`,
ownerProvisioningOptions) ownerProvisioningOptions)
this.assert(module, '管理员用户名默认使用联系电话', createResponse, const administratorUsername = String(createData.administratorUsername || '')
createData.administratorUsername === phone, this.assert(module, '管理员用户名作为独立登录标识返回', createResponse,
`matches=${createData.administratorUsername === phone}`, administratorUsername.length > 0,
`present=${administratorUsername.length > 0}`,
ownerProvisioningOptions) ownerProvisioningOptions)
this.assert(module, '待完善客户的审核字段保持为空', createResponse, this.assert(module, '待完善客户的审核字段保持为空', createResponse,
customer.unifiedSocialCreditCode == null && customer.approvedAt == null, customer.unifiedSocialCreditCode == null && customer.approvedAt == null,
`creditCode=${customer.unifiedSocialCreditCode ?? 'omitted'}, approvedAt=${customer.approvedAt ?? 'omitted'}`) `creditCode=${customer.unifiedSocialCreditCode ?? 'omitted'}, approvedAt=${customer.approvedAt ?? 'omitted'}`)
await this.runCustomerDimensionRules(tenantId) const dimensionInitializationPath = `/api/platform/v1/iam/tenants/${tenantId}/dimension-initialization`
let dimensionInitializationResponse = null
let dimensionInitialization = null
for (let attempt = 0; attempt < 30; attempt += 1) {
dimensionInitializationResponse = await this.request('GET', dimensionInitializationPath)
dimensionInitialization = dimensionInitializationResponse.http === 200
&& String(dimensionInitializationResponse.body?.code) === '200'
? dimensionInitializationResponse.body?.data : null
if (dimensionInitialization?.status === 'READY') break
if (attempt < 29) await new Promise((resolve) => setTimeout(resolve, 1_000))
}
this.assert(module, '客户默认尺寸异步初始化完成', dimensionInitializationResponse,
dimensionInitialization?.status === 'READY',
`status=${dimensionInitialization?.status || 'missing'}`, ownerProvisioningOptions)
if (dimensionInitialization?.status === 'READY') await this.runCustomerDimensionRules(tenantId)
const listResponse = await this.request('GET', `/api/platform/v1/iam/tenants?page=1&size=20&keyword=${encodeURIComponent(payload.tenantName)}`) const listResponse = await this.request('GET', `/api/platform/v1/iam/tenants?page=1&size=20&keyword=${encodeURIComponent(payload.tenantName)}`)
const page = this.apiSuccess(module, '查询客户列表', listResponse); const listed = page?.records?.find((item) => String(item.id) === tenantId) const page = this.apiSuccess(module, '查询客户列表', listResponse); const listed = page?.records?.find((item) => String(item.id) === tenantId)
@@ -1136,9 +1152,9 @@ export class AcceptanceRun {
} }
const memberResponse = await this.request('GET', `${detailPath}/members`); const members = this.apiSuccess(module, '业务概况查询成员', memberResponse) const memberResponse = await this.request('GET', `${detailPath}/members`); const members = this.apiSuccess(module, '业务概况查询成员', memberResponse)
const owner = members?.find?.((item) => item.memberType === 'OWNER') const owner = members?.find?.((item) => item.memberType === 'OWNER')
this.assert(module, '联系电话账号是企业管理员', memberResponse, this.assert(module, '独立用户名账号是企业管理员且联系电话不承担用户名约束', memberResponse,
owner?.username === phone, owner?.username === administratorUsername,
`ownerFound=${Boolean(owner)}, matches=${owner?.username === phone}`, `ownerFound=${Boolean(owner)}, matches=${owner?.username === administratorUsername}`,
ownerProvisioningOptions) ownerProvisioningOptions)
const countResponse = await this.request('GET', `/api/platform/v1/orders/first-leg/tenant-counts?tenantIds=${tenantId}`); const counts = this.apiSuccess(module, '业务概况查询运单数量', countResponse); const count = counts?.find?.((item) => String(item.tenantId) === tenantId) const countResponse = await this.request('GET', `/api/platform/v1/orders/first-leg/tenant-counts?tenantIds=${tenantId}`); const counts = this.apiSuccess(module, '业务概况查询运单数量', countResponse); const count = counts?.find?.((item) => String(item.tenantId) === tenantId)
this.assert(module, '业务概况返回当前客户运单数', countResponse, count && Number.isFinite(Number(count.orderCount)), `orderCount=${count?.orderCount ?? 'missing'}`) this.assert(module, '业务概况返回当前客户运单数', countResponse, count && Number.isFinite(Number(count.orderCount)), `orderCount=${count?.orderCount ?? 'missing'}`)
@@ -9,7 +9,7 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
'/api/platform/v1/first-leg/coload-groups/by-inbound-code/', '/api/platform/v1/first-leg/coload-groups/by-inbound-code/',
'/group-assignments', '/group-assignments',
'/inbound-assignments', '/inbound-assignments',
'/push-preload', '/plans/by-booking/',
'创建同键同载荷精确重放', '创建同键同载荷精确重放',
'创建同键异载荷返回并发冲突', '创建同键异载荷返回并发冲突',
'不同目的仓港仍可创建同柜不可拆组', '不同目的仓港仍可创建同柜不可拆组',
@@ -31,7 +31,7 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
'短拣完成拒绝后任务、父子树和组锁零漂移', '短拣完成拒绝后任务、父子树和组锁零漂移',
'同柜组足量完成命令可靠受理', '同柜组足量完成命令可靠受理',
'足量完成整组已消费、全部入库完成且不生成余量子单', '足量完成整组已消费、全部入库完成且不生成余量子单',
'未成组普通单推送预装不回归', '未成组普通单自动预装不回归',
'未成组普通单仍按既有规则生成唯一余量子单', '未成组普通单仍按既有规则生成唯一余量子单',
'拆分余量子单仍可与同仓同渠道订单创建同柜组', '拆分余量子单仍可与同仓同渠道订单创建同柜组',
'拆分身份不影响当前WAIT_SPLIT片段完整纳组', '拆分身份不影响当前WAIT_SPLIT片段完整纳组',
@@ -42,4 +42,5 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
assert.doesNotMatch(source, /跨仓库渠道或目的仓港创建整组被拒绝/) assert.doesNotMatch(source, /跨仓库渠道或目的仓港创建整组被拒绝/)
assert.doesNotMatch(source, /已有父子拆分订单创建同柜组被拒绝/) assert.doesNotMatch(source, /已有父子拆分订单创建同柜组被拒绝/)
assert.doesNotMatch(source, /push-preload/)
}) })
@@ -99,10 +99,8 @@ async function createAssignedPushedPlan(context, fixture, groupId, suffix) {
evidence(simulationResponse), OPTIONS) evidence(simulationResponse), OPTIONS)
const bookingId = id(simulation?.id) const bookingId = id(simulation?.id)
if (!bookingId) return null if (!bookingId) return null
const planResponse = await context.request('POST', `${SMART}/plans`, { const planResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(bookingId)}`)
bookingId, remark: `${context.marker}-${suffix}`, let plan = context.apiSuccess(MODULE, `${suffix} 读取模拟柜自动创建的组装柜计划`, planResponse,
})
let plan = context.apiSuccess(MODULE, `${suffix} 创建组装柜计划`, planResponse,
evidence(planResponse), OPTIONS) evidence(planResponse), OPTIONS)
const planId = id(plan?.id) const planId = id(plan?.id)
if (!planId) return null if (!planId) return null
@@ -112,10 +110,8 @@ async function createAssignedPushedPlan(context, fixture, groupId, suffix) {
}) })
plan = context.apiSuccess(MODULE, `${suffix} 整组原子分配`, assignResponse, plan = context.apiSuccess(MODULE, `${suffix} 整组原子分配`, assignResponse,
evidence(assignResponse, `groupId=${groupId}`), OPTIONS) evidence(assignResponse, `groupId=${groupId}`), OPTIONS)
const pushResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(planId)}/push-preload`, { const pushResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}`)
expectedVersion: Number(plan?.version), const pushed = context.apiSuccess(MODULE, `${suffix} 整组分配后自动建立预装关联`, pushResponse,
}, undefined, { 'Idempotency-Key': `${context.marker}-${suffix}-push`.slice(0, 128) })
const pushed = context.apiSuccess(MODULE, `${suffix} 整组完整推送预装`, pushResponse,
evidence(pushResponse), OPTIONS) evidence(pushResponse), OPTIONS)
return pushed ? { containerBookingId: bookingId, planId, plan: pushed, simulation } : null return pushed ? { containerBookingId: bookingId, planId, plan: pushed, simulation } : null
} }
@@ -129,10 +125,8 @@ async function createNormalPushedPlan(context, fixture, suffix) {
evidence(simulationResponse), OPTIONS) evidence(simulationResponse), OPTIONS)
const containerBookingId = id(simulation?.id) const containerBookingId = id(simulation?.id)
if (!containerBookingId) return null if (!containerBookingId) return null
const planResponse = await context.request('POST', `${SMART}/plans`, { const planResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(containerBookingId)}`)
bookingId: containerBookingId, remark: `${context.marker}-${suffix}`, let plan = context.apiSuccess(MODULE, `${suffix} 读取模拟柜自动创建的普通单装柜计划`, planResponse,
})
let plan = context.apiSuccess(MODULE, `${suffix} 创建普通单装柜计划`, planResponse,
evidence(planResponse), OPTIONS) evidence(planResponse), OPTIONS)
const planId = id(plan?.id) const planId = id(plan?.id)
if (!planId) return null if (!planId) return null
@@ -149,10 +143,8 @@ async function createNormalPushedPlan(context, fixture, suffix) {
context.assert(MODULE, `${suffix} 普通单维持既有部分分配语义`, assignResponse, context.assert(MODULE, `${suffix} 普通单维持既有部分分配语义`, assignResponse,
assignedTotal === assignedCount && assignedCount < Number(fixture.packages), assignedTotal === assignedCount && assignedCount < Number(fixture.packages),
`assigned=${assignedTotal}, source=${fixture.packages}`, OPTIONS) `assigned=${assignedTotal}, source=${fixture.packages}`, OPTIONS)
const pushResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(planId)}/push-preload`, { const pushResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}`)
expectedVersion: Number(plan?.version), const pushed = context.apiSuccess(MODULE, `${suffix} 未成组普通单自动预装不回归`, pushResponse,
}, undefined, { 'Idempotency-Key': `${context.marker}-${suffix}-push`.slice(0, 128) })
const pushed = context.apiSuccess(MODULE, `${suffix} 未成组普通单推送预装不回归`, pushResponse,
evidence(pushResponse), OPTIONS) evidence(pushResponse), OPTIONS)
return pushed ? { containerBookingId, planId, plan: pushed, simulation, assignedCount } : null return pushed ? { containerBookingId, planId, plan: pushed, simulation, assignedCount } : null
} }
@@ -344,7 +336,7 @@ async function createRouteVariantFixture(context, base, variant) {
context.marker = `${originalMarker}-${variant === 'DESTINATION' ? 'DEST' : 'ROUTE'}-${attempt}` context.marker = `${originalMarker}-${variant === 'DESTINATION' ? 'DEST' : 'ROUTE'}-${attempt}`
context.context.orderProfile = { context.context.orderProfile = {
destinationType: variant === 'DESTINATION' && attempt >= 3 ? 'OVERSEAS' : 'FBA', fulfillmentType: 'STANDARD', destinationType: variant === 'DESTINATION' && attempt >= 3 ? 'OVERSEAS' : 'FBA', fulfillmentType: 'STANDARD',
pickupMethod: 'PICKUP', taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false, pickupMethod: 'SELF_EXPRESS', taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false,
} }
context.context.orderDestinationType = context.context.orderProfile.destinationType context.context.orderDestinationType = context.context.orderProfile.destinationType
context.context.orderFulfillmentType = 'STANDARD' context.context.orderFulfillmentType = 'STANDARD'
@@ -510,10 +502,8 @@ export async function run(context) {
evidence(simulationResponse), OPTIONS) evidence(simulationResponse), OPTIONS)
let bookingId = id(simulation?.id) let bookingId = id(simulation?.id)
if (!bookingId) return if (!bookingId) return
const planResponse = await context.request('POST', '/api/platform/v1/first-leg/smart-loading/plans', { const planResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(bookingId)}`)
bookingId, remark: `${context.marker} 同柜不可拆组验收`, let plan = context.apiSuccess(MODULE, '读取容量拒绝专用自动空计划', planResponse,
})
let plan = context.apiSuccess(MODULE, '创建容量拒绝专用空计划', planResponse,
evidence(planResponse, `bookingId=${bookingId}`), OPTIONS) evidence(planResponse, `bookingId=${bookingId}`), OPTIONS)
let planId = id(plan?.id) let planId = id(plan?.id)
if (!planId) return if (!planId) return
@@ -580,10 +570,8 @@ export async function run(context) {
recommendSimulationResponse, evidence(recommendSimulationResponse), OPTIONS) recommendSimulationResponse, evidence(recommendSimulationResponse), OPTIONS)
const recommendBookingId = id(recommendSimulation?.id) const recommendBookingId = id(recommendSimulation?.id)
if (!recommendBookingId) return if (!recommendBookingId) return
const recommendPlanResponse = await context.request('POST', `${SMART}/plans`, { const recommendPlanResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(recommendBookingId)}`)
bookingId: recommendBookingId, remark: `${context.marker} 智能推荐整组全取`, let recommendPlan = context.apiSuccess(MODULE, '读取智能推荐整组成功专用自动计划', recommendPlanResponse,
})
let recommendPlan = context.apiSuccess(MODULE, '创建智能推荐整组成功专用计划', recommendPlanResponse,
evidence(recommendPlanResponse), OPTIONS) evidence(recommendPlanResponse), OPTIONS)
const recommendPlanId = id(recommendPlan?.id) const recommendPlanId = id(recommendPlan?.id)
if (!recommendPlanId) return if (!recommendPlanId) return
@@ -628,10 +616,8 @@ export async function run(context) {
evidence(workingSimulationResponse), OPTIONS) evidence(workingSimulationResponse), OPTIONS)
bookingId = id(workingSimulation?.id) bookingId = id(workingSimulation?.id)
if (!bookingId) return if (!bookingId) return
const workingPlanResponse = await context.request('POST', `${SMART}/plans`, { const workingPlanResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(bookingId)}`)
bookingId, remark: `${context.marker} 同柜不可拆组PDA主链`, plan = context.apiSuccess(MODULE, '读取PDA主链自动创建的同柜装柜计划', workingPlanResponse,
})
plan = context.apiSuccess(MODULE, '创建PDA主链同柜装柜计划', workingPlanResponse,
evidence(workingPlanResponse, `bookingId=${bookingId}`), OPTIONS) evidence(workingPlanResponse, `bookingId=${bookingId}`), OPTIONS)
planId = id(plan?.id) planId = id(plan?.id)
if (!planId) return if (!planId) return
@@ -718,12 +704,8 @@ export async function run(context) {
evidence(manualGroupResponse), OPTIONS) evidence(manualGroupResponse), OPTIONS)
if (!plan) return if (!plan) return
const pushKey = `${context.marker}-coload-push`.slice(0, 128) const pushResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}`)
const pushResponse = await context.request('POST', const pushed = context.apiSuccess(MODULE, '整组完整时自动预装关联可读', pushResponse,
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/push-preload`, {
expectedVersion: Number(plan.version),
}, undefined, { 'Idempotency-Key': pushKey })
const pushed = context.apiSuccess(MODULE, '整组完整时推送预装成功', pushResponse,
evidence(pushResponse), OPTIONS) evidence(pushResponse), OPTIONS)
const pushedTotals = new Map() const pushedTotals = new Map()
for (const item of pushed?.items || []) pushedTotals.set(id(item?.inboundId), for (const item of pushed?.items || []) pushedTotals.set(id(item?.inboundId),
@@ -956,10 +938,9 @@ export async function run(context) {
const uiSimulation = context.apiSuccess(MODULE, '为Admin页面保留可编辑智能装柜柜次', uiSimulationResponse, const uiSimulation = context.apiSuccess(MODULE, '为Admin页面保留可编辑智能装柜柜次', uiSimulationResponse,
evidence(uiSimulationResponse), OPTIONS) evidence(uiSimulationResponse), OPTIONS)
const uiBookingId = id(uiSimulation?.id) const uiBookingId = id(uiSimulation?.id)
const uiPlanResponse = uiBookingId ? await context.request('POST', `${SMART}/plans`, { const uiPlanResponse = uiBookingId
bookingId: uiBookingId, remark: `${context.marker} Admin整组候选人工验收保留`, ? await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(uiBookingId)}`) : null
}) : null const uiPlan = uiPlanResponse ? context.apiSuccess(MODULE, '为Admin页面保留自动创建的未分配空计划', uiPlanResponse,
const uiPlan = uiPlanResponse ? context.apiSuccess(MODULE, '为Admin页面保留未分配空计划', uiPlanResponse,
evidence(uiPlanResponse, `bookingId=${uiBookingId}`), OPTIONS) : null evidence(uiPlanResponse, `bookingId=${uiBookingId}`), OPTIONS) : null
const uiMembers = memberFacts(uiGroup) const uiMembers = memberFacts(uiGroup)
context.assert(MODULE, '页面验收保留组仍为ACTIVE且未锁定', uiGroupResponse, context.assert(MODULE, '页面验收保留组仍为ACTIVE且未锁定', uiGroupResponse,
@@ -7,7 +7,7 @@ const source = readFileSync(new URL('./manual-state-loading-pda.mjs', import.met
test('retains all clickable container, loading-list and PDA workflow states through public APIs', () => { test('retains all clickable container, loading-list and PDA workflow states through public APIs', () => {
for (const status of ['WAIT_LOADING', 'EDITING', 'LOADING', 'COMPLETED', for (const status of ['WAIT_LOADING', 'EDITING', 'LOADING', 'COMPLETED',
'UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW']) assert.match(source, new RegExp(`['"]${status}['"]`)) 'UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW']) assert.match(source, new RegExp(`['"]${status}['"]`))
for (const endpoint of ['/ocean-bookings', '/smart-loading', '/push-preload', '/inventory-snapshot', for (const endpoint of ['/ocean-bookings', '/smart-loading', '/plans/by-booking/', '/inventory-snapshot',
'/batch', '/complete', '/loading-plans', '/loading-lists']) assert.match(source, new RegExp(endpoint)) '/batch', '/complete', '/loading-plans', '/loading-lists']) assert.match(source, new RegExp(endpoint))
assert.match(source, /requiredStatuses/) assert.match(source, /requiredStatuses/)
assert.match(source, /detailPath/) assert.match(source, /detailPath/)
@@ -18,7 +18,7 @@ test('retains all clickable container, loading-list and PDA workflow states thro
assert.match(source, /\.\/inbound-measurement\.mjs/) assert.match(source, /\.\/inbound-measurement\.mjs/)
assert.match(source, /context\.request\('POST', CONTAINERS/) assert.match(source, /context\.request\('POST', CONTAINERS/)
assert.match(source, /summary\?\.processStatus === 'WAIT_SPLIT'/) assert.match(source, /summary\?\.processStatus === 'WAIT_SPLIT'/)
const pushIndex = source.indexOf('`${SMART}/plans/${plan.id}/push-preload`') const pushIndex = source.indexOf('`${SMART}/plans/${plan.id}`')
const transportIndex = source.indexOf('ocean-booking-link') const transportIndex = source.indexOf('ocean-booking-link')
assert.ok(transportIndex >= 0 && pushIndex > transportIndex) assert.ok(transportIndex >= 0 && pushIndex > transportIndex)
const unconfirmedTaskIndex = source.indexOf('`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`', pushIndex) const unconfirmedTaskIndex = source.indexOf('`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`', pushIndex)
@@ -26,6 +26,7 @@ test('retains all clickable container, loading-list and PDA workflow states thro
const canonicalTaskIndex = source.indexOf('`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`', unconfirmedTaskIndex + 1) const canonicalTaskIndex = source.indexOf('`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`', unconfirmedTaskIndex + 1)
assert.ok(unconfirmedTaskIndex > pushIndex && confirmIndex > unconfirmedTaskIndex assert.ok(unconfirmedTaskIndex > pushIndex && confirmIndex > unconfirmedTaskIndex
&& canonicalTaskIndex > confirmIndex) && canonicalTaskIndex > confirmIndex)
assert.doesNotMatch(source, /push-preload/)
assert.match(source, /计划确认前不生成 PDA 任务/) assert.match(source, /计划确认前不生成 PDA 任务/)
assert.match(source, /summary\?\.readiness\?\.canConfirm === true/) assert.match(source, /summary\?\.readiness\?\.canConfirm === true/)
assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion \}\)/) assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion \}\)/)
@@ -193,18 +193,15 @@ async function createPlanTask(context, label) {
: null : null
if (!candidate?.inboundDetailId) throw new Error(`${label} 独立入库单未进入同渠道排柜候选`) if (!candidate?.inboundDetailId) throw new Error(`${label} 独立入库单未进入同渠道排柜候选`)
const planResponse = await context.request('POST', `${SMART}/plans`, const planResponse = await context.request('GET', `${SMART}/plans/by-booking/${containerId}`)
{ bookingId: containerId, remark: `${context.marker}-${label}` }) let plan = context.apiSuccess(MODULE, `${label} 读取模拟柜自动创建的智能排柜计划`, planResponse, '', OPTIONS)
let plan = context.apiSuccess(MODULE, `${label} 创建智能排柜计划`, planResponse, '', OPTIONS)
const assignResponse = await context.request('POST', `${SMART}/plans/${plan.id}/assignments`, { const assignResponse = await context.request('POST', `${SMART}/plans/${plan.id}/assignments`, {
inboundId: source.inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1, inboundId: source.inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(plan.version), operationId: operationId(context, `${label}-assign`), expectedVersion: Number(plan.version), operationId: operationId(context, `${label}-assign`),
}) })
plan = context.apiSuccess(MODULE, `${label} 分配独立候选货物`, assignResponse, '', OPTIONS) plan = context.apiSuccess(MODULE, `${label} 分配独立候选货物`, assignResponse, '', OPTIONS)
const containerNo = iso6346ContainerNumber(`${context.marker}-manual-${label}`) const pushResponse = await context.request('GET', `${SMART}/plans/${plan.id}`)
const pushResponse = await post(context, `${SMART}/plans/${plan.id}/push-preload`, plan = context.apiSuccess(MODULE, `${label} 分配后自动建立预装关联`, pushResponse, '', OPTIONS)
{ expectedVersion: Number(plan.version) }, `${label}-push`)
plan = context.apiSuccess(MODULE, `${label} 补真实柜号后推送预装`, pushResponse, '', OPTIONS)
const planId = id(plan?.id) const planId = id(plan?.id)
const unconfirmedTaskResponse = await context.request('GET', const unconfirmedTaskResponse = await context.request('GET',
`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`) `${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`)
@@ -23,7 +23,7 @@ test('each remainder uses a simulation linked to a released booking requirement'
assert.match(source, /oceanBooking\?\.status === 'RELEASED' && releasedAllocationMatches/) assert.match(source, /oceanBooking\?\.status === 'RELEASED' && releasedAllocationMatches/)
assert.match(source, /id\(item\?\.containerBookingId\) === containerId/) assert.match(source, /id\(item\?\.containerBookingId\) === containerId/)
assert.match(source, /candidates\?bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/) assert.match(source, /candidates\?bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
assert.match(source, /`\$\{SMART\}\/plans`, \{ bookingId: containerId/) assert.match(source, /`\$\{SMART\}\/plans\/by-booking\/\$\{containerId\}`/)
assert.doesNotMatch(source, /candidates\?warehouseId=/) assert.doesNotMatch(source, /candidates\?warehouseId=/)
}) })
@@ -37,7 +37,7 @@ test('release freezes and detail replays the same complete transport snapshot',
}) })
test('PDA opens only after linking a released requirement and uses stable plan identity', () => { test('PDA opens only after linking a released requirement and uses stable plan identity', () => {
const pushIndex = source.indexOf("`${SMART}/plans/${planId}/push-preload`") const pushIndex = source.indexOf("`${SMART}/plans/${planId}`")
const linkIndex = source.indexOf("`${CONTAINERS}/${containerId}/ocean-booking-link`") const linkIndex = source.indexOf("`${CONTAINERS}/${containerId}/ocean-booking-link`")
const releaseIndex = source.indexOf("`/api/platform/v1/ocean-bookings/${oceanBookingId}/release`") const releaseIndex = source.indexOf("`/api/platform/v1/ocean-bookings/${oceanBookingId}/release`")
const confirmIndex = source.indexOf("`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId)}/confirm`") const confirmIndex = source.indexOf("`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId)}/confirm`")
@@ -60,6 +60,7 @@ test('PDA opens only after linking a released requirement and uses stable plan i
assert.match(source, /id\(task\?\.loadingPlanId\) === planId/) assert.match(source, /id\(task\?\.loadingPlanId\) === planId/)
assert.match(source, /id\(task\?\.containerBookingId\) === containerId/) assert.match(source, /id\(task\?\.containerBookingId\) === containerId/)
assert.doesNotMatch(source, /\$\{PDA\}\/tasks\?containerNo=/) assert.doesNotMatch(source, /\$\{PDA\}\/tasks\?containerNo=/)
assert.doesNotMatch(source, /push-preload/)
}) })
test('each remainder settles only through inventory snapshot, atomic batch and explicit complete', () => { test('each remainder settles only through inventory snapshot, atomic batch and explicit complete', () => {
@@ -230,8 +230,8 @@ async function loadOneFragment(context, state) {
`orderId=${sourceOrderId}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS) `orderId=${sourceOrderId}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
if (!candidate?.inboundId || !candidate?.inboundDetailId) return null if (!candidate?.inboundId || !candidate?.inboundDetailId) return null
const createPlan = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker }) const createPlan = await request(context, 'GET', `${SMART}/plans/by-booking/${containerId}`)
const plan = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 创建装柜计划`, createPlan, '', OPTIONS) const plan = context.apiSuccess(MODULE, `读取 ${sourceOrderNo} 模拟柜自动创建的装柜计划`, createPlan, '', OPTIONS)
const planId = id(plan?.id) const planId = id(plan?.id)
if (!planId) return null if (!planId) return null
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(planId) context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(planId)
@@ -289,11 +289,11 @@ async function loadOneFragment(context, state) {
`status=${bookingDetail?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}`, OPTIONS) `status=${bookingDetail?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}`, OPTIONS)
if (bookingDetail?.oceanBooking?.status !== 'RELEASED' || !releasedAllocationMatches) return null if (bookingDetail?.oceanBooking?.status !== 'RELEASED' || !releasedAllocationMatches) return null
const push = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`, const push = await request(context, 'GET', `${SMART}/plans/${planId}`)
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': `${operationBase}-push` }) const pushed = context.apiSuccess(MODULE, `读取 ${sourceOrderNo} 自动关联的预装计划`, push, '', OPTIONS)
const pushed = context.apiSuccess(MODULE, `推送 ${sourceOrderNo} 的预装计划`, push, '', OPTIONS) context.assert(MODULE, `${sourceOrderNo} 补真实柜号后自动预装计划保持 EDITING`, push,
context.assert(MODULE, `${sourceOrderNo} 补真实柜号后计划保持 EDITING`, push, pushed?.status === 'EDITING' && Number(pushed?.version) === Number(assigned.version),
pushed?.status === 'EDITING', `status=${pushed?.status || 'missing'}`, OPTIONS) `status=${pushed?.status || 'missing'}, version=${pushed?.version ?? 'missing'}`, OPTIONS)
if (pushed?.status !== 'EDITING') return null if (pushed?.status !== 'EDITING') return null
const unconfirmedTaskResponse = await context.request('GET', const unconfirmedTaskResponse = await context.request('GET',