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

# Conflicts:
#	scripts/admin-test-env-modules/phase3-container-loading.mjs
#	scripts/admin-test-env-modules/phase3-loading.mjs
This commit is contained in:
2026-09-09 01:56:29 +08:00
3 changed files with 23 additions and 16 deletions
@@ -7,6 +7,7 @@ import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
const MODULE = 'phase3-container-loading' const MODULE = 'phase3-container-loading'
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true } const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
const CONTAINERS = '/api/platform/v1/containers' const CONTAINERS = '/api/platform/v1/containers'
const DOMESTIC_CONTAINERS = `${CONTAINERS}/domestic`
const SMART = '/api/platform/v1/first-leg/smart-loading' const SMART = '/api/platform/v1/first-leg/smart-loading'
const PDA = '/api/platform/v1/first-leg/pda-loading' const PDA = '/api/platform/v1/first-leg/pda-loading'
const FIRST_LEG = '/api/platform/v1/first-leg' const FIRST_LEG = '/api/platform/v1/first-leg'
@@ -21,6 +22,7 @@ function failure(response) {
|| (response?.http === 200 && codeOf(response) !== '200') || (response?.http === 200 && codeOf(response) !== '200')
} }
function records(page) { return Array.isArray(page?.records) ? page.records : [] } function records(page) { return Array.isArray(page?.records) ? page.records : [] }
function containerRecord(value) { return value?.container || value }
async function waitForCompletedTask(context, taskId, warehouseId, attempts = 45) { async function waitForCompletedTask(context, taskId, warehouseId, attempts = 45) {
let response = null let response = null
@@ -258,9 +260,8 @@ export async function run(context) {
} }
} }
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/domestic/${containerId}`) const containerDetailResponse = await context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)
const containerWorkbench = context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS) const containerDetail = containerRecord(context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS))
const containerDetail = containerWorkbench?.container
context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse, context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse,
(!containerDetail?.destinationWarehouseCode || !candidate.destinationWarehouseCode (!containerDetail?.destinationWarehouseCode || !candidate.destinationWarehouseCode
|| containerDetail.destinationWarehouseCode === candidate.destinationWarehouseCode) || containerDetail.destinationWarehouseCode === candidate.destinationWarehouseCode)
@@ -399,9 +400,8 @@ export async function run(context) {
unlinkedTaskAbsent, responseEvidence(unlinkedTaskResponse), OPTIONS) unlinkedTaskAbsent, responseEvidence(unlinkedTaskResponse), OPTIONS)
if (!unlinkedTaskAbsent) return if (!unlinkedTaskAbsent) return
const simulationBeforeLinkResponse = await context.request('GET', `${CONTAINERS}/domestic/${containerId}`) const simulationBeforeLinkResponse = await context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)
const simulationWorkbench = context.apiSuccess(MODULE, '预装确认前读取模拟柜最新版本', simulationBeforeLinkResponse, '', OPTIONS) const simulationBeforeLink = containerRecord(context.apiSuccess(MODULE, '预装确认前读取模拟柜最新版本', simulationBeforeLinkResponse, '', OPTIONS))
const simulationBeforeLink = simulationWorkbench?.container
if (!Number.isInteger(Number(simulationBeforeLink?.version))) return if (!Number.isInteger(Number(simulationBeforeLink?.version))) return
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
@@ -473,11 +473,9 @@ export async function run(context) {
`containerBookingId=${containerId}, cabinetNo=${linkedContainer?.cabinetNo || 'missing'}, loadingPlanId=${id(afterLinkPlan?.id) || 'missing'}`, OPTIONS) `containerBookingId=${containerId}, cabinetNo=${linkedContainer?.cabinetNo || 'missing'}, loadingPlanId=${id(afterLinkPlan?.id) || 'missing'}`, OPTIONS)
const activeContainersResponse = await context.request('GET', const activeContainersResponse = await context.request('GET',
`${CONTAINERS}/domestic?scope=ACTIVE&page=1&size=50&keyword=${encodeURIComponent(realContainerNo)}`) `${DOMESTIC_CONTAINERS}?page=1&size=50&scope=ACTIVE&keyword=${encodeURIComponent(realContainerNo)}`)
const activeWorkbench = context.apiSuccess(MODULE, '关联后按真实柜号回查正常货柜列表', activeContainersResponse, '', OPTIONS) const activeContainers = context.apiSuccess(MODULE, '关联后按真实柜号回查正常货柜列表', activeContainersResponse, '', OPTIONS)
const activeContainers = { ...activeWorkbench, const activeRealRows = records(activeContainers).map(containerRecord).filter((item) => item.containerNo === realContainerNo)
records: records(activeWorkbench).map((item) => item?.container).filter(Boolean) }
const activeRealRows = records(activeContainers).filter((item) => item.containerNo === realContainerNo)
context.assert(MODULE, '关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份', activeContainersResponse, context.assert(MODULE, '关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份', activeContainersResponse,
activeRealRows.length === 1 && id(activeRealRows[0]?.id) === containerId, activeRealRows.length === 1 && id(activeRealRows[0]?.id) === containerId,
`activeIds=${activeRealRows.map((item) => id(item?.id)).join('|') || 'none'}`, OPTIONS) `activeIds=${activeRealRows.map((item) => id(item?.id)).join('|') || 'none'}`, OPTIONS)
@@ -593,9 +591,8 @@ export async function run(context) {
`taskId=${taskId || 'missing'}, itemId=${id(pickItem?.id) || 'missing'}`, OPTIONS) `taskId=${taskId || 'missing'}, itemId=${id(pickItem?.id) || 'missing'}`, OPTIONS)
if (!taskId || !pickItem?.id) return if (!taskId || !pickItem?.id) return
const releasedContainerResponse = await context.request('GET', `${CONTAINERS}/domestic/${containerId}`) const releasedContainerResponse = await context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)
const releasedWorkbench = context.apiSuccess(MODULE, '任务生成后回查货柜运输版本', releasedContainerResponse, '', OPTIONS) const releasedContainer = context.apiSuccess(MODULE, '任务生成后回查货柜运输版本', releasedContainerResponse, '', OPTIONS)
const releasedContainer = releasedWorkbench?.container
const stableIdentity = { const stableIdentity = {
taskId, loadingPlanId: id(task.loadingPlanId), containerBookingId: id(task.containerBookingId), taskId, loadingPlanId: id(task.loadingPlanId), containerBookingId: id(task.containerBookingId),
@@ -20,9 +20,19 @@ const truckForm = read(`${adminRoot}trucking/components/TruckAppointmentForm.vue
const containerList = read(`${adminRoot}container-loading/pages/ContainersView.vue`) const containerList = read(`${adminRoot}container-loading/pages/ContainersView.vue`)
const containerOverviewPanel = read(`${adminRoot}container-loading/components/ContainerOverviewPanel.vue`) const containerOverviewPanel = read(`${adminRoot}container-loading/components/ContainerOverviewPanel.vue`)
const containerLoadingAcceptance = read('phase3-container-loading.mjs') const containerLoadingAcceptance = read('phase3-container-loading.mjs')
const phase3LoadingAcceptance = read('phase3-loading.mjs')
const truckAcceptance = read('truck-appointments.mjs') const truckAcceptance = read('truck-appointments.mjs')
const matrix = read('../../../docs/国内货柜履约/货柜管理全链验收矩阵-2026-08-13.md') const matrix = read('../../../docs/国内货柜履约/货柜管理全链验收矩阵-2026-08-13.md')
test('container acceptance reads the canonical domestic workbench route', () => {
assert.match(containerLoadingAcceptance, /const DOMESTIC_CONTAINERS = `\$\{CONTAINERS\}\/domestic`/)
assert.match(containerLoadingAcceptance, /function containerRecord\(value\) \{ return value\?\.container \|\| value \}/)
assert.match(containerLoadingAcceptance, /DOMESTIC_CONTAINERS\}\?page=1&size=50&scope=ACTIVE&keyword=/)
assert.doesNotMatch(containerLoadingAcceptance, /request\('GET', `\$\{CONTAINERS\}\/\$\{containerId\}`\)/)
assert.match(phase3LoadingAcceptance, /\/api\/platform\/v1\/containers\/domestic\/\$\{containerBookingId\}/)
assert.match(phase3LoadingAcceptance, /containerRecord\(context\.apiSuccess/)
})
test('DRAFT stores all five transport fields and release validates the persisted snapshot', () => { test('DRAFT stores all five transport fields and release validates the persisted snapshot', () => {
assert.match(oceanOpenApi, /ReleaseRequest: \{type: object, required: \[expectedVersion\]/) assert.match(oceanOpenApi, /ReleaseRequest: \{type: object, required: \[expectedVersion\]/)
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
@@ -25,6 +25,7 @@ function failure(response) {
return [400, 401, 403, 404, 409, 503].includes(response?.http) return [400, 401, 403, 404, 409, 503].includes(response?.http)
|| (response?.http === 200 && codeOf(response) !== '200') || (response?.http === 200 && codeOf(response) !== '200')
} }
function containerRecord(value) { return value?.container || value }
async function ensureMatsonSupplier(context, records) { async function ensureMatsonSupplier(context, records) {
const existing = records.find((value) => value?.id && value?.name === MATSON_SUPPLIER_NAME) const existing = records.find((value) => value?.id && value?.name === MATSON_SUPPLIER_NAME)
@@ -535,8 +536,7 @@ export async function run(context) {
`bookingId=${bookingId || 'missing'}, status=${bookingConfirmed?.oceanBooking?.status || 'missing'}`, OPTIONS) `bookingId=${bookingId || 'missing'}, status=${bookingConfirmed?.oceanBooking?.status || 'missing'}`, OPTIONS)
if (!bookingId || !requirement?.id || !frozenTransportMatches) return if (!bookingId || !requirement?.id || !frozenTransportMatches) return
const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/domestic/${containerBookingId}`) const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/domestic/${containerBookingId}`)
const latestWorkbench = context.apiSuccess(MODULE, '合并前读取模拟柜最新版本', latestContainerResponse, '', OPTIONS) const latestContainer = containerRecord(context.apiSuccess(MODULE, '合并前读取模拟柜最新版本', latestContainerResponse, '', OPTIONS))
const latestContainer = latestWorkbench?.container
const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, { const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, {
oceanBookingId: bookingId, requirementId: id(requirement.id), oceanBookingId: bookingId, requirementId: id(requirement.id),
...(LATE_CONTAINER_GATE_ONLY ? {} : { containerNo }), ...(LATE_CONTAINER_GATE_ONLY ? {} : { containerNo }),