diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index fd610a5e6..acf3a29ed 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -16,14 +16,22 @@ const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'over export function normalizeModuleOrder(modules) { const normalized = [...modules] + const manualGroups = ['order-inbound-status-fixtures', 'manual-state-loading-pda', 'manual-state-ocean-trucking'] + const firstManualGroupIndex = normalized.findIndex((name) => manualGroups.includes(name)) + if (!normalized.includes('manual-state-matrix') && firstManualGroupIndex >= 0) { + normalized.splice(firstManualGroupIndex, 0, 'manual-state-matrix') + } const manualMatrixIndex = normalized.indexOf('manual-state-matrix') if (manualMatrixIndex >= 0) { - const internalGroupIndex = normalized.indexOf('order-inbound-status-fixtures') - if (internalGroupIndex >= 0) normalized.splice(internalGroupIndex, 1) + for (const group of manualGroups) { + const internalGroupIndex = normalized.indexOf(group) + if (internalGroupIndex >= 0) normalized.splice(internalGroupIndex, 1) + } const prerequisites = [ 'merchant-qualification-fixture', 'phase3-container-loading', 'phase3-loading', + 'ocean-bookings', 'phase3-customs', 'phase3-loading-finalize', ] @@ -33,6 +41,20 @@ export function normalizeModuleOrder(modules) { } } } + if (normalized.includes('phase3-order-splitting')) { + const prerequisites = ['merchant-qualification-fixture', 'phase3-container-loading', 'phase3-loading'] + for (const prerequisite of prerequisites) { + if (!normalized.includes(prerequisite)) { + normalized.splice(normalized.indexOf('phase3-order-splitting'), 0, prerequisite) + } + } + const splitIndex = normalized.indexOf('phase3-order-splitting') + const loadingIndex = normalized.indexOf('phase3-loading') + if (splitIndex < loadingIndex) { + normalized.splice(splitIndex, 1) + normalized.splice(normalized.indexOf('phase3-loading') + 1, 0, 'phase3-order-splitting') + } + } if (normalized.includes('fee-items')) { const prerequisites = ['merchant-qualification-fixture', 'orders'] const feeItemsIndex = normalized.indexOf('fee-items') diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs index e0736802b..5e891039d 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs @@ -34,6 +34,11 @@ test('does not mutate historical fixtures or remove retained records', () => { assert.match(source, /context\.marker = originalMarker/) }) +test('fails closed when invoked outside the manual matrix wrapper', () => { + assert.match(source, /if \(!matrix\?\.pages\)/) + assert.match(source, /missing=manualStateMatrix\.pages/) +}) + test('covers simplified clearance and customs actions through public multipart upload and complete APIs', () => { assert.match(source, /new FormData\(\)/) assert.match(source, /form\.append\('file'/) diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs index d1a2810a6..f0aea53a7 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs @@ -318,6 +318,11 @@ async function completeSimpleDocuments(context, chain, matrix) { } export async function run(context, matrix = context.createdResources.manualStateMatrix) { + if (!matrix?.pages) { + context.addStep(MODULE, '人工装柜状态模块由人工矩阵统一编排', null, false, + 'missing=manualStateMatrix.pages;请通过 manual-state-matrix 入口执行', OPTIONS) + return + } matrix.pages.containerLoading = { requiredStatuses: CONTAINER_STATUSES, requiredActions: ['clearance'], fixtures: {}, actions: {} } matrix.pages.preloadPlans = { requiredStatuses: PRELOAD_STATUSES, fixtures: {} } diff --git a/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs index 8f0f66925..37da55c15 100644 --- a/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs @@ -18,6 +18,7 @@ test('focused matrix execution injects real container lifecycle prerequisites an 'merchant-qualification-fixture', 'phase3-container-loading', 'phase3-loading', + 'ocean-bookings', 'phase3-customs', 'phase3-loading-finalize', 'manual-state-matrix', @@ -28,6 +29,22 @@ test('focused matrix execution injects real container lifecycle prerequisites an ]).modules, expected) }) +test('focused splitting and manual groups resolve producers before consumers without duplicate internal runs', () => { + const expected = [ + 'merchant-qualification-fixture', + 'phase3-container-loading', + 'phase3-loading', + 'phase3-order-splitting', + 'ocean-bookings', + 'phase3-customs', + 'phase3-loading-finalize', + 'manual-state-matrix', + ] + assert.deepEqual(normalizeModuleOrder([ + 'phase3-order-splitting', 'manual-state-loading-pda', 'manual-state-matrix', + ]), expected) +}) + test('matrix delegates bounded groups and asserts every declared status has a retained id', () => { for (const group of ['order-inbound-status-fixtures', 'manual-state-loading-pda', 'manual-state-ocean-trucking']) assert.match(source, new RegExp(group)) diff --git a/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs index ad246eee5..e97e71f3d 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs @@ -4,13 +4,19 @@ import test from 'node:test' const source = readFileSync(new URL('./manual-state-ocean-trucking.mjs', import.meta.url), 'utf8') -test('retains every ocean booking and truck appointment list status through real APIs', () => { +test('retains only public ocean booking list states while hidden states stay detail-only', () => { 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', '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/) 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}:`)) diff --git a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs index 13b063761..212588bdc 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs @@ -2,6 +2,8 @@ const MODULE = 'manual-state-ocean-trucking' const OPTIONS = { scenario: 'manual-ocean-trucking-state-matrix', deploymentBlocker: true } 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 TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE', 'LOADING', 'DEPARTED', 'COMPLETED', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED'] @@ -128,6 +130,19 @@ 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('缺少现有订舱生命周期模块生成的模板资源') @@ -136,17 +151,26 @@ 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_STATUSES, fixtures: {} } + matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {}, hiddenFixtures: {} } 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 - matrix.pages.oceanBookings.fixtures[status] = { - id: resourceId, status: actual, detailPath: `/ocean-bookings/${resourceId}`, + context.assert(MODULE, `订舱保留 ${status}`, null, resourceId && actual === status, + `id=${resourceId || 'missing'}, actual=${actual || 'missing'}`, OPTIONS) + if (!resourceId || actual !== status) continue + const fixtureValue = { + id: resourceId, status: actual, customerId: id(value?.oceanBooking?.customerId), customerName: value?.oceanBooking?.customerName, warehouseId: id(value?.oceanBooking?.warehouseId), } - context.assert(MODULE, `订舱保留 ${status}`, null, resourceId && actual === status, - `id=${resourceId || 'missing'}, actual=${actual || 'missing'}`, OPTIONS) + 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}`, + } await verify(context, OCEAN_BASE, status, resourceId) }