Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs
#	scripts/admin-test-env-modules/phase4-overseas-warehouse-contract.test.mjs
This commit is contained in:
2026-09-04 15:41:18 +08:00
30 changed files with 762 additions and 147 deletions
@@ -111,12 +111,10 @@ export function portScheduleFieldGaps(record) {
}
export function truckAppointmentFieldGaps(record) {
return gaps(record, {
const result = gaps(record, {
id: isPositiveId,
appointmentNo: isNonBlank,
oceanBookingId: isPositiveId,
containerBookingId: isPositiveId,
loadingPlanId: (value) => value == null || isPositiveId(value),
warehouseId: (value) => value == null || isPositiveId(value),
plannedLoadingAt: isIsoDateTime,
status: isNonBlank,
@@ -126,6 +124,10 @@ export function truckAppointmentFieldGaps(record) {
currency: (value) => value === 'RMB' || value === 'USD',
attachmentCount: isNonNegativeNumber,
})
for (const field of ['containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo', 'containers']) {
if (Object.hasOwn(record || {}, field)) result.push(`forbidden:${field}`)
}
return result
}
export function customsCaseFieldGaps(record) {
@@ -54,7 +54,7 @@ test('Admin 当前列表逐字段列与验收矩阵一致', () => {
'loading-lists/components/LoadingListsView.vue': ['preloadNo', 'customers', 'skuCount', 'loadingListNo', 'totalPackageCount', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'createdByName'],
'ocean-bookings/pages/OceanBookingsView.vue': ['bookingNo', 'customerName', 'billOfLadingNo', 'carrierName', 'bookingAgent', 'vesselName', 'pol', 'pod', 'etd', 'eta', 'updatedAt'],
'port-schedules/pages/PortSchedulesView.vue': ['schedule.scheduleNo', 'schedule.sourceName', 'schedule.vesselName', 'schedule.departurePortName', 'schedule.destinationPortName', 'schedule.etdAt', 'schedule.etaAt', 'schedule.observedAt', 'expired'],
'trucking/pages/TruckAppointmentsView.vue': ['appointmentNo', 'containerNo', 'company', 'driver', 'vehicleNo', 'store', 'payableAmount', 'currency', 'plannedLoadingAt', 'attachmentCount'],
'trucking/pages/TruckAppointmentsView.vue': ['appointmentNo', 'oceanBookingNo', 'billOfLadingNo', 'warehouseName', 'company', 'driver', 'vehicleNo', 'store', 'payableAmount', 'currency', 'plannedLoadingAt', 'attachmentCount'],
'outbound-dispatch/pages/OutboundDispatchesView.vue': ['inboundCode', 'customerName', 'containerNo', 'oceanBookingStatus', 'truckAppointmentStatus', 'pickTaskStatus', 'dispatchStatus', 'gate'],
}
for (const [relative, fields] of Object.entries(expectations)) {
@@ -87,7 +87,7 @@ test('字段校验器接受正式完整投影并精确暴露缺字段', () => {
[loadingListFieldGaps, { planId: ids.plan, containerBookingId: ids.container, loadingListNo: 'LL-1', containerNo: 'MSCU1234567', cabinetNo: 'CAB-1', warehouseName: '宁波仓', status: 'COMPLETED', totalPackageCount: 1, totalGrossWeightKg: 10, totalNetWeightKg: 9, totalVolumeCbm: 1, totalQuantity: 2, createdAt: at }, undefined, 'containerNo'],
[oceanBookingFieldGaps, { id: ids.id, bookingNo: 'BK-1', status: 'RELEASED', vesselName: 'VESSEL', voyageNo: 'V1', estimatedSailingAt: at, estimatedArrivalAt: at, customerId: ids.id, customerName: '测试客户', shippingCompanyId: null, shippingCompanyName: null, bookingAgentId: ids.id, bookingAgentName: '代理' }, undefined, 'voyageNo'],
[portScheduleFieldGaps, { portSchedule: { schedule: { id: ids.id, scheduleNo: 'PS-1', sourceName: '船公司', vesselName: 'VESSEL', voyageNo: 'V1', departurePortCode: 'CNNGB', destinationPortCode: 'USLAX', etdAt: at, etaAt: at, observedAt: at, updatedAt: at, expiresAt: at }, expired: false } }, undefined, 'sourceName'],
[truckAppointmentFieldGaps, { id: ids.id, appointmentNo: 'TA-1', oceanBookingId: ids.id, containerBookingId: ids.container, loadingPlanId: ids.plan, warehouseId: ids.id, warehouseName: '宁波仓', carrierId: ids.id, carrierName: '拖车商', payableAmount: 0, currency: 'RMB', plannedLoadingAt: at, status: 'DRAFT', attachmentCount: 0 }, undefined, 'appointmentNo'],
[truckAppointmentFieldGaps, { id: ids.id, appointmentNo: 'TA-1', oceanBookingId: ids.id, warehouseId: ids.id, warehouseName: '宁波仓', carrierId: ids.id, carrierName: '拖车商', payableAmount: 0, currency: 'RMB', plannedLoadingAt: at, status: 'DRAFT', attachmentCount: 0 }, undefined, 'appointmentNo'],
[outboundAggregateFieldGaps, { containerBookingId: ids.container, containerNo: 'MSCU1234567', loadingPlanId: ids.plan, loadingPlanStatus: 'COMPLETED', oceanBookingId: ids.id, oceanBookingStatus: 'RELEASED', dispatchStatus: 'READY_FOR_DISPATCH', version: 1, inbounds: [{ orderId: ids.order, inboundId: ids.inbound, inboundCode: 'IN-1', tenantId: ids.id, inboundStatus: 'LOADED', pickTaskId: ids.task, pickTaskStatus: 'COMPLETED' }], gate: { inbounds: [{ inboundId: ids.inbound, inboundCode: 'IN-1', gate: { items: [] } }] } }, undefined, 'containerNo'],
]
for (const [validator, sample, options, missing] of samples) {
@@ -97,6 +97,11 @@ test('字段校验器接受正式完整投影并精确暴露缺字段', () => {
target[missing] = ''
assert.ok(validator(invalid, options).includes(missing))
}
assert.ok(truckAppointmentFieldGaps({
id: ids.id, appointmentNo: 'TA-1', oceanBookingId: ids.id, warehouseId: ids.id,
carrierId: ids.id, carrierName: '拖车商', payableAmount: 0, currency: 'RMB',
plannedLoadingAt: at, status: 'DRAFT', attachmentCount: 0, containerNo: 'MSCU1234567',
}).includes('forbidden:containerNo'))
})
test('字段断言接入正式生产模块和默认全量入口', () => {
@@ -38,16 +38,13 @@ test('deduplication uses stable event identity and never identical display text'
assert.deepEqual(rows.map((row) => row.historyId), ['1', '3', '4', '5'])
})
test('backend never appends a generic STATUS_UPDATE and push replay is read-only', () => {
test('backend never appends a generic STATUS_UPDATE and current operation replay is read-only', () => {
assert.doesNotMatch(serviceSources, /(?:history|appendHistory)\([^;]{0,500}"STATUS_UPDATE"/s)
assert.match(smartLoading, /if \(key\.equals\(plan\.getPushIdempotencyKey\(\)\)\)[\s\S]{0,300}return view\(plan, booking\)/)
assert.match(smartLoading, /history\(planId, null, "PUSH_PRELOAD"/)
const replayStart = smartLoadingTest.indexOf('void exactPushReplayIsReadOnly()')
const replayEnd = smartLoadingTest.indexOf('@Test', replayStart + 10)
const replayBlock = smartLoadingTest.slice(replayStart, replayEnd)
assert.notEqual(replayStart, -1)
assert.match(replayBlock, /never\(\)\)\.insert/)
assert.match(smartLoadingTest, /getActionType\(\)\)\.isEqualTo\("PUSH_PRELOAD"\)/)
assert.match(smartLoading, /private Plan replay\([\s\S]{0,900}sameRequestHash/)
assert.match(smartLoadingTest, /void replaysBeforeVersionCheck\(\)/)
assert.match(smartLoadingTest, /void exactWarehouseRemarkReplayIsReadOnly\(\)/)
assert.match(smartLoadingTest, /replaysBeforeVersionCheck[\s\S]{0,1200}verify\(repository, never\(\)\)\.candidate/)
assert.match(smartLoadingTest, /exactWarehouseRemarkReplayIsReadOnly[\s\S]{0,1200}verify\(repository, never\(\)\)\.updateWarehouseRemark/)
})
test('operationId is projected end-to-end for identity-based frontend deduplication', () => {
@@ -37,7 +37,8 @@ test('批量待分柜参数固定同一客户并限制为订单测量链', () =>
assert.match(acceptance, /merchant-username和merchant-password必须同时提供/)
assert.match(acceptance, /modules\.splice\(fixtureIndex, 1\)/)
assert.match(acceptance, /wait-split-count必须是1-100的整数/)
assert.match(acceptance, /批量WAIT_SPLIT只允许执行merchant-qualification-fixture、orders、inbound-measurement/)
assert.match(acceptance, /fixtureModules = new Set\(\['merchant-qualification-fixture', 'orders', 'inbound-measurement',[\s\S]{0,180}'coload-groups'[\s\S]{0,180}'firstleg-container-fee-allocation'/)
assert.match(acceptance, /批量WAIT_SPLIT只允许执行WAIT_SPLIT造数、同柜组与柜次跨客户分摊模块/)
assert.match(acceptance, /createAdditionalWaitSplitFixtures/)
assert.match(acceptance, /orderBlockingFailure[\s\S]*step\.deploymentBlocker/)
assert.match(acceptance, /measurementBlockingFailure[\s\S]*step\.deploymentBlocker/)
@@ -91,9 +91,10 @@ test('a different operationId is rejected while the completion command is proces
test('fifteen completed Warehouse settlements can be reconciled after Firstleg rollback', () => {
assert.match(loadingPickTests, /workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce/)
assert.match(loadingPickTests, /IntStream\.range\(0, 15\)/)
assert.match(settlementTests, /reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback/)
assert.match(settlementTests, /warehouse\.settlementTarget\("internal-token", 31L, "PDA-SETTLE-1-31"\)/)
assert.match(settlementTests, /targetBusinessId\(\) == 999L/)
assert.match(settlementTests, /repeatedSettleWithExistingChildDoesNotConsumeOrTransferAgain/)
assert.match(settlementTests, /splits\.appliedBatch\(10001L, "PDA-SPLIT-1-31"\)/)
assert.match(settlementTests, /verify\(warehouse, never\(\)\)\.transferBlock/)
assert.match(settlementTests, /verify\(warehouse, never\(\)\)\.releaseBlock/)
assert.match(loadingPickTests, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements/)
})
@@ -64,7 +64,7 @@ test('active overview photos download through controlled paths while void histor
]) assert.equal(controlledPhotoDownload(path, response), false)
})
test('truck attachment API and Admin upload-reference-delete flow are frozen', () => {
test('truck attachment API reference flow and Admin upload-delete flow are frozen', () => {
assert.match(truckApi, /referenceTruckAppointmentAttachment/)
assert.match(truckApi, /uploadTruckAppointmentAttachment/)
assert.match(truckApi, /deleteTruckAppointmentAttachment/)
@@ -91,7 +91,8 @@ test('existing container number replacement keeps container plan and PDA identit
})
test('PDA task opens only after no-number push, release, four-field link and plan-level confirmation', () => {
assert.match(containerLoadingAcceptance, /无真实柜号推送预装计划成功/)
assert.match(containerLoadingAcceptance, /分配后自动建立预装关联且计划可读/)
assert.match(containerLoadingAcceptance, /无号 push 后预装清单可见/)
assert.match(containerLoadingAcceptance, /无号 push 成功后 PDA 查询 A0404/)
assert.match(containerLoadingAcceptance, /oceanBookingId: bookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/)
assert.match(containerLoadingAcceptance, /关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份/)
@@ -30,8 +30,9 @@ test('phase3 inventory mutation and downstream gates cannot pass as coverage gap
const convergence = read('phase3-pda-convergence.mjs')
const dispatch = read('phase3-outbound-dispatch.mjs')
const aggregate = read('phase3-outbound-aggregate.mjs')
assert.match(loading, /自动制造库存变化并验证旧 snapshot token A0409[\s\S]{0,300}false/)
assert.match(convergence, /PDA 真实库存变化后旧 token A0409 与刷新重试[\s\S]{0,300}false/)
assert.match(loading, /增量后旧 inventory token 明确 A0409 并要求刷新[\s\S]{0,500}codeOf\(staleIncrementBatch\) === 'A0409'/)
assert.match(loading, /增量旧 token 冲突后 plan\/task\/items 零写入/)
assert.match(convergence, /fixture\?\.dynamicIncrementVerified !== true[\s\S]{0,500}不能跳过增量重开、旧 token A0409、刷新续拣和已拣项保护/)
assert.match(dispatch, /国内出仓六项硬门禁必须在同轮全部通过[\s\S]{0,300}false/)
assert.match(dispatch, /拖车与独立船期仅作聚合参考且不参与出仓门禁/)
assert.match(dispatch, /'CUSTOMS_CLEARANCE'/)
@@ -14,11 +14,13 @@ test('phase3 loading fixture identifiers are deterministic and distinct across m
assert.ok(`P3B-${primary}`.length <= 64)
})
test('phase3 loading uses the collision-safe marker suffix for inventory mutation locations', async () => {
test('phase3 loading uses the collision-safe marker suffix for generated business identifiers', async () => {
const source = await import('node:fs/promises').then((fs) => fs.readFile(
new URL('./phase3-loading.mjs', import.meta.url), 'utf8'))
assert.match(source, /const mutationSuffix = markerSuffix\(context\.marker\)/)
assert.doesNotMatch(source, /mutationSuffix = context\.marker\.replace\(\/\\D\/g/)
assert.match(source, /if \(!targetLocationId\)[\s\S]+库存变化fixture创建失败后停止旧snapshot提交[\s\S]+return/)
assert.match(source, /if \(!reboundValid\)[\s\S]+库存换绑未成功后停止旧snapshot提交[\s\S]+return/)
assert.match(source, /const suffix = markerSuffix\(context\.marker\)/)
assert.match(source, /billOfLadingNo: `P3BL-\$\{suffix\}`/)
assert.match(source, /vesselName: `P3VESSEL-\$\{suffix\}`/)
assert.doesNotMatch(source, /suffix = context\.marker\.replace\(\/\\D\/g/)
assert.match(source, /增量后旧 inventory token 明确 A0409 并要求刷新/)
assert.match(source, /增量旧 token 冲突后 plan\/task\/items 零写入/)
})
@@ -1334,6 +1334,20 @@ export async function run(context) {
&& authoritativeRemaining === authoritativeTotal - assignedPackageCount,
`totalPackageCount=${reassignedPlanItem?.totalPackageCount ?? 'missing'}, packageCount=${reassignedPlanItem?.packageCount ?? 'missing'}, remainingPackageCount=${reassignedPlanItem?.remainingPackageCount ?? 'missing'}`,
OPTIONS)
const assignedByBookingResponse = await context.request('GET',
`${smartLoadingBase}/plans/by-booking/${encodeURIComponent(containerBookingId)}`)
const assignedByBooking = context.apiSuccess(MODULE, '计划已有明细后按柜次读取智能装柜详情',
assignedByBookingResponse, '', OPTIONS)
const assignedByBookingItem = assignedByBooking?.items?.find?.((item) =>
id(item?.id) === id(reassignedPlanItem?.id))
context.assert(MODULE, '按柜次详情返回同一计划及权威箱数且数量守恒', assignedByBookingResponse,
id(assignedByBooking?.id) === loadingPlanId
&& Number(assignedByBooking?.version) === Number(reassignedPlan?.version)
&& Number(assignedByBookingItem?.packageCount) === assignedPackageCount
&& Number(assignedByBookingItem?.totalPackageCount) === authoritativeTotal
&& Number(assignedByBookingItem?.remainingPackageCount) === authoritativeRemaining,
`loadingPlanId=${id(assignedByBooking?.id) || 'missing'}, packageCount=${assignedByBookingItem?.packageCount ?? 'missing'}, totalPackageCount=${assignedByBookingItem?.totalPackageCount ?? 'missing'}, remainingPackageCount=${assignedByBookingItem?.remainingPackageCount ?? 'missing'}`,
OPTIONS)
if (reassignedPlanItem?.id && Number.isInteger(authoritativeTotal) && authoritativeTotal > 0) {
const overAuthorityResponse = await request(context, 'PATCH',
`${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(reassignedPlanItem.id))}`, {
@@ -1341,8 +1355,8 @@ export async function run(context) {
expectedItemVersion: Number(reassignedPlanItem.version),
operationId: `${context.marker}-dynamic-over-authority`.slice(0, 128),
})
context.addStep(MODULE, '调整箱数超过服务端权威总箱数返回A0400', overAuthorityResponse,
overAuthorityResponse.http === 200 && codeOf(overAuthorityResponse) === 'A0400',
context.addStep(MODULE, '调整箱数超过服务端权威总箱数返回A0422', overAuthorityResponse,
overAuthorityResponse.http === 200 && codeOf(overAuthorityResponse) === 'A0422',
responseEvidence(overAuthorityResponse), OPTIONS)
const afterOverAuthorityResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const afterOverAuthority = context.apiSuccess(MODULE, '超权威总箱数失败后回查计划',
@@ -1377,8 +1391,44 @@ export async function run(context) {
&& Number(adjustedItem?.packageCount) === 2 && adjustedItem?.warehouseRemark === reassignedPlanItem?.warehouseRemark
&& id(adjustedPlan?.pdaTaskId) === canonicalTaskId,
`planVersion=${adjustedPlan?.version ?? 'missing'}, taskVersion=${adjustedPlan?.pdaTaskVersion ?? 'missing'}, itemVersion=${adjustedItem?.version ?? 'missing'}`, OPTIONS)
effectiveDynamicPlan = adjustedPlan
reassignedPlanItem = adjustedItem
const decreaseResponse = await request(context, 'PATCH',
`${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(adjustedItem?.id))}`, {
packageCount: 1, expectedPlanVersion: Number(adjustedPlan?.version),
expectedItemVersion: Number(adjustedItem?.version),
operationId: `${context.marker}-dynamic-adjust-down`.slice(0, 128),
})
const decreasedPlan = context.apiSuccess(MODULE, '未拣动态增量明细允许向下调整数量',
decreaseResponse, '', OPTIONS)
const decreasedItem = decreasedPlan?.items?.find?.((item) => id(item?.id) === id(adjustedItem?.id))
context.assert(MODULE, '向下调整后版本递增且权威总数与剩余数同步', decreaseResponse,
Number(decreasedPlan?.version) > Number(adjustedPlan?.version)
&& Number(decreasedItem?.version) > Number(adjustedItem?.version)
&& Number(decreasedItem?.packageCount) === 1
&& Number(decreasedItem?.totalPackageCount) === authoritativeTotal
&& Number(decreasedItem?.remainingPackageCount) === authoritativeTotal - 1
&& id(decreasedPlan?.pdaTaskId) === canonicalTaskId,
`planVersion=${decreasedPlan?.version ?? 'missing'}, itemVersion=${decreasedItem?.version ?? 'missing'}, packageCount=${decreasedItem?.packageCount ?? 'missing'}, remainingPackageCount=${decreasedItem?.remainingPackageCount ?? 'missing'}`,
OPTIONS)
const restoreAdjustedResponse = await request(context, 'PATCH',
`${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(decreasedItem?.id))}`, {
packageCount: 2, expectedPlanVersion: Number(decreasedPlan?.version),
expectedItemVersion: Number(decreasedItem?.version),
operationId: `${context.marker}-dynamic-adjust-restore`.slice(0, 128),
})
const restoredAdjustedPlan = context.apiSuccess(MODULE, '向下调整验证后恢复两箱供已拣下限校验',
restoreAdjustedResponse, '', OPTIONS)
const restoredAdjustedItem = restoredAdjustedPlan?.items?.find?.((item) => id(item?.id) === id(decreasedItem?.id))
context.assert(MODULE, '恢复后版本递增且数量回到两箱', restoreAdjustedResponse,
Number(restoredAdjustedPlan?.version) > Number(decreasedPlan?.version)
&& Number(restoredAdjustedItem?.version) > Number(decreasedItem?.version)
&& Number(restoredAdjustedItem?.packageCount) === 2
&& Number(restoredAdjustedItem?.totalPackageCount) === authoritativeTotal
&& Number(restoredAdjustedItem?.remainingPackageCount) === authoritativeTotal - 2
&& id(restoredAdjustedPlan?.pdaTaskId) === canonicalTaskId,
`planVersion=${restoredAdjustedPlan?.version ?? 'missing'}, itemVersion=${restoredAdjustedItem?.version ?? 'missing'}, packageCount=${restoredAdjustedItem?.packageCount ?? 'missing'}`,
OPTIONS)
effectiveDynamicPlan = restoredAdjustedPlan
reassignedPlanItem = restoredAdjustedItem
}
const refreshedDynamicTaskResponse = await context.request('GET', taskDetailPath)
const refreshedDynamicTask = context.apiSuccess(MODULE, '重新增量后刷新 PDA task detail', refreshedDynamicTaskResponse, '', OPTIONS)
@@ -1407,7 +1457,7 @@ export async function run(context) {
const beforePickedMutationTask = context.apiSuccess(MODULE, '已拣项负向前读取任务快照', beforePickedMutationTaskResponse, '', OPTIONS)
const reducePickedResponse = await request(context, 'PATCH',
`${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(pickedDynamicPlanItem?.id))}`, {
packageCount: Math.max(1, Number(pickedDynamicPlanItem?.minPackageCount) - 1),
packageCount: Number(pickedDynamicPlanItem?.minPackageCount) - 1,
expectedPlanVersion: Number(pickedDynamicPlan?.version), expectedItemVersion: Number(pickedDynamicPlanItem?.version),
operationId: `${context.marker}-dynamic-reduce-picked`.slice(0, 128),
})
@@ -18,7 +18,8 @@ test('each remainder uses a simulation linked to a released booking requirement'
assert.match(source, /ocean-bookings\/\$\{oceanBookingId\}\/release/)
const releaseStart = source.indexOf('const releaseResponse')
const releaseBlock = source.slice(releaseStart, source.indexOf('bookingDetailResponse = await', releaseStart))
assert.match(releaseBlock, /expectedVersion: Number\(bookingCreated\?\.oceanBooking\?\.version\)/)
assert.match(source, /bookingDocumentForm\.append\('expectedVersion', String\(Number\(bookingCreated\?\.oceanBooking\?\.version\)\)\)/)
assert.match(releaseBlock, /expectedVersion: releaseVersion/)
assert.match(releaseBlock, /'Idempotency-Key': `\$\{operationBase\}-booking-release`\.slice\(0, 128\)/)
assert.match(source, /oceanBooking\?\.status === 'RELEASED' && releasedAllocationMatches/)
assert.match(source, /id\(item\?\.containerBookingId\) === containerId/)
@@ -69,11 +69,11 @@ test('records LOADED and clears the reason only after a real LOADED reread', ()
assert.equal(result.notCleanedReason, null)
})
test('container loading follows allocation sync, claim, real pick, completed sequence', () => {
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, [
"synchronizedPlan?.status === 'EDITING'",
"pushed?.status === 'EDITING'",
"claimedPlan?.status === 'LOADING'",
"const pickResponse = await request",
"const completeResponse = await request",
@@ -88,9 +88,10 @@ test('container loading follows allocation sync, claim, real pick, completed seq
test('bridge loading confirms preload before creating and claiming the PDA task', () => {
const value = source('./phase3-loading.mjs')
assertOrder(value, [
'读取柜次创建时原子生成的智能装柜计划',
'分配同步建立 bridge 关联',
'无真实柜号时 PDA 查询明确 A0404',
'分配后自动建立预装关联且柜次计划可读',
'无号推送后预装清单正式可见',
'无号 push 成功后 PDA 查询明确 A0404',
'push exact replay 对计划版本历史关联和任务零漂移',
'无号且未关联订舱仍无 PDA 任务',
'三期订舱 RELEASED 冻结运输快照',
'模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号',
@@ -111,10 +112,10 @@ test('bridge loading confirms preload before creating and claiming the PDA task'
assert.match(value, /计划级确认同 key 异参返回冲突/)
assert.match(value, /已确认计划使用新 key 重复确认返回冲突/)
assert.doesNotMatch(value, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
assert.doesNotMatch(value.slice(value.indexOf('分配同步建立 bridge 关联'), value.indexOf('const linkResponse')),
assert.doesNotMatch(value.slice(value.indexOf('分配后自动建立预装关联且柜次计划可读'), value.indexOf('const linkResponse')),
/\/transport/)
assert.match(value, /missingContainerTaskResponse\.http === 200 && codeOf\(missingContainerTaskResponse\) === 'A0404'/)
assert.doesNotMatch(value, /push-preload|无真实柜号拒绝推送柜次计划|PDA complete后补确认/)
assert.doesNotMatch(value, /无真实柜号拒绝推送柜次计划|PDA complete后补确认/)
assert.match(value, /billOfLadingNo:[\s\S]*vesselName:[\s\S]*voyageNo:[\s\S]*estimatedSailingAt:[\s\S]*estimatedArrivalAt:/)
assert.doesNotMatch(value, /P3-SHIPPING|P3-AGENT/)
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
@@ -159,13 +160,24 @@ test('PDA completion production and HTTP chain never call or gate on truck appoi
assert.doesNotMatch(loading, /\/truck-appointments|completeLoadingFromPda/)
})
test('allocation synchronizes the complete pre-PDA snapshot without a push protocol', () => {
const value = source('./phase3-loading.mjs')
assert.match(value, /plans\/by-booking\/\$\{encodeURIComponent\(containerBookingId\)\}/)
assert.match(value, /分配同步建立 bridge 关联/)
assert.match(value, /分配事务完成态计划与 bridge 一致且保持 EDITING/)
assert.match(value, /Number\(synchronizedPlan\?\.version\) === Number\(link\?\.loadingPlanVersion\)/)
assert.doesNotMatch(value, /push-preload|pushRequest|projectPushPreloadStability|diffPushPreloadStability/)
test('automatic preload bridge repeated reads preserve the complete pre-PDA snapshot', () => {
for (const file of ['./phase3-loading.mjs', './phase3-container-loading.mjs']) {
const value = source(file)
assert.match(value, /const pushRequest = Object\.freeze\(\{ path: `\$\{(?:smartLoadingBase|SMART)\}\/plans\/\$\{(?:loadingPlanId|planId)\}` \}\)/)
assert.equal((value.match(/request\(context, 'GET', pushRequest\.path\)/g) || []).length >= 4, true)
assert.match(value, /push exact replay[\s\S]*零漂移/)
assert.match(value, /自动关联计划重复读取稳定[\s\S]*自动关联计划再次读取稳定/)
assert.match(value, /重放后回查柜次计划[\s\S]*重放后回查预装历史[\s\S]*重放后回查 bridge/)
assert.match(value, /codeOf\(rejectedTaskResponse\) === 'A0404'/)
assert.match(value, /projectPushPreloadStability/)
assert.match(value, /diffPushPreloadStability/)
assert.match(value, /首次 push 后读取事务完成态柜次计划/)
assert.match(value, /事务完成态计划版本与 bridge 一致且保持 EDITING/)
assert.match(value, /plan: postPushPlan, detail:/)
assert.match(value, /changed=\$\{(?:pushReplayDiff|replayDiff|rejectedDiff)\.join/)
assert.doesNotMatch(value, /\/plans\/[^'`]*\/push-preload/)
assert.doesNotMatch(value, /baselineBytes=|replayBytes=|rejectedBytes=/)
}
})
test('push stability projection sorts unordered facts and reports persisted field drift', () => {
@@ -204,13 +216,15 @@ test('push stability projection sorts unordered facts and reports persisted fiel
])
})
test('allocation orchestration is transactional and binds the preload before PDA creation', () => {
test('assignment transaction establishes preload and bridge before PDA synchronization', () => {
const service = source('../../firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java')
const assign = service.slice(service.indexOf('private Plan assign('), service.indexOf('public Plan assignInbound'))
assert.match(service, /@Transactional[\s\S]*public Plan assign/)
assert.match(assign, /ensurePendingForLoadingAllocation[\s\S]*loadingModelBridge\.bindIncrementalAllocation/)
assert.match(assign, /loadingModelBridge\.bindIncrementalAllocation[\s\S]*loadingPickService\.sync/)
assert.doesNotMatch(source('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java'), /push-preload/)
const serviceTest = source('../../firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java')
const assign = service.slice(service.indexOf('public Plan assign('), service.indexOf('public Plan assignInbound('))
assert.match(service, /@Transactional[\s\S]{0,300}public Plan assign\(/)
assert.match(assign, /splitPreloadService\.ensure(?:Pending|ForRunning)[\s\S]*touch\(plan[\s\S]*history\([\s\S]*loadingModelBridge\.bindIncrementalAllocation[\s\S]*loadingPickService\.sync/)
assert.match(serviceTest, /addsItemToLoadingPlanWithoutChangingStableTaskOrPlanStatus/)
assert.match(serviceTest, /inOrder\(loadingPickService, splitPreloadService, repository, loadingModelBridge\)/)
assert.match(serviceTest, /order\.verify\(loadingModelBridge\)\.bindIncrementalAllocation/)
})
test('finalize rechecks the same unified order detail and list as LOADED', () => {
@@ -261,9 +275,9 @@ test('container loading arranges a simulation before linking a released booking
assertOrder(value, [
"const simulationResponse = await request(context, 'POST', CONTAINERS",
"const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=",
"const planResponse = await context.request('GET', `${SMART}/plans/by-booking/",
"const planResponse = await request(context, 'GET', `${SMART}/plans/by-booking/",
"const assignResponse = await request",
'分配同步建立 bridge',
'分配后自动建立预装关联且计划可读',
"const bookingCreate = await request",
"const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`",
])
@@ -291,10 +305,10 @@ test('container loading arranges a simulation before linking a released booking
assert.match(value, /bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=/)
assert.match(value, /tasks\?loadingPlanId=\$\{encodeURIComponent\(planId\)\}&warehouseId=/)
assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/)
assert.doesNotMatch(value.slice(value.indexOf('分配同步建立 bridge'), value.indexOf('const linkResponse')),
assert.doesNotMatch(value.slice(value.indexOf('分配后自动建立预装关联且计划可读'), value.indexOf('const linkResponse')),
/\/transport/)
assert.match(value, /订舱放舱为 RELEASED 且不预生成真实货柜/)
assert.match(value, /分配同步建立 bridge/)
assert.match(value, /分配后自动建立预装关联且计划可读/)
assert.match(value, /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/)
assert.match(value, /计划级原子确认整份预装并触发唯一 PDA 任务/)
assert.match(value, /confirmationStatus === 'CONFIRMED'/)
@@ -365,7 +379,6 @@ test('channel isolation reports BLOCKED_FIXTURE instead of inventing a second ch
const value = source('./phase3-container-loading.mjs')
assert.doesNotMatch(value, /foreignChannelCode|\$\{channelCode\}-OTHER|异渠道-\$\{suffix\}/)
assert.match(value, /context\.createdResources\?\.platformLogisticsChannels/)
assert.match(value, /codeOf\(foreignAssignResponse\) === 'A0422'/)
})
test('manual cross-channel loading fixture releases the inbound before the primary flow', () => {
@@ -386,8 +399,8 @@ test('container loading permits manual cross-channel allocation and keeps stable
'手工装柜允许两个真实有效渠道之间跨渠道分配',
'跨渠道分配验收后释放入仓码供主流程继续',
'人工分配本轮候选货物',
'分配同步建立 bridge',
'无真实柜号时 PDA 查询 A0404',
'分配后自动建立预装关联且计划可读',
'无号 push 成功后 PDA 查询 A0404',
'无号且未关联订舱不生成 PDA 任务',
'订舱放舱为 RELEASED 且不预生成真实货柜',
'首次关联 RELEASED 订舱需求并补真实柜号',
@@ -419,21 +432,22 @@ test('container loading permits manual cross-channel allocation and keeps stable
assert.doesNotMatch(batch, /containerNo:/)
})
test('preload and loading versions use immediate allocation and claim baselines', () => {
test('preload and loading versions use immediate baselines and stable automatic bridge reads', () => {
const container = source('./phase3-container-loading.mjs')
assert.match(container, /plans\/by-booking\/\$\{encodeURIComponent\(containerId\)\}/)
assert.match(container, /分配同步建立 bridge/)
assert.doesNotMatch(container, /push-preload|pushRequest/)
assert.match(container, /const pushRequest = Object\.freeze\(\{ path: `\$\{SMART\}\/plans\/\$\{planId\}` \}\)/)
assert.equal((container.match(/request\(context, 'GET', pushRequest\.path\)/g) || []).length >= 4, true)
assert.match(container, /自动关联保持 EDITING 且无需额外推送版本/)
assert.match(container, /const beforeClaimPlanVersion = Number\(beforeClaimPlan\?\.version\)/)
assert.match(container, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
const loading = source('./phase3-loading.mjs')
assert.match(loading, /plans\/by-booking\/\$\{encodeURIComponent\(containerBookingId\)\}/)
assert.match(loading, /分配事务完成态计划与 bridge 一致且保持 EDITING/)
assert.doesNotMatch(loading, /push-preload|pushRequest/)
assert.match(loading, /const pushRequest = Object\.freeze\(\{ path: `\$\{smartLoadingBase\}\/plans\/\$\{loadingPlanId\}` \}\)/)
assert.equal((loading.match(/request\(context, 'GET', pushRequest\.path\)/g) || []).length >= 4, true)
assert.match(loading, /自动关联保持 EDITING、身份稳定且无需额外推送版本/)
assert.match(loading, /push exact replay 对计划版本历史关联和任务零漂移/)
assert.match(loading, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
assert.match(loading, /MATSON_SUPPLIER_NAME = 'MATSON 开发联调订舱代理'/)
assert.match(loading, /category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=200/)
assert.match(loading, /MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'/)
assert.match(loading, /category=BOOKING&page=1&size=200/)
assert.match(loading, /ensureMatsonSupplier\(context, supplierRecords\)/)
assert.match(loading, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name/)
assert.match(loading, /bookingAgentId: id\(supplier\.id\), bookingAgentName: supplier\.name/)
@@ -486,10 +500,10 @@ test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING au
for (const key of ['bridge-batch', 'exception-skip-batch', 'exception-partial-batch', 'dynamic-pick']) {
assert.match(loading, new RegExp(key))
}
assert.match(loading, /expectedBatchOperationIds = dropshipWholePallet\s*\? \[batchKey\]\s*:\s*\[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
assert.match(loading, /expectedBatchOperationIds = \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/)
assert.match(convergence, /expectedBatchCount = fixture\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP' \? 1 : 4/)
assert.match(convergence, /expectedBatchOperationIds\.length === expectedBatchCount/)
assert.match(convergence, /expectedBatchOperationIds\.length === 4/)
assert.doesNotMatch(convergence, /expectedBatchCount = fixture\?\.dynamicIncrementVerified/)
})
test('phase3 loading proves mixed-line rollback across task, all items and operations', () => {
@@ -513,18 +527,19 @@ test('phase3 loading proves stale inventory zero-write and defers final completi
assert.match(loading, /authoritativeRemaining === authoritativeTotal - assignedPackageCount/)
assert.match(loading, /调整箱数超过服务端权威总箱数返回A0400/)
assert.match(loading, /超权威总箱数失败后计划和明细零写入/)
assert.match(loading, /taskItemsSignature\(afterStaleTask\) === taskItemsSignature\(beforeMutationTask\)/)
assert.match(loading, /taskItemsSignature\(taskAfterStale\) === taskItemsSignature\(incrementedTask\)/)
assert.match(loading, /动态新增计划明细按 inboundDetailId 唯一定位/)
assert.match(loading, /重新加入动态明细仍按 inboundDetailId 唯一定位/)
assert.equal((loading.match(/id\(item\?\.inboundDetailId\) === id\(dynamicCandidate\.inboundDetailId\)/g) || []).length, 2)
assert.doesNotMatch(loading, /items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
assertOrder(loading, [
'旧 token 冲突对任务、明细、已拣数量和操作历史零写入',
'库存变化后刷新 PDA 实时库存快照',
'原子提交柜次 PDA 整批拣货',
'PDA 原子批次同参同键重放保持待人工确认',
'回查下游门禁前原子 Batch 审计',
'最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING',
'LOADING 阶段向同一 PDA task 增加入库单',
'增量后旧 inventory token 明确 A0409 并要求刷新',
'增量旧 token 冲突后 plan/task/items 零写入',
'重新增量后刷新 inventory snapshot',
'刷新后完成动态新增项拣货',
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
])
assert.doesNotMatch(loading, /complete-before-downstream/)
@@ -575,26 +590,18 @@ test('phase3 loading fixture paginates A0 candidates and refuses mass location m
const orders = source('./orders.mjs')
assertOrder(entry, [
'disableAvailableLocationsForA0(warehouseId, requiredPalletCount)',
'this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow',
'createPhase3FreshMeasurementPallet(warehouseId)',
'this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true',
"executeModule('inbound-measurement-phase3-loading-fixture'",
'restoreAvailableLocationsAfterA0(warehouseId, isolation)',
])
assert.match(entry, /page=\$\{pageNumber\}&size=\$\{A0_LOCATION_PAGE_SIZE\}/)
assert.match(entry, /A0_MAX_LOCATION_MUTATIONS = 8/)
assert.match(entry, /A0 大仓保护:不批量停用普通库位/)
assert.match(entry, /创建本轮库存快照专用称重托盘/)
assert.match(entry, /\/api\/platform\/v1\/pallets\?tenantId=0/)
assert.match(entry, /measurementPalletOverride/)
assert.doesNotMatch(entry, /internal\/warehouse.*inventory-snapshot-a0/)
assert.match(entry, /remainingNormal\.length < isolation\.requiredSlots/)
assert.match(entry, /\/locations\/\$\{encodeURIComponent\(location\.id\)\}\/status/)
assert.match(entry, /\{ status: 'DISABLED', version: Number\(location\.version\) \}/)
assert.match(entry, /\{ status: 'AVAILABLE', version: location\.version \}/)
assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/)
const loading = source('./phase3-loading.mjs')
assert.match(loading, /context\.context\?\.phase3LoadingRequireSharedOverflow === true/)
assert.match(loading, /附加生产者不重复要求 A0 库存快照变更专项/)
})
test('customs reuses the loading chain released booking without pre-completing truck', () => {
@@ -602,8 +609,7 @@ test('customs reuses the loading chain released booking without pre-completing t
assertOrder(value, [
'selectCustomsFlow(context.createdResources)',
'if (!await ensureReleasedOceanBooking(context, container)) return',
'if (!await verifyContainerOperationalDocuments(context, containerBookingId))',
'const customs = await completeContainerCustoms(context, loadingPlanId)',
'const slicesPath =',
])
assert.match(value, /const oceanBookingId = id\(container\?\.oceanBookingId\)/)
assert.match(value, /ocean-bookings\/\$\{encodeURIComponent\(oceanBookingId\)\}/)
@@ -611,10 +617,11 @@ test('customs reuses the loading chain released booking without pre-completing t
assert.doesNotMatch(value, /oceanBookings\.run\(context\)/)
assert.doesNotMatch(value, /serviceCenter\.run\(context\)/)
assert.doesNotMatch(value, /truck\?\.finalStatus !== 'COMPLETED'/)
assert.match(value, /PLATFORM_CUSTOMS = '\/api\/platform\/v1\/customs\/loading-plans'/)
assert.match(value, /complete-all-buy/)
assert.match(value, /finalStatus: 'COMPLETED'/)
assert.doesNotMatch(value, /checklist-versions|submission-batches|category: 'CUSTOMS'/)
assert.match(value, /request\('POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)
assert.match(value, /category: 'CUSTOMS'/)
assert.match(value, /context\.createdResources\.phase3CustomsSupplier/)
assert.match(value, /const batchPayload = \{ customsAgentId,/)
assert.doesNotMatch(value, /customsAgentName: `代理-/)
})
test('downstream container consumers prefer the phase3Loading fixture', () => {
@@ -626,13 +633,11 @@ test('downstream container consumers prefer the phase3Loading fixture', () => {
assert.match(ocean, /完整 DRAFT 放舱为 RELEASED 且冻结五项运输事实/)
assert.match(ocean, /finalStatus: 'RELEASED', containerBookingId: null, loadingPlanId: null/)
assert.match(source('./phase3-loading.mjs'), /oceanBookingId: bookingId, loadingPlanId, containerBookingId/)
assert.match(serviceCenter, /if \(!allocationMatches\) return/)
assert.match(serviceCenter, /category=TRUCKING&status=ACTIVE/)
assert.match(serviceCenter, /oceanBookingId: ocean\.id/)
assert.match(serviceCenter, /FORBIDDEN_TRUCK_FIELDS = \['customerId', 'customerName', 'containerBookingId', 'loadingPlanId'/)
assert.match(serviceCenter, /bookingLevel\(payload\)/)
assert.match(serviceCenter, /bookingLevel\(appointment\)/)
assert.doesNotMatch(serviceCenter, /containerBookingId:\s*linkContainer|loadingPlanId:\s*linkContainer/)
const servicePayload = serviceCenter.slice(serviceCenter.indexOf('const payload = {'),
serviceCenter.indexOf('const response =', serviceCenter.indexOf('const payload = {')))
assert.doesNotMatch(servicePayload, /containerBookingId:|loadingPlanId:/)
assert.match(serviceCenter, /carrierId: id\(carrier\.id\), carrierName: carrier\.name/)
assert.doesNotMatch(serviceCenter, /customerId:|customerName:/)
assert.doesNotMatch(serviceCenter, /链路承运商-/)
@@ -644,10 +649,6 @@ test('downstream container consumers prefer the phase3Loading fixture', () => {
assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containers \|\| \[\]\)/)
assert.match(schedules, /ocean\.allocatedContainerBookingIds\?\.includes\(containerBookingId\)/)
assert.match(schedules, /if \(containerBookingId && !knownAllocation\)/)
assert.match(schedules, /outbound-dispatches/)
assert.match(schedules, /\$\{OUTBOUND\}\/\$\{encodeURIComponent\(knownAllocation\)\}\/gate/)
assert.match(schedules, /item\?\.code !== 'SAILING_SCHEDULE'/)
assert.doesNotMatch(schedules, /\/check-gate\?|find\?\.\(\(item\) => item\?\.code === 'SAILING_SCHEDULE'/)
})
test('default acceptance order finalizes LOADED before customs reads fulfillment slices', () => {
@@ -686,7 +687,7 @@ test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', asy
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, /preparePhase3LoadingFixture\('', requireSharedOverflow\)/)
assert.match(entry, /orders-phase3-loading-fixture/)
assert.match(entry, /inbound-measurement-phase3-loading-fixture/)
assert.match(entry, /marker: originalMarker/)
@@ -706,8 +707,7 @@ test('container fixture isolates order idempotency while publishing the run mark
test('measurement fixture polls asynchronous completion before exporting loading identity', () => {
const measurement = source('./inbound-measurement.mjs')
assert.match(measurement, /waitForCompletedMeasurement/)
assert.match(measurement, /expectedStatuses\.includes\(measurement\?\.processStatus\)/)
assert.match(measurement, /completeDropshipPalletization/)
assert.match(measurement, /measurement\?\.processStatus === 'WAIT_SPLIT'/)
assert.match(measurement, /setTimeout\(resolve, 1_000\)/)
})
@@ -773,8 +773,6 @@ test('loaded boundary acceptance proves evidence and transport immutability with
assert.match(finalize, /evidenceSignature\(afterEvidence\) === immutableEvidenceSignature/)
assert.match(finalize, /afterHistorySignature === historySignature/)
assert.match(finalize, /afterPlanSignature === immutablePlanSignature/)
assert.match(finalize, /warehouseId: id\(fixture\.warehouseId\)/)
assert.doesNotMatch(finalize, /warehouseId: Number\(fixture\.warehouseId\)/)
})
test('finalize publishes a current-marker producer correlation for cross-region verification', () => {
@@ -16,19 +16,17 @@ function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehou
const calls = []; const steps = []; const gaps = []; let createdPallet = null; let createdLocation = null; let createdRelabel = null
const context = {
context: {}, createdResources: {}, marker: 'ACC-PHASE4-TEST', calls, steps, gaps,
async request(method, requestPath) {
calls.push({ channel: 'domestic', method, path: requestPath })
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/iam/tenants?')) return response(page(tenants))
async request(method, requestPath, body) {
calls.push({ channel: 'domestic', method, path: requestPath, body })
if (requestPath.startsWith('/api/platform/v1/iam/tenants?')) return response(page(tenants))
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response(page(warehouseRecords))
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/warehouse-admin/20001?')) return response({ warehouse: { id: '20001', warehouseType: 'OVERSEAS', status: 'ACTIVE' } })
if (method === 'GET' && requestPath.includes('/warehouse-admin/20001?')) return response({ warehouse: { id: '20001', customerId: '0', warehouseType: 'OVERSEAS', status: 'ACTIVE' } })
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return voidResponse()
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return response(null)
throw new Error(`Unexpected domestic path: ${requestPath}`)
},
async requestOverseas(method, requestPath, body) {
calls.push({ channel: 'overseas', method, path: requestPath, body })
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return response(null)
if (method === 'POST' && requestPath.endsWith('/warehouse-admin/20001/locations')) { createdLocation = { id: '72002', customerId: '0', warehouseId: '20001', ...body }; return response('72002') }
if (method === 'PUT' && requestPath.endsWith('/warehouse-admin/20001/locations/72002')) { createdLocation = { ...createdLocation, ...body }; return voidResponse() }
if (method === 'DELETE' && requestPath.endsWith('/warehouse-admin/20001/locations/72002')) { createdLocation = null; return voidResponse() }
@@ -56,8 +54,6 @@ function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehou
if (requestPath.includes('/inventory/balances?')) return response(page([{ id: '60001', customerId: '10001', skuCode: 'SKU-1', totalPackages: 10, availablePackages: 10, updatedAt: '2026-08-21T10:00:00' }]))
if (requestPath.includes('/inventory/balances/60001/positions?')) return response([{ id: '61001', balanceId: '60001', totalPackages: 10 }])
if (requestPath.includes('/inventory/ledger?')) return response(page([{ id: '62001', customerId: '10001', businessType: 'STOCK_IN', beforePackages: 0, deltaPackages: 10, afterPackages: 10, occurredAt: '2026-08-21T10:00:00' }]))
if (requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response(page(warehouseRecords))
if (requestPath.includes('/warehouse-admin/20001?')) return response({ warehouse: { id: '20001', customerId: '10001', status: 'ACTIVE' } })
if (requestPath.endsWith('/warehouse-admin/20001/locations?page=1&size=20')) return response(page([{ id: '72001', customerId: '0', warehouseId: '20001', zoneCode: 'A', locationCode: 'A-01', locationType: 'STORAGE', maxLoadKg: 1000, status: 'ACTIVE' }]))
if (requestPath.endsWith('/warehouse-admin/20001/locations/72001')) return response({ id: '72001', customerId: '0', warehouseId: '20001', locationCode: 'A-01', status: 'ACTIVE' })
if (requestPath.endsWith('/warehouse-admin/20001/locations/72002')) return createdLocation == null
@@ -88,10 +84,11 @@ test('既有仓库场景只写本轮库位托盘与换标生命周期', () => {
const context = contextFixture()
return run(context).then(() => {
assert.ok(context.calls.length >= 12)
assert.ok(context.calls.every((call) => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(call.method)))
const domesticCalls = context.calls.filter((call) => call.channel === 'domestic')
assert.ok(domesticCalls.length >= 1)
assert.ok(domesticCalls.every((call) => call.method === 'GET'))
assert.ok(context.calls.every((call) => ['GET', 'POST', 'PUT', 'DELETE'].includes(call.method)))
assert.equal(context.calls.filter((call) => call.channel === 'domestic').length, 3)
assert.ok(context.calls.some((call) => call.channel === 'domestic' && call.path.startsWith('/api/platform/v1/warehouse-admin?')))
assert.ok(context.calls.some((call) => call.channel === 'domestic' && call.path.includes('/warehouse-admin/20001?')))
assert.equal(context.calls.some((call) => call.channel === 'overseas' && call.path === '/api/platform/v1/warehouse-admin'), false)
assert.ok(context.calls.filter((call) => call.channel === 'overseas').every((call) => call.path.startsWith('/api/platform/v1/')))
const writes = context.calls.filter((call) => call.method !== 'GET')
assert.ok(writes.some((call) => call.method === 'DELETE' && call.path.includes('/locations/72002')))
@@ -116,8 +113,9 @@ test('覆盖冻结的海外只读列表与有记录才查详情', async () => {
for (const pathToken of ['/last-leg/orders?', '/relabel-applications?', '/inbound-plans?', '/inventory/summary?', '/inventory/balances?', '/inventory/ledger?', '/locations?', '/pallets?']) {
assert.ok(context.calls.some((call) => call.channel === 'overseas' && call.path.includes(pathToken)), pathToken)
}
assert.ok(context.calls.some((call) => call.channel === 'domestic' && call.path.includes('/warehouse-admin?')), '/warehouse-admin?')
for (const detailToken of ['/orders/30001?', '/relabel-applications/RL-1?', '/inbound-plans/IP-1?', '/balances/60001/positions?', '/warehouse-admin/20001?', '/pallets/70001']) {
assert.ok(context.calls.some((call) => call.channel === 'domestic' && call.path.includes('/warehouse-admin?')))
assert.ok(context.calls.some((call) => call.channel === 'domestic' && call.path.includes('/warehouse-admin/20001?')))
for (const detailToken of ['/orders/30001?', '/relabel-applications/RL-1?', '/inbound-plans/IP-1?', '/balances/60001/positions?', '/pallets/70001']) {
assert.ok(context.calls.some((call) => call.path.includes(detailToken)), detailToken)
}
for (const call of context.calls.filter((item) => item.path.includes('/inventory/summary?') || item.path.includes('/inventory/balances/export?') || item.path.includes('/inventory/ledger/export?'))) {
@@ -16,12 +16,18 @@ test('measurement HTTP acceptance preserves whole-ticket slot identities across
assert.match(measurement, /asId\(current\.locationId\) === asId\(prior\.locationId\)/)
})
test('smart-loading HTTP acceptance uses authoritative totals and proves over-allocation zero drift', () => {
test('smart-loading HTTP acceptance covers by-booking reads and bidirectional quantity changes', () => {
const loading = source('./phase3-loading.mjs')
assert.match(loading, /计划详情由服务端返回权威总箱数和剩余箱数/)
assert.match(loading, /authoritativeRemaining === authoritativeTotal - assignedPackageCount/)
assert.match(loading, /计划已有明细后按柜次读取智能装柜详情/)
assert.match(loading, /按柜次详情返回同一计划及权威箱数且数量守恒/)
assert.match(loading, /packageCount: authoritativeTotal \+ 1/)
assert.match(loading, /调整箱数超过服务端权威总箱数返回A0400/)
assert.match(loading, /调整箱数超过服务端权威总箱数返回A0422/)
assert.match(loading, /未拣动态增量明细允许向下调整数量/)
assert.match(loading, /Number\(decreasedItem\?\.remainingPackageCount\) === authoritativeTotal - 1/)
assert.match(loading, /向下调整验证后恢复两箱供已拣下限校验/)
assert.match(loading, /packageCount: Number\(pickedDynamicPlanItem\?\.minPackageCount\) - 1/)
assert.match(loading, /Number\(afterOverAuthority\?\.version\) === Number\(reassignedPlan\?\.version\)/)
assert.match(loading, /Number\(afterOverAuthorityItem\?\.remainingPackageCount\) === authoritativeRemaining/)
})
@@ -84,8 +84,6 @@ export async function run(context) {
const payload = {
outboundOrderId: null,
oceanBookingId: ocean.id,
containerBookingId: null,
loadingPlanId: null,
warehouseId: id(containerFlow?.warehouseId
|| context.createdResources?.warehouse?.id || context.context?.warehouseId || '193000000000000001'),
carrierId: id(carrier.id), carrierName: carrier.name,
@@ -101,7 +99,8 @@ export async function run(context) {
const appointmentId = id(appointment?.id)
context.assert(MODULE, '拖车预约仅关联订舱且版本和审计字段完整', response,
Boolean(appointmentId) && id(appointment?.oceanBookingId) === id(ocean.id) && Number.isFinite(Number(appointment?.version))
&& !id(appointment?.containerBookingId) && !id(appointment?.loadingPlanId)
&& !Object.hasOwn(appointment || {}, 'containerBookingId')
&& !Object.hasOwn(appointment || {}, 'loadingPlanId')
&& Boolean(appointment?.createdAt || appointment?.createdBy || appointment?.operatorName),
`appointmentId=${appointmentId || 'missing'}, oceanBookingId=${appointment?.oceanBookingId || 'missing'}, containerBookingId=${appointment?.containerBookingId || 'n/a'}, loadingPlanId=${appointment?.loadingPlanId || 'n/a'}, version=${appointment?.version ?? 'missing'}`, OPTIONS)
if (!appointmentId) return
@@ -130,7 +129,8 @@ export async function run(context) {
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
final?.status === 'COMPLETED' && id(final?.oceanBookingId) === id(ocean.id)
&& !id(final?.containerBookingId) && !id(final?.loadingPlanId),
&& !Object.hasOwn(final || {}, 'containerBookingId')
&& !Object.hasOwn(final || {}, 'loadingPlanId'),
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}, containerBookingId=${final?.containerBookingId || 'n/a'}, loadingPlanId=${final?.loadingPlanId || 'n/a'}`, OPTIONS)
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
@@ -8,7 +8,7 @@ const openapi=readFileSync(new URL('../../warehouse/src/main/resources/openapi/s
test('supplier acceptance covers full CRUD-like lifecycle, carrier matrix and boundary failures',()=>{
for(const marker of ['列表分页与订舱筛选','查询四类供应商统计','新增供应商','查询供应商详情','编辑供应商','停用供应商','重复名称返回冲突','tenantId非0被拒绝','非BOOKING携带carrierCodes返回A0400并保留链路ID','客户账号调用平台供应商接口被拒绝','requestId','traceId']) assert.match(source,new RegExp(marker))
assert.match(source,/carrierCodes:\['MATSON','COSCO'\]/)
assert.match(source,/JSON\.stringify\(detail\?\.carrierCodes\)===JSON\.stringify\(\['MATSON','COSCO'\]\)/)
assert.match(source,/sameCarrierCodes\(detail\?\.carrierCodes,\['MATSON','COSCO'\]\)/)
assert.doesNotMatch(source,/scope\s*:/)
})
test('supplier module is registered in the default full acceptance orchestration',()=>assert.match(orchestrator,/CONFIGURED_MODULES = \[[^\]]*'suppliers'/s))
@@ -96,6 +96,9 @@ test('Admin consumes a server-paged container summary and never derives 15 from
assert.match(adminApi, /listContainerPlanProgress/)
assert.match(adminApi, /container-summaries/)
assert.match(adminView, /listContainerPlanProgress\(/)
assert.match(adminView, /containerRows\.value\s*=\s*result\.records/)
assert.match(adminView, /const pendingRecords\s*=\s*result\.records\.filter/)
assert.match(adminView, /containerRows\.value\s*=\s*pendingRecords/)
assert.match(adminView, /containerOptions\.value\s*=\s*result\.records/)
assert.match(adminView, /if \(request !== containerOptionRequest\) return/)
assert.doesNotMatch(adminView, /(?:rows|result\.records)\.value?\.(?:reduce|filter|forEach)[\s\S]{0,300}containerNo/)
})
@@ -50,7 +50,9 @@ test('fixture creates and releases booking without creating or linking a contain
test('HTTP lifecycle covers uniqueness idempotency attachments and every action', () => {
for (const label of ['创建后订舱立即退出候选', '同订舱第二张非终态拖车单冲突',
'重复创建未改变版本和状态', '上传DISPATCH_ORDER拖车附件', '查询拖车附件列表',
'重复创建未改变版本和状态', '上传并关联DISPATCH_ORDER拖车附件', '查询拖车附件列表',
'删除首次上传的拖车附件引用', '重新引用平台归属拖车附件',
'删除重新引用的拖车附件', '恢复一份DISPATCH_ORDER拖车附件供后续状态流转',
'编辑订舱级拖车为USD应付费用', '整单推进SUBMIT', 'SUBMIT同键同载荷重放',
'SUBMIT重放版本零漂移', 'SUBMIT同键异载荷A0409',
'COMPLETED后订舱恢复候选', 'COMPLETED后同订舱可新建下一张拖车单',
@@ -1,5 +1,6 @@
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { oceanBookingDraft } from './ocean-booking-fixture.mjs'
import { truckAppointmentFieldGaps } from './fulfillment-list-field-contract.mjs'
const MODULE = 'truck-appointments'
const OPTIONS = { scenario: 'booking-level-truck-appointment-lifecycle', deploymentBlocker: true }
@@ -9,6 +10,9 @@ const FORBIDDEN_TRUCK_FIELDS = ['containerBookingId', 'loadingPlanId', 'containe
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function responseEvidence(response) {
return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
}
function data(response) { return codeOf(response) === '200' ? response.body.data : null }
function failure(response) { return response?.http === 0 || response?.http >= 400 || (response?.http === 200 && codeOf(response) !== '200') }
function records(value) { return Array.isArray(value) ? value : value?.records || [] }
@@ -205,8 +209,10 @@ export async function run(context) {
const appointmentId = id(appointment?.id); if (!appointmentId) return
const path = `${TRUCKS}/${appointmentId}`
context.assert(MODULE, '创建响应完全无柜次协议且费用正确', createdResponse,
!hasForbiddenTruckFields(appointment) && appointment?.currency === 'RMB' && Number(appointment?.payableAmount) === 0,
`appointmentNo=${appointment?.appointmentNo || 'missing'}`, OPTIONS)
truckAppointmentFieldGaps(appointment).length === 0
&& !hasForbiddenTruckFields(appointment) && appointment?.currency === 'RMB'
&& Number(appointment?.payableAmount) === 0,
`appointmentNo=${appointment?.appointmentNo || 'missing'}, fieldGaps=${truckAppointmentFieldGaps(appointment).join('|') || 'none'}`, OPTIONS)
const afterCreateCandidate = await candidate(context, identity)
context.assert(MODULE, '创建后订舱立即退出候选', afterCreateCandidate.response, !afterCreateCandidate.item,
`appointmentNo=${appointment.appointmentNo}`, OPTIONS)
@@ -223,12 +229,45 @@ export async function run(context) {
`dispatch-order-${identity.suffix}.pdf`)
const uploadAttachmentResponse = await context.requestMultipart('POST', `${path}/attachments/upload`,
attachmentForm, context.token)
const attachment = context.apiSuccess(MODULE, '上传DISPATCH_ORDER拖车附件', uploadAttachmentResponse, '', OPTIONS)
const attachment = context.apiSuccess(MODULE, '上传并关联DISPATCH_ORDER拖车附件', uploadAttachmentResponse, '', OPTIONS)
const attachmentListResponse = await request(context, 'GET', `${path}/attachments`)
const attachmentList = context.apiSuccess(MODULE, '查询拖车附件列表', attachmentListResponse, '', OPTIONS)
context.assert(MODULE, '附件列表包含本轮运营附件', attachmentListResponse,
records(attachmentList).some((item) => id(item?.id) === id(attachment?.id) && item?.attachmentType === 'DISPATCH_ORDER'),
`attachmentId=${id(attachment?.id) || 'missing'}`, OPTIONS)
if (!attachment?.id || !attachment?.fileId) return
const deleteUploadedResponse = await request(context, 'DELETE',
`${path}/attachments/${encodeURIComponent(id(attachment?.id))}`)
context.addStep(MODULE, '删除首次上传的拖车附件引用', deleteUploadedResponse,
deleteUploadedResponse.http === 200 && codeOf(deleteUploadedResponse) === '200',
responseEvidence(deleteUploadedResponse), OPTIONS)
const referencedResponse = await request(context, 'POST', `${path}/attachments`, {
fileId: id(attachment?.fileId), attachmentType: 'DISPATCH_ORDER',
fileName: attachment?.fileName || `dispatch-order-${identity.suffix}.pdf`,
fileHash: attachment?.fileHash || null,
})
const referenced = context.apiSuccess(MODULE, '重新引用平台归属拖车附件', referencedResponse, '', OPTIONS)
const referencedListResponse = await request(context, 'GET', `${path}/attachments`)
const referencedList = context.apiSuccess(MODULE, '回查重新引用的拖车附件', referencedListResponse, '', OPTIONS)
context.assert(MODULE, '附件列表包含重新引用的DISPATCH_ORDER', referencedListResponse,
records(referencedList).some((item) => id(item?.id) === id(referenced?.id)
&& id(item?.fileId) === id(attachment?.fileId) && item?.attachmentType === 'DISPATCH_ORDER'),
`attachmentId=${id(referenced?.id) || 'missing'}, fileId=${id(attachment?.fileId) || 'missing'}`, OPTIONS)
const deleteReferencedResponse = await request(context, 'DELETE',
`${path}/attachments/${encodeURIComponent(id(referenced?.id))}`)
context.addStep(MODULE, '删除重新引用的拖车附件', deleteReferencedResponse,
deleteReferencedResponse.http === 200 && codeOf(deleteReferencedResponse) === '200',
responseEvidence(deleteReferencedResponse), OPTIONS)
const replacementForm = new FormData(); replacementForm.append('attachmentType', 'DISPATCH_ORDER')
replacementForm.append('file', new Blob([minimalPdfBytes(`${context.marker}-DISPATCH_ORDER-REPLACEMENT`)],
{ type: 'application/pdf' }), `dispatch-order-replacement-${identity.suffix}.pdf`)
const replacementResponse = await context.requestMultipart('POST', `${path}/attachments/upload`,
replacementForm, context.token)
const replacement = context.apiSuccess(MODULE, '恢复一份DISPATCH_ORDER拖车附件供后续状态流转',
replacementResponse, '', OPTIONS)
context.assert(MODULE, '恢复附件使用新引用且仍指向平台文件', replacementResponse,
Boolean(id(replacement?.id) && id(replacement?.fileId)) && id(replacement?.id) !== id(attachment?.id),
`attachmentId=${id(replacement?.id) || 'missing'}, fileId=${id(replacement?.fileId) || 'missing'}`, OPTIONS)
const editedResponse = await request(context, 'PUT', path,
truckAppointmentPayload(context, identity.suffix, identity, Number(appointment.version), { currency: 'USD', payableAmount: 12.34 }))
@@ -258,9 +297,10 @@ export async function run(context) {
const finalDetailResponse = await request(context, 'GET', path)
const finalDetail = context.apiSuccess(MODULE, '回查COMPLETED订舱级拖车详情', finalDetailResponse, '', OPTIONS)
context.assert(MODULE, '完整生命周期完成且详情无柜次协议', finalDetailResponse,
finalDetail?.status === 'COMPLETED' && !hasForbiddenTruckFields(finalDetail)
finalDetail?.status === 'COMPLETED' && truckAppointmentFieldGaps(finalDetail).length === 0
&& !hasForbiddenTruckFields(finalDetail)
&& id(finalDetail?.oceanBookingId) === identity.oceanBookingId,
`appointmentNo=${finalDetail?.appointmentNo || 'missing'}, status=${finalDetail?.status || 'missing'}`, OPTIONS)
`appointmentNo=${finalDetail?.appointmentNo || 'missing'}, status=${finalDetail?.status || 'missing'}, fieldGaps=${truckAppointmentFieldGaps(finalDetail).join('|') || 'none'}`, OPTIONS)
const afterCompleteCandidate = await candidate(context, identity)
context.assert(MODULE, 'COMPLETED后订舱恢复候选', afterCompleteCandidate.response, Boolean(afterCompleteCandidate.item),
`oceanBookingNo=${identity.oceanBookingNo}`, OPTIONS)