Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
15 changed files with 118 additions and 149 deletions
Showing only changes of commit 1982a7dd69 - Show all commits
@@ -101,10 +101,14 @@ async function createConfirmedContainer(context, source, label) {
const draft = {
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
channelCode: summary.channelCode, warehouseId: source.warehouseId,
warehouseName: summary.warehouseName, shippingCompanyId: null, shippingCompanyName: `人工船司-${value}`,
warehouseName: summary.warehouseName, billOfLadingNo: `BL-${value}`.slice(0, 128),
shippingCompanyId: null, shippingCompanyName: `人工船司-${value}`,
bookingAgentId: null, bookingAgentName: `人工代理-${value}`, departurePortCode: 'CNSZX',
vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64),
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: `${context.marker}-${label}`,
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00',
remark: `${context.marker}-${label}`,
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${value}` },
{ partyType: 'CONSIGNEE', partyName: `收货方-${value}` },
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${value}` }],
@@ -122,12 +126,10 @@ async function createConfirmedContainer(context, source, label) {
{ expectedVersion: 0, data: draft }, `${label}-booking-create`)
const created = context.apiSuccess(MODULE, `${label} 创建订舱`, createdResponse, '', OPTIONS)
const bookingId = id(created?.oceanBooking?.id)
const submittedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: Number(created?.oceanBooking?.version), reason: context.marker }, `${label}-booking-submit`)
const submitted = context.apiSuccess(MODULE, `${label} 提交订舱`, submittedResponse, '', OPTIONS)
const confirmedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/confirm`,
{ expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker }, `${label}-booking-confirm`)
const confirmed = context.apiSuccess(MODULE, `${label} 确认订舱`, confirmedResponse, '', OPTIONS)
const confirmed = created
context.assert(MODULE, `${label} 一次登记直接返回 CONFIRMED`, createdResponse,
confirmed?.oceanBooking?.status === 'CONFIRMED',
`status=${confirmed?.oceanBooking?.status || 'missing'}`, OPTIONS)
const requirement = confirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
if (!requirement?.id) throw new Error(`${label} 确认订舱未返回箱型需求`)
const linkedResponse = await context.request('POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
@@ -4,27 +4,24 @@ import test from 'node:test'
const source = readFileSync(new URL('./manual-state-ocean-trucking.mjs', import.meta.url), 'utf8')
test('retains only public ocean booking list states while hidden states stay detail-only', () => {
test('retains only operational ocean booking states from direct CONFIRMED registration', () => {
assert.match(source, /const OCEAN_BASE = ['"]\/api\/platform\/v1\/ocean-bookings['"]/)
assert.doesNotMatch(source, /\/api\/platform\/v1\/first-leg\/ocean-bookings/)
for (const status of ['DRAFT','SUBMITTED','CONFIRMED','RELEASED','FAILED','CHANGE_PENDING','CANCELLED',
for (const status of ['CONFIRMED','RELEASED','CHANGE_PENDING','CANCELLED',
'PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED',
'COMPLETED','RESCHEDULED','EXCEPTION']) assert.match(source, new RegExp(`['"]${status}['"]`))
assert.match(source, /change-request/)
assert.match(source, /const OCEAN_LIST_STATUSES = \['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED'\]/)
assert.match(source, /const OCEAN_HIDDEN_STATUSES = \['DRAFT', 'SUBMITTED', 'FAILED'\]/)
assert.match(source, /hiddenFixtures/)
assert.match(source, /code\(listResponse\) === 'A0400'/)
assert.match(source, /后台样本仅通过详情契约验证/)
assert.doesNotMatch(source, /code\(listResponse\) !== 'A0400'\) return/)
assert.match(source, /const OCEAN_STATUSES = \[\.\.\.OCEAN_LIST_STATUSES\]/)
assert.doesNotMatch(source, /OCEAN_HIDDEN_STATUSES|hiddenFixtures|verifyHiddenOcean/)
assert.match(source, /一次创建直接返回 CONFIRMED/)
assert.doesNotMatch(source, /ocean-bookings\/\$\{[^}]+\}\/submit|ocean-bookings\/\$\{[^}]+\}\/confirm/)
const draftPayload = source.slice(source.indexOf('function oceanPayload'), source.indexOf('async function createOcean'))
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
assert.doesNotMatch(draftPayload, new RegExp(`${field}:`))
assert.match(draftPayload, new RegExp(`${field}:`))
}
const lifecycle = source.slice(source.indexOf('async function buildOcean'), source.indexOf('function truckPayload'))
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
assert.match(lifecycle, new RegExp(`${field}:`))
}
assert.doesNotMatch(lifecycle, /'submit'|'confirm'|'fail'/)
assert.match(source, /\/reschedule\?plannedLoadingAt=/)
assert.match(source, /page=1&size=100&status=/)
assert.match(source, /customerId: oceanBooking\.customerId/)
@@ -3,8 +3,7 @@ const OPTIONS = { scenario: 'manual-ocean-trucking-state-matrix', deploymentBloc
const OCEAN_BASE = '/api/platform/v1/ocean-bookings'
const TRUCK_BASE = '/api/platform/v1/truck-appointments'
const OCEAN_LIST_STATUSES = ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED']
const OCEAN_HIDDEN_STATUSES = ['DRAFT', 'SUBMITTED', 'FAILED']
const OCEAN_STATUSES = ['DRAFT', 'SUBMITTED', 'CONFIRMED', 'RELEASED', 'FAILED', 'CHANGE_PENDING', 'CANCELLED']
const OCEAN_STATUSES = [...OCEAN_LIST_STATUSES]
const TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE',
'LOADING', 'DEPARTED', 'COMPLETED', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED']
@@ -24,12 +23,15 @@ function oceanPayload(context, template, index) {
customerId: source.customerId, customerName: source.customerName,
channelCode: source.channelCode,
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
billOfLadingNo: `BL-${suffix(context, index)}`, vesselName: `人工轮-${suffix(context, index)}`,
voyageNo: `V${suffix(context, index)}`,
shippingCompanyId: source.shippingCompanyId,
shippingCompanyName: source.shippingCompanyName, bookingAgentId: source.bookingAgentId,
bookingAgentName: source.bookingAgentName,
departurePortCode: source.departurePortCode, departurePortName: source.departurePortName,
destinationPortCode: source.destinationPortCode, destinationPortName: source.destinationPortName,
placeOfDelivery: source.placeOfDelivery, serviceTerm: source.serviceTerm, freightTerm: source.freightTerm,
estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00',
remark: `${context.marker} 人工列表状态 ${index}`,
parties: template.parties.map(({ partyType, partyName, address, contactName, contactPhone }) =>
({ partyType, partyName, address, contactName, contactPhone })),
@@ -41,7 +43,12 @@ function oceanPayload(context, template, index) {
async function createOcean(context, template, index) {
const response = await post(context, OCEAN_BASE, oceanPayload(context, template, index), `ocean-create-${index}`)
const value = context.apiSuccess(MODULE, `创建订舱状态样本 ${index}`, response, '', OPTIONS)
if (!value?.oceanBooking?.id) throw new Error(`创建订舱样本 ${index} 失败`)
context.assert(MODULE, `订舱样本 ${index} 一次创建直接返回 CONFIRMED`, response,
Boolean(value?.oceanBooking?.id) && value?.oceanBooking?.status === 'CONFIRMED',
`id=${id(value?.oceanBooking?.id) || 'missing'}, status=${value?.oceanBooking?.status || 'missing'}`, OPTIONS)
if (!value?.oceanBooking?.id || value?.oceanBooking?.status !== 'CONFIRMED') {
throw new Error(`创建订舱样本 ${index} 未直接进入 CONFIRMED`)
}
return value
}
@@ -55,13 +62,8 @@ async function oceanAction(context, value, endpoint, body = {}) {
async function buildOcean(context, template, status, index) {
let value = await createOcean(context, template, index)
if (status === 'DRAFT') return value
value = await oceanAction(context, value, 'submit')
if (status === 'SUBMITTED') return value
if (status === 'FAILED') return oceanAction(context, value, 'fail')
if (status === 'CANCELLED') return oceanAction(context, value, 'cancel')
value = await oceanAction(context, value, 'confirm')
if (status === 'CONFIRMED') return value
if (status === 'CANCELLED') return oceanAction(context, value, 'cancel')
if (status === 'CHANGE_PENDING') return oceanAction(context, value, 'change-request', {
vesselName: `变更轮-${suffix(context, index)}`, voyageNo: `CV${suffix(context, index)}`,
estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00' })
@@ -130,19 +132,6 @@ async function verify(context, base, status, resourceId) {
`status=${status}, id=${resourceId}`, OPTIONS)
}
async function verifyHiddenOcean(context, status, resourceId) {
const listResponse = await context.request('GET', `${OCEAN_BASE}?page=1&size=100&status=${status}`)
context.assert(MODULE, `${status} 不进入正式订舱列表筛选`, listResponse,
listResponse?.http === 200 && code(listResponse) === 'A0400',
`HTTP=${listResponse?.http ?? 'missing'}, code=${code(listResponse)}, id=${resourceId}`, OPTIONS)
const detailResponse = await context.request('GET', `${OCEAN_BASE}/${resourceId}`)
const detail = context.apiSuccess(MODULE, `${status} 后台样本仅通过详情契约验证`, detailResponse, '', OPTIONS)
context.assert(MODULE, `${status} 后台详情身份与状态正确`, detailResponse,
id(detail?.oceanBooking?.id) === resourceId && detail?.oceanBooking?.status === status,
`id=${id(detail?.oceanBooking?.id) || 'missing'}, actual=${detail?.oceanBooking?.status || 'missing'}`, OPTIONS)
}
export async function run(context, matrix = context.createdResources.manualStateMatrix) {
const sourceOceanId = id(context.createdResources?.oceanBooking?.id)
if (!sourceOceanId) throw new Error('缺少现有订舱生命周期模块生成的模板资源')
@@ -151,7 +140,7 @@ export async function run(context, matrix = context.createdResources.manualState
if (!template?.oceanBooking || !template?.parties?.length || !template?.containerRequirements?.length) {
throw new Error('订舱模板缺少渠道、仓库、参与方或箱型需求')
}
matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {}, hiddenFixtures: {} }
matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {} }
for (const [index, status] of OCEAN_STATUSES.entries()) {
const value = await buildOcean(context, template, status, index)
const resourceId = id(value?.oceanBooking?.id); const actual = value?.oceanBooking?.status
@@ -163,11 +152,6 @@ export async function run(context, matrix = context.createdResources.manualState
customerId: id(value?.oceanBooking?.customerId), customerName: value?.oceanBooking?.customerName,
warehouseId: id(value?.oceanBooking?.warehouseId),
}
if (OCEAN_HIDDEN_STATUSES.includes(status)) {
matrix.pages.oceanBookings.hiddenFixtures[status] = fixtureValue
await verifyHiddenOcean(context, status, resourceId)
continue
}
matrix.pages.oceanBookings.fixtures[status] = {
...fixtureValue, detailPath: `/ocean-bookings/${resourceId}`,
}
@@ -41,7 +41,7 @@ test('backend OpenAPI freezes a platform channel query scoped by originWarehouse
test('explicit real booking association locks the selected channel onto the simulation', () => {
const fixture = source(new URL('./phase3-container-loading.mjs', import.meta.url))
assert.match(fixture, /ocean-booking-link/)
assert.match(fixture, /CONFIRMED 只产生真实订舱事实且尚未占用模拟柜/)
assert.match(fixture, /一次登记直接返回 CONFIRMED 且尚未占用模拟柜/)
assert.match(fixture, /containerDetail\?\.channelCode === channelCode/)
assert.match(fixture, /selectPlatformChannelIsolationPair/)
assert.doesNotMatch(fixture, /\$\{channelCode\}-OTHER|foreignChannelCode/)
@@ -31,7 +31,7 @@ test('ocean booking fixture blocks creation instead of inventing missing master
assert.match(steps[0][4], /missing=customerId,customerName,channelCode,channelName,warehouseId,warehouseName/)
})
test('shared booking draft omits server-owned channelName and release transport data', () => {
test('shared booking data omits server-owned channelName and includes offline-confirmed transport snapshots', () => {
const draft = oceanBookingDraft({ marker: 'ADMIN-RUN-3' }, '3', {
customerId: '21', customerName: 'IT 测试客户', channelCode: 'SEA-US', channelName: '美西海运',
warehouseId: '41', warehouseName: '深圳仓',
@@ -44,13 +44,15 @@ test('shared booking draft omits server-owned channelName and release transport
assert.equal(draft.freightTerm, 'NINGBO')
assert.equal(draft.containerRequirements[0].capacityCbm, 68)
assert.equal(draft.containerRequirements[0].maxWeightKg, 28000)
for (const key of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
assert.equal(Object.hasOwn(draft, key), false, `${key} belongs to release, not draft`)
}
assert.equal(draft.billOfLadingNo, 'BL-3')
assert.equal(draft.vesselName, '验收船-3')
assert.equal(draft.voyageNo, 'V-3')
assert.equal(draft.estimatedSailingAt, '2026-09-01T08:00:00')
assert.equal(draft.estimatedArrivalAt, '2026-09-20T08:00:00')
const fixtureSource = readFileSync(new URL('./ocean-booking-fixture.mjs', import.meta.url), 'utf8')
const builderSource = fixtureSource.slice(fixtureSource.indexOf('export function oceanBookingDraft'), fixtureSource.indexOf('export function oceanBookingDraftWithoutCustomer'))
for (const key of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
assert.doesNotMatch(builderSource, new RegExp(`${key}\\s*:`), `${key} must not be emitted by the shared draft builder`)
assert.match(builderSource, new RegExp(`${key}\\s*:`), `${key} must be emitted by the confirmed booking builder`)
}
})
@@ -101,11 +103,22 @@ test('all direct ocean booking payload builders omit channelName under the froze
const value = readFileSync(new URL(name, import.meta.url), 'utf8')
const region = value.slice(value.indexOf(start), value.indexOf(end))
assert.doesNotMatch(region, /channelName\s*:/, name)
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
assert.match(region, new RegExp(`${field}\\s*:`), `${name} must register ${field}`)
}
}
const container = readFileSync(new URL('phase3-container-loading.mjs', import.meta.url), 'utf8')
assert.doesNotMatch(container, /data:\s*\{[^}]*channelName\s*:/s)
})
test('ocean booking producers register CONFIRMED data directly without draft submit or confirm transitions', () => {
for (const name of ['ocean-bookings.mjs', 'phase3-loading.mjs', 'phase3-container-loading.mjs',
'phase3-order-splitting.mjs', 'manual-state-loading-pda.mjs', 'manual-state-ocean-trucking.mjs']) {
const source = readFileSync(new URL(name, import.meta.url), 'utf8')
assert.doesNotMatch(source, /ocean-bookings\/[^`'"\\s]+\/(?:submit|confirm)|booking-(?:submit|confirm)/, name)
}
})
test('all independent ocean booking producers use the shared master-data contract', () => {
for (const name of ['ocean-bookings.mjs', 'phase3-port-schedules.mjs', 'phase3-carrier-schedule-queries.mjs']) {
const source = readFileSync(new URL(name, import.meta.url), 'utf8')
@@ -56,10 +56,14 @@ export function oceanBookingDraft(context, suffix, master, labels = {}) {
const { channelName: _readOnlyChannelName, ...writableMaster } = master
return {
...writableMaster,
billOfLadingNo: labels.billOfLadingNo || `BL-${suffix}`,
shippingCompanyId: null, shippingCompanyName: labels.shippingCompanyName || `验收船公司-${suffix}`,
bookingAgentId: null, bookingAgentName: labels.bookingAgentName || `验收代理-${suffix}`,
vesselName: labels.vesselName || `验收船-${suffix}`, voyageNo: labels.voyageNo || `V-${suffix}`,
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
estimatedSailingAt: labels.estimatedSailingAt || '2026-09-01T08:00:00',
estimatedArrivalAt: labels.estimatedArrivalAt || '2026-09-20T08:00:00',
remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
@@ -61,18 +61,24 @@ test('customer is optional across Admin DraftData DTO and OpenAPI', () => {
assert.match(openapi, /customerName: \{[^\n]*nullable: true/)
})
test('draft save omits release-only transport values while release still requires all five', () => {
test('offline-confirmed registration requires transport values while release keeps the frozen five-field snapshot', () => {
const draftType = adminTypes.slice(adminTypes.indexOf('export interface OceanBookingDraftData'), adminTypes.indexOf('export interface OceanBookingResource'))
for (const field of releaseFields) {
assert.doesNotMatch(adminPage, new RegExp(`v-model="form\\.data\\.${field}"`),
`${field} must be read-only outside the release dialog`)
assert.doesNotMatch(draftType, new RegExp(`${field}\\??:`), `${field} must not belong to OceanBookingDraftData`)
assert.match(adminPage, new RegExp(`(?:v-model="releaseForm\\.${field}"|releaseForm\\.${field})`))
assert.match(adminPage, new RegExp(`v-model="form\\.data\\.${field}"`),
`${field} must be entered when registering an offline-confirmed booking`)
assert.match(draftType, new RegExp(`${field}\\??:`), `${field} must belong to OceanBookingDraftData`)
}
assert.match(adminTest, /\['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt'\]/)
assert.match(adminTest, /not\.toContain\(`v-model="form\.data\.\$\{field\}"`\)/)
assert.match(adminTest, /线下订舱资料已登记并确认/)
assert.match(adminTest, /登记后直接确认/)
const saveRegion = adminPage.slice(adminPage.indexOf('async function save'), adminPage.indexOf('function requireReason'))
assert.doesNotMatch(saveRegion, /data:\s*form\.data/)
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
for (const field of releaseFields) assert.match(required, new RegExp(field))
assert.match(dto, /@NotBlank @Size\(max = 128\) String billOfLadingNo/)
assert.match(dto, /@NotBlank @Size\(max = 128\) String vesselName/)
assert.match(dto, /@NotBlank @Size\(max = 64\) String voyageNo/)
assert.match(dto, /@NotNull LocalDateTime estimatedSailingAt/)
assert.match(dto, /@NotNull LocalDateTime estimatedArrivalAt/)
const releaseCall = adminPage.slice(adminPage.indexOf('releaseOceanBooking(id.value'), adminPage.indexOf('releaseDialogOpen.value = false'))
for (const field of releaseFields) assert.match(releaseCall, new RegExp(field))
})
@@ -33,22 +33,7 @@ async function createConfirmedUnassignedBooking(context, suffix, master) {
`bookingId=${bookingId || 'missing'}, customerId=${id(created?.oceanBooking?.customerId) || 'null'}, customerName=${created?.oceanBooking?.customerName || 'null'}`, OPTIONS)
if (!bookingId) return null
const hiddenResponse = await request(context, 'GET', `${BASE}?page=1&size=20&keyword=${encodeURIComponent(createdBookingNo)}`)
const hiddenPage = context.apiSuccess(MODULE, '未确认订舱不进入正式订舱列表', hiddenResponse, '', OPTIONS)
context.assert(MODULE, 'DRAFT 未指定客户订舱仅可按详情维护', hiddenResponse,
!hiddenPage?.records?.some?.((item) => id(item.id) === bookingId),
`bookingId=${bookingId}, records=${hiddenPage?.records?.length ?? 'n/a'}`, OPTIONS)
const path = `${BASE}/${bookingId}`
const submitResponse = await request(context, 'POST', `${path}/submit`, {
expectedVersion: Number(created.oceanBooking.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-ocean-unassigned-submit`.slice(0, 128) })
const submitted = context.apiSuccess(MODULE, '提交未指定客户订舱', submitResponse, '', OPTIONS)
const confirmResponse = await request(context, 'POST', `${path}/confirm`, {
expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-ocean-unassigned-confirm`.slice(0, 128) })
context.apiSuccess(MODULE, '确认未指定客户真实订舱', confirmResponse, '', OPTIONS)
const detailResponse = await request(context, 'GET', path)
const detail = context.apiSuccess(MODULE, '回查未指定客户订舱持久化结果', detailResponse, '', OPTIONS)
const bookingNo = id(detail?.oceanBooking?.bookingNo || created?.oceanBooking?.bookingNo)
@@ -135,29 +120,23 @@ export async function run(context) {
const createResponse = await createBooking(context, suffix, key, master)
const detail = data(createResponse); const bookingId = id(detail?.oceanBooking?.id)
context.apiSuccess(MODULE, '创建隔离海运订舱', createResponse, `key=${key}`, OPTIONS)
context.assert(MODULE, '订舱 DTO 字段、DRAFT 和9类关联字段完整', createResponse, Boolean(bookingId) && detail?.oceanBooking?.status === 'DRAFT' && detail?.parties?.length === 3 && detail?.containerRequirements?.length === 1, `bookingId=${bookingId || 'missing'}, parties=${detail?.parties?.length ?? 'n/a'}, requirements=${detail?.containerRequirements?.length ?? 'n/a'}`, OPTIONS)
context.assert(MODULE, '线下已确认订舱 DTO、运输快照和关联字段完整', createResponse,
Boolean(bookingId) && detail?.oceanBooking?.status === 'CONFIRMED'
&& detail?.parties?.length === 3 && detail?.containerRequirements?.length === 1
&& ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
.every((field) => Boolean(detail?.oceanBooking?.[field])),
`bookingId=${bookingId || 'missing'}, status=${detail?.oceanBooking?.status || 'missing'}, parties=${detail?.parties?.length ?? 'n/a'}, requirements=${detail?.containerRequirements?.length ?? 'n/a'}`, OPTIONS)
context.assert(MODULE, '指定客户订舱持久化本轮客户范围快照', createResponse,
id(detail?.oceanBooking?.customerId) === id(master.customerId) && detail?.oceanBooking?.customerName === master.customerName,
`expectedCustomerId=${id(master.customerId)}, actualCustomerId=${id(detail?.oceanBooking?.customerId) || 'missing'}`, OPTIONS)
if (!bookingId) return
context.createdResources.oceanBooking = { id: bookingId, customerId: master.customerId, marker: context.marker,
containerBookingId: null, loadingPlanId: null, finalStatus: 'DRAFT' }
containerBookingId: null, loadingPlanId: null, finalStatus: 'CONFIRMED' }
const replay = await createBooking(context, suffix, key, master)
context.assert(MODULE, '创建 Idempotency-Key 重放返回同一订舱', replay, String(data(replay)?.oceanBooking?.id) === bookingId, `first=${bookingId}, replay=${id(data(replay)?.oceanBooking?.id)}`, OPTIONS)
const path = `${BASE}/${bookingId}`
const initialVersion = Number(detail.oceanBooking.version)
const edited = { ...oceanBookingDraft(context, suffix, master), shippingCompanyName: `编辑船司-${suffix}` }
const update = await request(context, 'PUT', path, { expectedVersion: initialVersion, data: edited }, { 'Idempotency-Key': `${context.marker}-ocean-edit` })
context.apiSuccess(MODULE, '编辑 DRAFT 订舱', update, `version=${initialVersion}`, OPTIONS)
const stale = await request(context, 'PUT', path, { expectedVersion: initialVersion, data: { ...edited, shippingCompanyName: `STALE-${suffix}` } }, { 'Idempotency-Key': `${context.marker}-ocean-stale` })
step(context, '订舱旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409', '409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`)
const afterStale = await request(context, 'GET', path); const afterData = data(afterStale)
context.apiSuccess(MODULE, '冲突后回查订舱', afterStale, '', OPTIONS)
context.assert(MODULE, '订舱冲突未部分更新', afterStale, afterData?.oceanBooking?.shippingCompanyName === edited.shippingCompanyName, `shippingCompanyName=${afterData?.oceanBooking?.shippingCompanyName || 'missing'}`, OPTIONS)
let version = Number(afterData?.oceanBooking?.version)
let version = Number(detail.oceanBooking.version)
const action = async (name, endpoint, expected, reason) => { const r = await request(context, 'POST', `${path}/${endpoint}`, { expectedVersion: expected, reason }, { 'Idempotency-Key': `${context.marker}-ocean-${endpoint}` }); const d = data(r); context.apiSuccess(MODULE, name, r, `status=${d?.oceanBooking?.status || 'missing'}`, OPTIONS); version = Number(d?.oceanBooking?.version); return d }
await action('提交订舱', 'submit', version, 'marker submit')
await action('确认订舱', 'confirm', version, 'marker confirm')
const changed = await request(context, 'POST', `${path}/change-request`, { expectedVersion: version, vesselName: `改船-${suffix}`, voyageNo: `CV${suffix}`, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-change` })
context.apiSuccess(MODULE, '申请改船', changed, '', OPTIONS); version = Number(data(changed)?.oceanBooking?.version)
const changeConfirmed = await action('确认改船', 'change-confirm', version, 'confirm change')
@@ -185,7 +164,7 @@ export async function run(context) {
const final = await request(context, 'GET', path); const fd = data(final)
context.apiSuccess(MODULE, '回查 RELEASED 订舱详情', final, '', OPTIONS)
context.assert(MODULE, '独立订舱状态历史、审计操作者和终态完整', final,
fd?.oceanBooking?.status === 'RELEASED' && fd.histories?.length >= 5 && fd.histories.every((h) => h.operatorName)
fd?.oceanBooking?.status === 'RELEASED' && fd.histories?.length >= 4 && fd.histories.every((h) => h.operatorName)
&& fd?.containerAllocations?.length === 0,
`status=${fd?.oceanBooking?.status || 'missing'}, histories=${fd?.histories?.length ?? 'n/a'}, allocations=${fd?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
let downstreamBooking = {
@@ -115,11 +115,14 @@ export async function run(context) {
const bookingDraft = {
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
channelCode, warehouseId, warehouseName,
billOfLadingNo: `BL-${suffix}`.slice(0, 128),
shippingCompanyId: null, shippingCompanyName: `验收船公司-${suffix}`,
bookingAgentId: null, bookingAgentName: `验收代理-${suffix}`,
vesselName: `验收船-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64),
departurePortCode: 'CNSZX', departurePortName: '深圳',
destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX',
serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: context.marker,
serviceTerm: 'CY-CY', freightTerm: 'NINGBO', estimatedSailingAt: '2026-09-01T08:00:00',
estimatedArrivalAt: '2026-09-20T08:00:00', remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }],
@@ -232,32 +235,12 @@ export async function run(context) {
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` })
const bookingCreated = context.apiSuccess(MODULE, '创建含渠道仓库快照的真实订舱草稿', bookingCreate, '', OPTIONS)
const bookingCreated = context.apiSuccess(MODULE, '登记含渠道仓库和运输快照的已确认真实订舱', bookingCreate, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
const bookingNo = id(bookingCreated?.oceanBooking?.bookingNo)
if (!bookingId) return
const hiddenDraftResponse = await context.request('GET',
`/api/platform/v1/ocean-bookings?page=1&size=20&keyword=${encodeURIComponent(bookingNo)}`)
const hiddenDraftPage = context.apiSuccess(MODULE, '正式订舱列表查询不含草稿', hiddenDraftResponse, '', OPTIONS)
context.assert(MODULE, '真实订舱确认前不进入正式列表', hiddenDraftResponse,
!records(hiddenDraftPage).some((item) => id(item.id) === bookingId),
`bookingId=${bookingId}, records=${records(hiddenDraftPage).length}`, OPTIONS)
const draftFilterResponse = await request(context, 'GET', '/api/platform/v1/ocean-bookings?page=1&size=5&status=DRAFT')
context.addStep(MODULE, '正式订舱列表拒绝 DRAFT 状态筛选', draftFilterResponse,
failure(draftFilterResponse), `HTTP=${draftFilterResponse.http}, code=${codeOf(draftFilterResponse)}`, OPTIONS)
const submit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: Number(bookingCreated.oceanBooking.version), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-loading-booking-submit` })
const submitted = context.apiSuccess(MODULE, '提交隔离订舱', submit, '', OPTIONS)
const submittedFilterResponse = await request(context, 'GET', '/api/platform/v1/ocean-bookings?page=1&size=5&status=SUBMITTED')
context.addStep(MODULE, '正式订舱列表拒绝 SUBMITTED 状态筛选', submittedFilterResponse,
failure(submittedFilterResponse), `HTTP=${submittedFilterResponse.http}, code=${codeOf(submittedFilterResponse)}`, OPTIONS)
const confirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`,
{ expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-loading-booking-confirm` })
const confirmedBooking = context.apiSuccess(MODULE, '确认真实订舱但不自动创建内部柜', confirm, '', OPTIONS)
context.assert(MODULE, 'CONFIRMED 只产生真实订舱事实且尚未占用模拟柜', confirm,
const confirmedBooking = bookingCreated
context.assert(MODULE, '一次登记直接返回 CONFIRMED 且尚未占用模拟柜', bookingCreate,
confirmedBooking?.oceanBooking?.status === 'CONFIRMED'
&& Array.isArray(confirmedBooking?.containerAllocations) && confirmedBooking.containerAllocations.length === 0,
`status=${confirmedBooking?.oceanBooking?.status || 'missing'}, allocations=${confirmedBooking?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
@@ -387,7 +370,7 @@ export async function run(context) {
voyageNo: `V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt,
estimatedArrivalAt: arrivalAt, reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) })
const releasedBooking = context.apiSuccess(MODULE, '订舱五项运输字段放舱并满足 PDA 订舱门禁', releaseResponse, '', OPTIONS)
const releasedBooking = context.apiSuccess(MODULE, '订舱携带已冻结运输快照放舱并满足 PDA 门禁', releaseResponse, '', OPTIONS)
context.assert(MODULE, '放舱后订舱进入 RELEASED 且柜次分配保持稳定', releaseResponse,
releasedBooking?.oceanBooking?.status === 'RELEASED'
&& releasedBooking?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerId),
@@ -22,14 +22,16 @@ const containerLoadingAcceptance = read('phase3-container-loading.mjs')
const truckAcceptance = read('truck-appointments.mjs')
const matrix = read('../../../docs/三期/货柜管理全链验收矩阵-2026-08-13.md')
test('release request freezes all five transport fields and UI blocks incomplete submission', () => {
test('registration freezes all five transport fields and release reuses the confirmed snapshot', () => {
assert.match(oceanOpenApi, /required: \[expectedVersion, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt\]/)
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
assert.match(releaseTypes, new RegExp(`${field}: string`), field)
assert.match(releaseView, new RegExp(`releaseForm\\.${field}`), field)
assert.match(releaseView, new RegExp(`form\\.data\\.${field}`), field)
assert.match(releaseView, new RegExp(`detail\\.value\\.${field}`), field)
assert.match(oceanAcceptance, new RegExp(`${field}:`), field)
}
assert.match(releaseView, /if \(!billOfLadingNo \|\| !vesselName \|\| !voyageNo \|\| !releaseForm\.estimatedSailingAt \|\| !releaseForm\.estimatedArrivalAt\)/)
assert.match(releaseView, /当前订舱缺少已确认运输资料,无法放舱/)
assert.match(releaseView, /放舱不会重新采集或修改这些信息/)
})
test('overview regression requires workflowStatus and distinguishes 100 percent awaiting review', () => {
@@ -88,7 +90,7 @@ test('PDA task opens only after preload push, real container number and ocean re
assert.match(containerLoadingAcceptance, /version: Number\(linkedContainer\.version\), reason: context\.marker/)
assert.match(containerLoadingAcceptance, /订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务/)
assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/)
assert.match(containerLoadingAcceptance, /订舱五项运输字段放舱并满足 PDA 订舱门禁/)
assert.match(containerLoadingAcceptance, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/)
assert.match(containerLoadingAcceptance, /柜号与 RELEASED 订舱就绪后推送预装计划/)
assert.match(containerLoadingAcceptance, /releasedBooking\?\.oceanBooking\?\.status === 'RELEASED'/)
})
@@ -121,9 +121,12 @@ export async function run(context) {
customerId: tenantId, customerName: initial?.summary?.customerName,
warehouseId, warehouseName: initial?.summary?.warehouseName,
channelCode: initial?.summary?.channelCode,
billOfLadingNo: `P3BL-${suffix}`.slice(0, 128),
shippingCompanyName: `P3-SHIPPING-${suffix}`, bookingAgentName: `P3-AGENT-${suffix}`,
vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64),
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00',
remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `P3-SHIPPER-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `P3-CONSIGNEE-${suffix}` },
@@ -148,16 +151,9 @@ export async function run(context) {
const bookingCreated = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期订舱', bookingCreateResponse, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
if (!bookingId) return
const bookingSubmitResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: Number(bookingCreated.oceanBooking.version), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-p3-booking-submit`.slice(0, 128) })
const bookingSubmitted = context.apiSuccess(MODULE, '提交三期订舱', bookingSubmitResponse, '', OPTIONS)
const bookingConfirmResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`,
{ expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-p3-booking-confirm`.slice(0, 128) })
const bookingConfirmed = context.apiSuccess(MODULE, '确认真实订舱且不自动生成三期内部柜', bookingConfirmResponse, '', OPTIONS)
const bookingConfirmed = bookingCreated
const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
context.assert(MODULE, '订舱确认仅冻结真实订舱及箱型需求', bookingConfirmResponse,
context.assert(MODULE, '一次登记直接返回 CONFIRMED 且不自动生成三期内部柜', bookingCreateResponse,
bookingConfirmed?.oceanBooking?.status === 'CONFIRMED' && Boolean(requirement?.id)
&& bookingConfirmed?.containerAllocations?.length === 0,
`status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, requirementId=${id(requirement?.id) || 'missing'}, allocations=${bookingConfirmed?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
@@ -291,7 +287,7 @@ export async function run(context) {
voyageNo: `P3V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt,
estimatedArrivalAt: arrivalAt, reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) })
const bookingReleased = context.apiSuccess(MODULE, '三期订舱补齐五项运输字段并放舱', releaseResponse, '', OPTIONS)
const bookingReleased = context.apiSuccess(MODULE, '三期订舱携带已冻结运输快照并放舱', releaseResponse, '', OPTIONS)
context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份', releaseResponse,
bookingReleased?.oceanBooking?.status === 'RELEASED'
&& bookingReleased?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerBookingId),
@@ -7,8 +7,11 @@ const source = readFileSync(new URL('./phase3-order-splitting.mjs', import.meta.
test('each remainder uses a simulation linked to a confirmed then released booking', () => {
assert.match(source, /POST', CONTAINERS/)
assert.match(source, /POST', '\/api\/platform\/v1\/ocean-bookings'/)
assert.match(source, /ocean-bookings\/\$\{oceanBookingId\}\/submit/)
assert.match(source, /ocean-bookings\/\$\{oceanBookingId\}\/confirm/)
assert.match(source, /一次登记直接返回 CONFIRMED/)
assert.doesNotMatch(source, /ocean-bookings\/\$\{oceanBookingId\}\/(?:submit|confirm)/)
const createPayload = source.slice(source.indexOf('function bookingPayload'), source.indexOf('async function loadOneFragment'))
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt',
'estimatedArrivalAt']) assert.match(createPayload, new RegExp(`${field}:`))
assert.match(source, /ocean-booking-link/)
assert.match(source, /bookingDetail\.oceanBooking\.status === 'CONFIRMED'/)
assert.match(source, /`\$\{bookingPath\}\/release`/)
@@ -122,10 +122,13 @@ function bookingPayload(marker, sequence, source) {
customerId: source.tenantId, customerName: source.customerName,
channelCode: source.channelCode,
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
billOfLadingNo: `BL-${suffix}`.slice(0, 128),
shippingCompanyId: null, shippingCompanyName: `SPLIT-SHIPPING-${suffix}`.slice(0, 128),
bookingAgentId: null, bookingAgentName: `SPLIT-AGENT-${suffix}`.slice(0, 128),
vesselName: `SPLIT-VESSEL-${suffix}`.slice(0, 128), voyageNo: `SV-${sequence}`.slice(0, 64),
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: marker,
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00', remark: marker,
parties: [{ partyType: 'SHIPPER', partyName: `SPLIT-SHIPPER-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `SPLIT-CONSIGNEE-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `SPLIT-NOTIFY-${suffix}` }],
@@ -151,15 +154,11 @@ async function loadOneFragment(context, state) {
const oceanBookingId = id(bookingCreated?.oceanBooking?.id)
if (!oceanBookingId) return null
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(oceanBookingId)
const bookingSubmit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/submit`, {
expectedVersion: Number(bookingCreated?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${operationBase}-booking-submit` })
const bookingSubmitted = context.apiSuccess(MODULE, `提交 ${sourceOrderNo} 的订舱`, bookingSubmit, '', OPTIONS)
const bookingConfirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/confirm`, {
expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${operationBase}-booking-confirm` })
const bookingConfirmed = context.apiSuccess(MODULE, `确认 ${sourceOrderNo} 的真实订舱`, bookingConfirm, '', OPTIONS)
const bookingConfirmed = bookingCreated
context.assert(MODULE, `${sourceOrderNo} 一次登记直接返回 CONFIRMED`, bookingCreate,
bookingConfirmed?.oceanBooking?.status === 'CONFIRMED'
&& bookingConfirmed?.containerAllocations?.length === 0,
`status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, allocations=${bookingConfirmed?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
if (!requirement?.id) return null
const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
@@ -91,7 +91,7 @@ test('bridge loading starts through a real PDA batch before preload confirmation
'无真实柜号推送失败后不生成 PDA 任务',
'绑定真实柜号后开放三期 PDA 推送门禁',
'订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务',
'三期订舱补齐五项运输字段并放舱',
'三期订舱携带已冻结运输快照并放舱',
'三期订舱 RELEASED 后保持模拟柜关联身份',
'真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务',
"pushedLoadingPlan?.status === 'EDITING'",
@@ -153,15 +153,16 @@ test('container loading arranges a simulation before linking a confirmed real bo
"const planResponse = await request(context, 'POST', `${SMART}/plans`",
"const assignResponse = await request",
"const bookingCreate = await request",
"const submit = await request",
"const confirm = 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, \'订舱渠道与仓库快照完整\''))
assert.doesNotMatch(draft, /billOfLadingNo|vesselName|voyageNo|estimatedSailingAt|estimatedArrivalAt/)
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/)
@@ -183,7 +184,7 @@ test('container loading arranges a simulation before linking a confirmed real bo
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, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/)
assert.match(value, /无真实柜号拒绝推送预装计划/)
assert.match(value, /柜号与 RELEASED 订舱就绪后推送预装计划/)
assert.match(value, /const confirmedTaskAbsent =[\s\S]*if \(!confirmedTaskAbsent\) return/)
@@ -263,7 +264,7 @@ test('container loading enforces channel isolation and stable PDA identity acros
'无真实柜号推送失败后不生成 PDA 任务',
'在放舱前绑定本轮真实箱号',
'订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务',
'订舱五项运输字段放舱并满足 PDA 订舱门禁',
'订舱携带已冻结运输快照放舱并满足 PDA 门禁',
'柜号与 RELEASED 订舱就绪后推送预装计划',
'换柜号前读取 PDA 稳定库存快照',
'PDA 任务创建后替换真实柜号',
@@ -73,7 +73,7 @@ test('test-environment loading chains bind the latest container version before o
const bridgeIndex = bridgeLoading.indexOf('原子关联入库预装与柜次装柜计划')
const rejectedPushIndex = bridgeLoading.indexOf('无真实柜号拒绝推送柜次计划')
const transportIndex = bridgeLoading.indexOf('绑定真实柜号后开放三期 PDA 推送门禁')
const releaseIndex = bridgeLoading.indexOf('三期订舱补齐五项运输字段并放舱')
const releaseIndex = bridgeLoading.indexOf('三期订舱携带已冻结运输快照并放舱')
const successfulPushIndex = bridgeLoading.indexOf('真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务')
const pdaIndex = bridgeLoading.indexOf('读取柜次级 PDA 唯一事实任务')
assert.ok(bridgeIndex >= 0 && rejectedPushIndex > bridgeIndex && transportIndex > rejectedPushIndex
@@ -85,7 +85,7 @@ test('test-environment loading chains bind the latest container version before o
const containerRejectedPushIndex = containerLoading.indexOf('无真实柜号拒绝推送预装计划')
const containerTransportIndex = containerLoading.indexOf('在放舱前绑定本轮真实箱号')
const containerReleaseIndex = containerLoading.indexOf('订舱五项运输字段放舱并满足 PDA 订舱门禁')
const containerReleaseIndex = containerLoading.indexOf('订舱携带已冻结运输快照放舱并满足 PDA 门禁')
const containerSuccessfulPushIndex = containerLoading.indexOf('柜号与 RELEASED 订舱就绪后推送预装计划')
const containerPdaIndex = containerLoading.indexOf('查询本轮柜次 PDA 拣货任务')
assert.ok(containerRejectedPushIndex >= 0 && containerTransportIndex > containerRejectedPushIndex