Files
baiyue-firstleg/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs
T
2026-08-17 13:47:20 +08:00

116 lines
8.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const adminPage = readFileSync(new URL('../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue', import.meta.url), 'utf8')
const adminTest = readFileSync(new URL('../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts', import.meta.url), 'utf8')
const adminTypes = readFileSync(new URL('../../../apps/admin-web/src/modules/ocean-bookings/types.ts', import.meta.url), 'utf8')
const prototype = readFileSync(new URL('../../../docs/悦/货代SAAS平台后台/prototype-src/modules/06-containers.html', import.meta.url), 'utf8')
const dto = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java', import.meta.url), 'utf8')
const openapi = readFileSync(new URL('../../firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml', import.meta.url), 'utf8')
const schema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql', import.meta.url), 'utf8')
const unifiedSql = readFileSync(new URL('../../deploy/sql/头程数据库统一增量-MySQL8.sql', import.meta.url), 'utf8')
const fixture = readFileSync(new URL('./ocean-booking-fixture.mjs', import.meta.url), 'utf8')
const bookingModule = readFileSync(new URL('./ocean-bookings.mjs', import.meta.url), 'utf8')
const containerLoading = readFileSync(new URL('./phase3-container-loading.mjs', import.meta.url), 'utf8')
const releaseFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
test('prototype evidence keeps service term freight term and optional customer fields', () => {
assert.match(prototype, /主要运输条款 Service Term \*<\/label><select><option>CY-CY<\/option>/)
assert.match(prototype, /运费条款 Freight Term \*<\/label><select><option>NINGBO<\/option>/)
assert.match(prototype, /<label>客户\/公司名称<\/label><input id="booking-customer"/)
})
test('Admin keeps source-backed free text terms without invented option dictionaries', () => {
assert.match(adminPage, /v-model="form\.data\.serviceTerm"[^>]*maxlength="32"/)
assert.match(adminPage, /v-model="form\.data\.freightTerm"[^>]*maxlength="32"/)
assert.doesNotMatch(adminPage, /serviceTermOptions|freightTermOptions/)
for (const invented of ['CY-DOOR', 'DOOR-CY', 'DOOR-DOOR', 'PREPAID', 'COLLECT']) {
assert.doesNotMatch(adminPage, new RegExp(invented), `${invented} must not be an invented Admin option`)
}
assert.match(adminTest, /not\.toContain\('serviceTermOptions'\)/)
assert.match(adminTest, /not\.toContain\('freightTermOptions'\)/)
})
test('service and freight terms are required across Java OpenAPI and SQL', () => {
assert.match(dto, /@NotBlank @Size\(max = 32\) String serviceTerm/)
assert.match(dto, /@NotBlank @Size\(max = 32\) String freightTerm/)
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
assert.match(required, /serviceTerm/)
assert.match(required, /freightTerm/)
assert.match(openapi, /serviceTerm: \{type: string, maxLength: 32, description: Required free-text Service Term snapshot/)
assert.match(openapi, /freightTerm: \{type: string, maxLength: 32, description: Required free-text Freight Term snapshot/)
assert.doesNotMatch(openapi, /(?:serviceTerm|freightTerm): \{[^\n]*nullable: true/)
for (const sql of [schema, unifiedSql]) {
assert.match(sql, /service_term VARCHAR\(32\) NOT NULL/)
assert.match(sql, /freight_term VARCHAR\(32\) NOT NULL/)
}
})
test('customer is optional across Admin DraftData DTO and OpenAPI', () => {
assert.match(adminPage, /label="客户(可选)"[\s\S]*?filterable clearable/)
assert.doesNotMatch(adminPage, /customerId:\s*\[\{\s*required:\s*true/)
assert.match(adminTypes, /customerId:\s*string \| null;\s*customerName:\s*string \| null/)
assert.match(dto, /record DraftData\(@Positive Long customerId,\s*@Size\(max = 128\) String customerName/)
assert.doesNotMatch(dto, /record DraftData\(@NotNull @Positive Long customerId/)
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
assert.ok(required, 'BLOCKED_CONTRACT: DraftData.required is missing from OpenAPI')
assert.doesNotMatch(required, /customerId|customerName/)
assert.match(openapi, /customerId: \{[^\n]*nullable: true/)
assert.match(openapi, /customerName: \{[^\n]*nullable: true/)
})
test('offline-confirmed registration requires transport values while release keeps the frozen five-field snapshot', () => {
const draftType = adminTypes.slice(adminTypes.indexOf('export interface OceanBookingDraftData'), adminTypes.indexOf('export interface OceanBookingResource'))
for (const field of releaseFields) {
assert.match(adminPage, new RegExp(`v-model="form\\.data\\.${field}"`),
`${field} must be entered when registering an offline-confirmed booking`)
assert.match(draftType, new RegExp(`${field}\\??:`), `${field} must belong to OceanBookingDraftData`)
}
assert.match(adminTest, /线下订舱资料已登记并确认/)
assert.match(adminTest, /登记后直接确认/)
const saveRegion = adminPage.slice(adminPage.indexOf('async function save'), adminPage.indexOf('function requireReason'))
assert.doesNotMatch(saveRegion, /data:\s*form\.data/)
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
for (const field of releaseFields) assert.match(required, new RegExp(field))
assert.match(dto, /@NotBlank @Size\(max = 128\) String billOfLadingNo/)
assert.match(dto, /@NotBlank @Size\(max = 128\) String vesselName/)
assert.match(dto, /@NotBlank @Size\(max = 64\) String voyageNo/)
assert.match(dto, /@NotNull LocalDateTime estimatedSailingAt/)
assert.match(dto, /@NotNull LocalDateTime estimatedArrivalAt/)
const releaseCall = adminPage.slice(adminPage.indexOf('releaseOceanBooking(id.value'), adminPage.indexOf('releaseDialogOpen.value = false'))
for (const field of releaseFields) assert.match(releaseCall, new RegExp(field))
})
test('HTTP fixtures cover unassigned CONFIRMED persistence and assigned customer isolation', () => {
const unassignedFlow = bookingModule.slice(bookingModule.indexOf('async function createConfirmedUnassignedBooking'), bookingModule.indexOf('async function releaseLoadingBooking'))
assert.match(fixture, /oceanBookingDraftWithoutCustomer/)
assert.match(fixture, /delete draft\.customerId[\s\S]*delete draft\.customerName/)
assert.match(fixture, /serviceTerm: 'CY-CY', freightTerm: 'NINGBO'/)
assert.doesNotMatch(fixture, /serviceTerm: 'CY\/CY'|freightTerm: 'PREPAID'/)
assert.match(bookingModule, /createdResources\.oceanBookingUnassignedCustomer/)
assert.match(bookingModule, /未指定客户订舱保持空客户且原子生成一条真实柜/)
assert.match(bookingModule, /按真实订舱号查询未指定客户订舱/)
assert.match(bookingModule, /keyword=\$\{encodeURIComponent\(bookingNo\)\}/)
assert.doesNotMatch(unassignedFlow, /keyword=\$\{encodeURIComponent\(context\.marker\)\}/)
assert.doesNotMatch(bookingModule, /keyword=\$\{encodeURIComponent\(context\.marker\)\}/)
assert.match(bookingModule, /指定客户订舱持久化本轮客户范围快照/)
assert.match(bookingModule, /phase3Loading[\s\S]*phase3ContainerLoading/)
assert.match(containerLoading, /source\?\.marker !== context\.marker/)
assert.match(containerLoading, /id\(candidate\?\.accountId\) === tenantId/)
assert.match(containerLoading, /id\(item\.accountId\) === tenantId/)
})
test('independent ocean booking release reuses the server-frozen transport snapshot', () => {
assert.match(bookingModule, /estimatedSailingAt: detail\.oceanBooking\.estimatedSailingAt/)
assert.match(bookingModule, /estimatedArrivalAt: detail\.oceanBooking\.estimatedArrivalAt/)
assert.match(bookingModule, /const releaseSnapshotResponse = await request\(context, 'GET', path\)/)
assert.match(bookingModule, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
assert.match(bookingModule, /const frozenTransportMatches = frozenTransportValid/)
assert.match(bookingModule, /if \(!frozenTransportMatches\) return/)
assert.match(bookingModule, /expectedVersion: version, \.\.\.frozenTransport, reason: context\.marker/)
assert.doesNotMatch(bookingModule, /expectedVersion: version, billOfLadingNo: `BL-\$\{suffix\}`/)
assert.doesNotMatch(bookingModule, /estimatedSailingAt: '2026-09-01T08:00:00'/)
})