2026-08-14 14:26:56 +08:00
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { controlledPhotoDownload } from './phase3-container-overview.mjs'
2026-10-23 11:19:36 +08:00
import { parseArguments , validateExecutionOptions } from '../admin-test-env-acceptance.mjs'
2026-08-14 14:26:56 +08:00
const read = ( path ) => readFileSync ( new URL ( path , import . meta . url ), 'utf8' )
const overviewOpenApi = read ( '../../firstleg/src/main/resources/openapi/phase3-container-overview-v1.yaml' )
const oceanOpenApi = read ( '../../firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml' )
const overviewAcceptance = read ( 'phase3-container-overview.mjs' )
const oceanAcceptance = read ( 'ocean-bookings.mjs' )
2026-08-15 17:04:00 +08:00
const oceanFixture = read ( 'ocean-booking-fixture.mjs' )
2026-08-14 14:26:56 +08:00
const adminRoot = '../../../apps/admin-web/src/modules/'
const releaseView = read ( ` ${ adminRoot } ocean-bookings/pages/OceanBookingDetailView.vue` )
const releaseTypes = read ( ` ${ adminRoot } ocean-bookings/types.ts` )
const truckApi = read ( ` ${ adminRoot } trucking/api/platformTrucking.ts` )
const truckAttachmentDialog = read ( ` ${ adminRoot } trucking/components/TruckAttachmentDialog.vue` )
const truckList = read ( ` ${ adminRoot } trucking/pages/TruckAppointmentsView.vue` )
const truckForm = read ( ` ${ adminRoot } trucking/components/TruckAppointmentForm.vue` )
const containerList = read ( ` ${ adminRoot } container-loading/pages/ContainersView.vue` )
2026-08-14 14:50:39 +08:00
const containerOverviewPanel = read ( ` ${ adminRoot } container-loading/components/ContainerOverviewPanel.vue` )
2026-08-14 14:26:56 +08:00
const containerLoadingAcceptance = read ( 'phase3-container-loading.mjs' )
2026-09-09 01:49:52 +08:00
const phase3LoadingAcceptance = read ( 'phase3-loading.mjs' )
2026-08-14 14:26:56 +08:00
const truckAcceptance = read ( 'truck-appointments.mjs' )
2026-09-29 15:06:42 +08:00
const matrix = read ( '../../../docs/国内货柜履约/归档/货柜管理全链验收矩阵-2026-08-13.md' )
2026-08-14 14:26:56 +08:00
2026-10-23 11:19:36 +08:00
test ( 'non-ISO HTTP scope excludes unrelated deletion probes and keeps non-ISO completion facts' , () => {
const options = parseArguments ([ '--modules' , 'phase3-container-loading' , '--verify-non-iso-container' , 'true' ,
'--gateway' , 'https://acceptance.invalid' , '--username' , 'test' , '--password' , 'test' ,
'--pda-username' , 'pda' , '--pda-password' , 'test' ])
assert . equal ( validateExecutionOptions ( options ). verifyNonIsoContainer , true )
assert . throws (() => validateExecutionOptions ({ ... options , verifyPalletLocationGuide : true }), /删除探针/ )
assert . throws (() => validateExecutionOptions ({ ... options , requestedModules : [ 'phase3-loading' ] }), /单独柜号链/ )
assert . match ( containerLoadingAcceptance , /const realContainerNo = 'JZXH10231043'/ )
assert . match ( containerLoadingAcceptance , /const replacementContainerNo = `TEST-\$\{context.marker\}-NOISO`/ )
assert . match ( containerLoadingAcceptance , /if \(context.options\?\.verifyNonIsoContainer\)[\s\S]*?addCoverageGap[\s\S]*?else if \(!channelIsolation.pair\)/ )
})
2026-09-09 01:49:52 +08:00
test ( 'container acceptance reads the canonical domestic workbench route' , () => {
assert . match ( containerLoadingAcceptance , /const DOMESTIC_CONTAINERS = `\$\{CONTAINERS\}\/domestic`/ )
2026-09-09 12:19:16 +08:00
assert . match ( containerLoadingAcceptance , /function containerRecord\(value\) \{ return value\?\.container \}/ )
assert . doesNotMatch ( containerLoadingAcceptance , /value\?\.container \|\| value/ )
2026-09-09 01:54:54 +08:00
assert . match ( containerLoadingAcceptance , /DOMESTIC_CONTAINERS\}\?page=1&size=50&scope=ACTIVE&keyword=/ )
2026-09-09 01:49:52 +08:00
assert . doesNotMatch ( containerLoadingAcceptance , /request\('GET', `\$\{CONTAINERS\}\/\$\{containerId\}`\)/ )
assert . match ( phase3LoadingAcceptance , /\/api\/platform\/v1\/containers\/domestic\/\$\{containerBookingId\}/ )
2026-09-09 01:52:59 +08:00
assert . match ( phase3LoadingAcceptance , /containerRecord\(context\.apiSuccess/ )
2026-09-09 01:49:52 +08:00
})
2026-08-18 16:39:39 +08:00
test ( 'DRAFT stores all five transport fields and release validates the persisted snapshot' , () => {
assert . match ( oceanOpenApi , /ReleaseRequest: \{type: object, required: \[expectedVersion\]/ )
2026-08-14 14:26:56 +08:00
for ( const field of [ 'billOfLadingNo' , 'vesselName' , 'voyageNo' , 'estimatedSailingAt' , 'estimatedArrivalAt' ]) {
assert . match ( releaseTypes , new RegExp ( ` ${ field } : string` ), field )
2026-08-15 09:35:52 +08:00
assert . match ( releaseView , new RegExp ( `form\\.data\\. ${ field } ` ), field )
2026-08-15 17:04:00 +08:00
assert . match ( oceanFixture , new RegExp ( ` ${ field } :` ), field )
2026-08-14 14:26:56 +08:00
}
2026-08-18 16:39:39 +08:00
assert . match ( oceanAcceptance , /完整 DRAFT 放舱为 RELEASED 且冻结五项运输事实/ )
assert . match ( oceanAcceptance , /RELEASED 编辑拒绝且零写入/ )
2026-08-14 14:26:56 +08:00
})
test ( 'overview regression requires workflowStatus and distinguishes 100 percent awaiting review' , () => {
assert . match ( overviewAcceptance , /'workflowStatus'/ )
assert . match ( overviewAcceptance , /workflowStatus === 'AWAITING_REVIEW'/ )
assert . match ( overviewOpenApi , /workflowStatus:/ )
assert . doesNotMatch ( matrix , /PDA overview `workflowStatus` 缺失[\s\S]*BLOCKED_CONTRACT/ )
})
2026-09-09 20:36:53 +08:00
test ( 'pre-departure overview rejects destination workbench documents explicitly' , () => {
assert . match ( overviewAcceptance , /国内未出仓时 \$\{documentType\} 不生成国外工作台数据/ )
assert . match ( overviewAcceptance , /国内未出仓时目的港清关不生成国外工作台数据/ )
2026-10-21 15:53:33 +08:00
assert . match ( overviewAcceptance , /货柜尚未离仓或实际开船,不能维护目的港清关资料/ )
2026-09-09 20:36:53 +08:00
assert . match ( overviewAcceptance , /国外工作台货柜不存在/ )
})
2026-08-14 22:30:28 +08:00
test ( 'active overview photos download through controlled paths while void history has no download entry' , () => {
2026-08-14 14:26:56 +08:00
assert . match ( overviewOpenApi , /\/api\/platform\/v1\/containers\/\{id\}\/pda\/photos\/\{fileId\}\/download/ )
2026-08-14 22:43:07 +08:00
assert . match ( overviewAcceptance , /\['evidenceId', 'fileId', 'status'\]\.every/ )
assert . doesNotMatch ( overviewAcceptance , /\['evidenceId', 'fileId', 'status', 'downloadPath'\]\.every/ )
2026-08-14 14:26:56 +08:00
assert . match ( overviewAcceptance , /context\.requestBinary\('GET', downloadPath\)/ )
2026-08-14 22:30:28 +08:00
assert . match ( overviewAcceptance , /photo\.status === 'VOID'/ )
assert . match ( overviewAcceptance , /overview PDA 作废照片 \$\{index \+ 1\} 无下载入口/ )
assert . match ( overviewAcceptance , /photo\.status !== 'ACTIVE' \|\| !controlledPath/ )
assert . match ( overviewOpenApi , /status: \{type: string, enum: \[ACTIVE, VOID\]\}/ )
assert . match ( overviewOpenApi , /downloadPath: \{type: string, nullable: true, description: '仅 ACTIVE 照片提供鉴权下载路径;VOID 历史照片为空'\}/ )
2026-08-14 22:43:07 +08:00
assert . equal ( overviewAcceptance . indexOf ( "if (photo.status === 'VOID')" )
< overviewAcceptance . indexOf ( "context.requestBinary('GET', downloadPath)" ), true )
2026-08-14 14:50:39 +08:00
assert . equal ( controlledPhotoDownload ( '/api/platform/v1/containers/7/pda/photos/23/download' , {
2026-08-14 14:26:56 +08:00
http : 200 , bytes : new Uint8Array ([ 1 ]), contentType : 'image/png' ,
}), true )
for ( const [ path , response ] of [
[ 'https://example.test/file' , { http : 200 , bytes : new Uint8Array ([ 1 ]), contentType : 'image/png' }],
2026-08-14 14:50:39 +08:00
[ '/api/platform/v1/files/loading-evidence/21/22/23' , { http : 200 , bytes : new Uint8Array ([ 1 ]), contentType : 'image/png' }],
[ '/api/platform/v1/containers/7/pda/photos/23/download' , { http : 200 , bytes : new Uint8Array ([ 1 ]), contentType : 'application/json' }],
[ '/api/platform/v1/containers/7/pda/photos/23/download' , { http : 404 , bytes : new Uint8Array (), contentType : 'image/png' }],
2026-08-14 14:26:56 +08:00
]) assert . equal ( controlledPhotoDownload ( path , response ), false )
})
2026-09-04 15:19:45 +08:00
test ( 'truck attachment API reference flow and Admin upload-delete flow are frozen' , () => {
2026-08-14 14:26:56 +08:00
assert . match ( truckApi , /referenceTruckAppointmentAttachment/ )
2026-08-14 14:50:39 +08:00
assert . match ( truckApi , /uploadTruckAppointmentAttachment/ )
2026-08-14 14:26:56 +08:00
assert . match ( truckApi , /deleteTruckAppointmentAttachment/ )
2026-08-14 14:50:39 +08:00
assert . match ( truckAttachmentDialog , /uploadTruckAppointmentAttachment/ )
assert . match ( truckAttachmentDialog , /上传并关联/ )
assert . match ( truckAttachmentDialog , /confirmButtonText: '确认', cancelButtonText: '取消'/ )
2026-09-01 11:01:34 +08:00
for ( const type of [ 'DISPATCH_ORDER' , 'DRIVER_DOCUMENT' , 'VEHICLE_DOCUMENT' , 'OTHER' ])
assert . match ( truckAttachmentDialog , new RegExp ( type ))
assert . doesNotMatch ( truckAttachmentDialog , /BILL_OF_LADING/ )
for ( const label of [ '上传并关联DISPATCH_ORDER拖车附件' , '删除首次上传的拖车附件引用' ,
'重新引用平台归属拖车附件' , '附件列表包含重新引用的DISPATCH_ORDER' , '删除重新引用的拖车附件' ])
assert . match ( truckAcceptance , new RegExp ( label ))
assert . doesNotMatch ( truckAcceptance , /attachmentType=BILL_OF_LADING|派车前保留有效 BILL_OF_LADING 提单附件/ )
2026-08-14 14:50:39 +08:00
assert . match ( matrix , /拖车附件闭环[\s\S]*`LANDED`/ )
2026-08-14 14:26:56 +08:00
})
test ( 'existing container number replacement keeps container plan and PDA identities stable' , () => {
assert . match ( containerLoadingAcceptance , /replacementContainerNo/ )
2026-08-31 12:13:20 +08:00
assert . match ( containerLoadingAcceptance , /PDA任务领取后真实柜号纠错被拒绝/ )
2026-08-14 14:26:56 +08:00
assert . match ( containerLoadingAcceptance , /containerBookingId: containerId/ )
assert . match ( containerLoadingAcceptance , /loadingPlanId: planId/ )
assert . match ( containerLoadingAcceptance , /pdaTaskId: taskId/ )
assert . match ( containerLoadingAcceptance , /pdaIdentityStable: true/ )
})
2026-08-18 16:39:39 +08:00
test ( 'PDA task opens only after no-number push, release, four-field link and plan-level confirmation' , () => {
2026-09-04 15:19:45 +08:00
assert . match ( containerLoadingAcceptance , /分配后自动建立预装关联且计划可读/ )
assert . match ( containerLoadingAcceptance , /无号 push 后预装清单可见/ )
2026-08-15 17:04:00 +08:00
assert . match ( containerLoadingAcceptance , /无号 push 成功后 PDA 查询 A0404/ )
2026-08-18 16:39:39 +08:00
assert . match ( containerLoadingAcceptance , /oceanBookingId: bookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/ )
assert . match ( containerLoadingAcceptance , /关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份/ )
2026-08-14 16:06:27 +08:00
assert . match ( containerLoadingAcceptance , /ocean-bookings\/\$\{bookingId\}\/release/ )
2026-09-01 10:00:13 +08:00
assert . match ( containerLoadingAcceptance , /ocean-bookings\/\$\{bookingId\}\/documents\/upload/ )
assert . match ( containerLoadingAcceptance , /documentType', 'BOOKING_CONFIRMATION'/ )
2026-08-18 16:39:39 +08:00
assert . match ( containerLoadingAcceptance , /订舱放舱为 RELEASED 且不预生成真实货柜/ )
2026-08-15 17:04:00 +08:00
assert . match ( containerLoadingAcceptance , /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/ )
assert . match ( containerLoadingAcceptance , /loading-plans\/\$\{planId\}\/confirm/ )
2026-09-01 10:17:03 +08:00
assert . match ( containerLoadingAcceptance , /PDA}\/tasks\/\$\{taskId\}\/evidence/ )
assert . match ( containerLoadingAcceptance , /evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/content/ )
2026-08-15 17:04:00 +08:00
assert . match ( containerLoadingAcceptance , /计划级原子确认整份预装并触发唯一 PDA 任务/ )
2026-09-01 10:17:03 +08:00
assert . match ( containerLoadingAcceptance , /waitForCompletedTask/ )
assert . match ( containerLoadingAcceptance , /taskStatus === 'COMPLETED' && task\?\.workflowStatus === 'COMPLETED'/ )
2026-08-15 17:04:00 +08:00
assert . match ( containerLoadingAcceptance , /confirmationStatus === 'CONFIRMED'/ )
assert . match ( containerLoadingAcceptance , /readiness\?\.readyForPda === true/ )
assert . match ( containerLoadingAcceptance , /taskId === confirmedTaskId/ )
assert . match ( containerLoadingAcceptance , /计划级确认同 key 异参返回冲突/ )
assert . match ( containerLoadingAcceptance , /已确认计划使用新 key 重复确认返回冲突/ )
assert . doesNotMatch ( containerLoadingAcceptance , /\/preload\/confirming|\/preload\/confirm\?tenantId=/ )
2026-08-18 16:39:39 +08:00
assert . match ( containerLoadingAcceptance , /confirmedBooking\?\.oceanBooking\?\.status === 'RELEASED'/ )
assert . doesNotMatch ( containerLoadingAcceptance , /realContainerBookingId|MERGED/ )
2026-08-14 16:06:27 +08:00
})
2026-08-14 14:26:56 +08:00
test ( 'customer public display uses tenantName without a legalName fallback' , () => {
2026-09-01 11:01:34 +08:00
assert . match ( containerList , /label: customer\.tenantName/ )
for ( const source of [ truckList , truckForm ])
assert . doesNotMatch ( source , /customer\.(?:tenantName|legalName)/ )
2026-08-14 14:26:56 +08:00
for ( const source of [ truckList , containerList , truckForm ])
assert . doesNotMatch ( source , /customer\.legalName\?\.trim\(\) \|\| customer\.tenantName/ )
assert . match ( matrix , /客户公开名称一致性[\s\S]*已统一 `tenantName`[\s\S]*`LANDED`/ )
})
2026-08-14 14:50:39 +08:00
test ( 'Chinese status dialogs and 390px layout are landed' , () => {
assert . match ( releaseView , /确认放舱/ )
assert . match ( containerOverviewPanel , /AWAITING_REVIEW: '待人工复核'/ )
assert . match ( containerOverviewPanel , /PHOTO_STEP_EMPTY_CONTAINER: '空柜照片'/ )
assert . match ( matrix , /中文状态、弹框与 390px 布局[\s\S]*`LANDED`/ )
assert . doesNotMatch ( matrix , /拖车附件新增入口缺失|中文状态、弹框与 390px 布局[^\n]*BLOCKED_CONTRACT/ )
2026-08-14 14:26:56 +08:00
})