优化
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
export const EVENT_BRIDGE_RECOVERY_FIXTURE = Object.freeze({
|
||||
customerCode: 'C10000000',
|
||||
eventId: 'b569c7f6-3493-4a0f-a944-dd42698f183b',
|
||||
initialOutboxStatus: 'PENDING',
|
||||
initialRetryCount: 62,
|
||||
failedTarget: '172.31.198.8:9313',
|
||||
currentOutboxStatus: 'PENDING',
|
||||
currentRetryCount: 81,
|
||||
currentFailureCode: 'A0401',
|
||||
currentFailureMessage: '内部服务令牌无效',
|
||||
currentRequestId: '200edca3-428e-446d-977a-ca98a84dd704',
|
||||
currentTraceId: 'e8e6d620c1cff40caa765e6968f89024',
|
||||
dockerDnsProven: true,
|
||||
})
|
||||
|
||||
export const TEST_EVENT_BRIDGE_URLS = Object.freeze({
|
||||
LASTLEG_EVENT_BRIDGE_URL: 'http://event-bridge-overseas:9312',
|
||||
FIRSTLEG_EVENT_BRIDGE_URL: 'http://event-bridge-cn:9312',
|
||||
})
|
||||
|
||||
export const INTERNAL_TOKEN_COHORT_SERVICES = Object.freeze([
|
||||
'firstleg',
|
||||
'event-bridge-cn',
|
||||
'event-bridge-overseas',
|
||||
'lastleg-service',
|
||||
])
|
||||
|
||||
export function validTestEventBridgeEnvironment(values = {}) {
|
||||
return Object.entries(TEST_EVENT_BRIDGE_URLS)
|
||||
.every(([key, expected]) => values[key] === expected)
|
||||
&& Object.values(values).every((value) => !/^https?:\/\/(?:\d{1,3}\.){3}\d{1,3}(?::|\/|$)/.test(String(value)))
|
||||
}
|
||||
|
||||
// Callers must provide only a non-secret fingerprint/revision marker, never the token value.
|
||||
export function validInternalTokenCohort(fingerprints = {}) {
|
||||
const values = INTERNAL_TOKEN_COHORT_SERVICES.map((service) => fingerprints[service])
|
||||
return values.every((value) => typeof value === 'string' && value.length > 0)
|
||||
&& new Set(values).size === 1
|
||||
}
|
||||
|
||||
export function recoveredEventBridgeFacts(facts = {}) {
|
||||
return facts.firstlegOutboxStatus === 'SENT'
|
||||
&& Number(facts.overseasBridgeEventCount) === 1
|
||||
&& Number(facts.lastlegInboxEventCount) === 1
|
||||
&& Number(facts.lastlegOrderCount) === 1
|
||||
&& Number(facts.cnBridgeDuplicateCount) === 0
|
||||
&& Number(facts.manualReplayCount) === 0
|
||||
&& Number(facts.sqlMutationCount) === 0
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export const IMMUTABLE_TAILLEG_EVENT_FIXTURE = Object.freeze({
|
||||
eventId: 'b569c7f6-3493-4a0f-a944-dd42698f183b',
|
||||
status: 'PROCESSED',
|
||||
payloadMutable: false,
|
||||
reusableForAcceptance: false,
|
||||
})
|
||||
|
||||
export const TAILLEG_ORDER_NUMBER = Object.freeze({
|
||||
pattern: /^LMO-\d{6}-\d{4}$/,
|
||||
timeZone: 'Asia/Shanghai',
|
||||
sharedBy: 'containerNo',
|
||||
})
|
||||
|
||||
export function validOwnerSnapshots(orders, expectedCount = orders?.length) {
|
||||
if (!Array.isArray(orders) || orders.length !== expectedCount || orders.length === 0) return false
|
||||
const owners = new Set()
|
||||
for (const order of orders) {
|
||||
const customerId = String(order?.customerId ?? '').trim()
|
||||
const customerName = String(order?.customerName ?? '').trim()
|
||||
if (!customerId || !customerName) return false
|
||||
owners.add(`${customerId}\u0000${customerName}`)
|
||||
}
|
||||
return owners.size === 1
|
||||
}
|
||||
|
||||
export function validContainerOrderGroup(rows) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) return false
|
||||
const containerNos = new Set(rows.map((row) => String(row?.containerNo ?? '').trim()))
|
||||
const orderNos = new Set(rows.map((row) => String(row?.orderNo ?? '').trim()))
|
||||
const sourceInboundIds = rows.map((row) => String(row?.sourceInboundId ?? '').trim())
|
||||
return containerNos.size === 1 && !containerNos.has('')
|
||||
&& orderNos.size === 1 && TAILLEG_ORDER_NUMBER.pattern.test([...orderNos][0])
|
||||
&& sourceInboundIds.every(Boolean) && new Set(sourceInboundIds).size === rows.length
|
||||
}
|
||||
|
||||
export function validEventReplay(before, after) {
|
||||
return Number(before?.orderCount) === Number(after?.orderCount)
|
||||
&& String(before?.orderNo ?? '') === String(after?.orderNo ?? '')
|
||||
&& Number(before?.detailCount) === Number(after?.detailCount)
|
||||
&& Number(before?.historyCount) === Number(after?.historyCount)
|
||||
}
|
||||
|
||||
export function adminPreservesOrderNumber(apiValue, displayedValue, copiedValue) {
|
||||
return TAILLEG_ORDER_NUMBER.pattern.test(String(apiValue ?? ''))
|
||||
&& displayedValue === apiValue
|
||||
&& copiedValue === apiValue
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
EVENT_BRIDGE_RECOVERY_FIXTURE,
|
||||
INTERNAL_TOKEN_COHORT_SERVICES,
|
||||
recoveredEventBridgeFacts,
|
||||
TEST_EVENT_BRIDGE_URLS,
|
||||
validInternalTokenCohort,
|
||||
validTestEventBridgeEnvironment,
|
||||
} from './event-bridge-pending-recovery.mjs'
|
||||
|
||||
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||
const cnCompose = read('../../deploy/docker-compose.yml')
|
||||
const overseasCompose = read('../../deploy/docker-compose.overseas.yml')
|
||||
const deployBuilt = read('../../deploy/scripts/deploy-built-service.sh')
|
||||
const deployServices = read('../../deploy/scripts/deploy-services.sh')
|
||||
const crossRegionHttp = read('phase4-cross-region-events.mjs')
|
||||
|
||||
test('test bridge URLs use stable Compose DNS and internal port 9312 only', () => {
|
||||
assert.deepEqual(TEST_EVENT_BRIDGE_URLS, {
|
||||
LASTLEG_EVENT_BRIDGE_URL: 'http://event-bridge-overseas:9312',
|
||||
FIRSTLEG_EVENT_BRIDGE_URL: 'http://event-bridge-cn:9312',
|
||||
})
|
||||
assert.equal(validTestEventBridgeEnvironment(TEST_EVENT_BRIDGE_URLS), true)
|
||||
assert.equal(validTestEventBridgeEnvironment({
|
||||
...TEST_EVENT_BRIDGE_URLS, LASTLEG_EVENT_BRIDGE_URL: 'http://172.31.198.8:9313',
|
||||
}), false)
|
||||
})
|
||||
|
||||
test('Compose wires both producers to the stable service aliases used by test env', () => {
|
||||
assert.match(cnCompose, /LASTLEG_EVENT_BRIDGE_URL:\s*\$\{LASTLEG_EVENT_BRIDGE_URL\}/)
|
||||
assert.match(overseasCompose,
|
||||
/FIRSTLEG_EVENT_BRIDGE_URL:\s*\$\{CN_EVENT_BRIDGE_TARGET_URL:-http:\/\/event-bridge-cn:9312\}/)
|
||||
assert.match(cnCompose, /aliases:\s*\[event-bridge-cn\]/)
|
||||
assert.match(overseasCompose, /aliases:\s*\[event-bridge-overseas\]/)
|
||||
for (const source of [cnCompose, overseasCompose]) {
|
||||
assert.doesNotMatch(source, /(?:LASTLEG|FIRSTLEG|CN)_EVENT_BRIDGE_URL:[^\n]*(?:\d{1,3}\.){3}\d{1,3}/)
|
||||
assert.doesNotMatch(source, /(?:LASTLEG|FIRSTLEG|CN)_EVENT_BRIDGE_URL:[^\n]*:9313/)
|
||||
}
|
||||
})
|
||||
|
||||
test('all four recovery services inherit INTERNAL_SERVICE_TOKEN and deploy from one env file', () => {
|
||||
assert.deepEqual(INTERNAL_TOKEN_COHORT_SERVICES, [
|
||||
'firstleg', 'event-bridge-cn', 'event-bridge-overseas', 'lastleg-service',
|
||||
])
|
||||
assert.match(cnCompose, /x-common:\s*&common[\s\S]*INTERNAL_SERVICE_TOKEN:\s*\$\{INTERNAL_SERVICE_TOKEN\}/)
|
||||
assert.match(overseasCompose,
|
||||
/x-overseas-common:\s*&overseas-common[\s\S]*INTERNAL_SERVICE_TOKEN:\s*\$\{INTERNAL_SERVICE_TOKEN\}/)
|
||||
assert.match(cnCompose, /firstleg:[\s\S]*<<:\s*\*common/)
|
||||
assert.match(cnCompose, /event-bridge-service:[\s\S]*<<:\s*\*common/)
|
||||
assert.match(overseasCompose, /lastleg-service:[\s\S]*<<:\s*\*overseas-common/)
|
||||
assert.match(overseasCompose, /event-bridge-service:[\s\S]*<<:\s*\*overseas-common/)
|
||||
for (const source of [deployBuilt, deployServices]) {
|
||||
assert.match(source, /--env-file\s+"\$ENV_FILE"/)
|
||||
}
|
||||
assert.equal(validInternalTokenCohort({
|
||||
firstleg: 'revision-20260827',
|
||||
'event-bridge-cn': 'revision-20260827',
|
||||
'event-bridge-overseas': 'revision-20260827',
|
||||
'lastleg-service': 'revision-20260827',
|
||||
}), true)
|
||||
assert.equal(validInternalTokenCohort({
|
||||
firstleg: 'revision-20260827',
|
||||
'event-bridge-cn': 'revision-20260827',
|
||||
'event-bridge-overseas': 'stale-revision',
|
||||
'lastleg-service': 'revision-20260827',
|
||||
}), false)
|
||||
})
|
||||
|
||||
test('incident fixture records DNS recovery followed by retry81 token rejection', () => {
|
||||
assert.deepEqual(EVENT_BRIDGE_RECOVERY_FIXTURE, {
|
||||
customerCode: 'C10000000',
|
||||
eventId: 'b569c7f6-3493-4a0f-a944-dd42698f183b',
|
||||
initialOutboxStatus: 'PENDING',
|
||||
initialRetryCount: 62,
|
||||
failedTarget: '172.31.198.8:9313',
|
||||
currentOutboxStatus: 'PENDING',
|
||||
currentRetryCount: 81,
|
||||
currentFailureCode: 'A0401',
|
||||
currentFailureMessage: '内部服务令牌无效',
|
||||
currentRequestId: '200edca3-428e-446d-977a-ca98a84dd704',
|
||||
currentTraceId: 'e8e6d620c1cff40caa765e6968f89024',
|
||||
dockerDnsProven: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('post-rebuild convergence uses the original pending event with no replay or SQL repair', () => {
|
||||
assert.equal(recoveredEventBridgeFacts({
|
||||
firstlegOutboxStatus: 'SENT', overseasBridgeEventCount: 1,
|
||||
lastlegInboxEventCount: 1, lastlegOrderCount: 1, cnBridgeDuplicateCount: 0,
|
||||
manualReplayCount: 0, sqlMutationCount: 0,
|
||||
}), true)
|
||||
assert.equal(recoveredEventBridgeFacts({
|
||||
firstlegOutboxStatus: 'SENT', overseasBridgeEventCount: 1,
|
||||
lastlegInboxEventCount: 1, lastlegOrderCount: 2, cnBridgeDuplicateCount: 0,
|
||||
manualReplayCount: 0, sqlMutationCount: 0,
|
||||
}), false)
|
||||
assert.equal(recoveredEventBridgeFacts({
|
||||
firstlegOutboxStatus: 'SENT', overseasBridgeEventCount: 1,
|
||||
lastlegInboxEventCount: 1, lastlegOrderCount: 1, cnBridgeDuplicateCount: 0,
|
||||
manualReplayCount: 1, sqlMutationCount: 0,
|
||||
}), false)
|
||||
assert.match(crossRegionHttp, /const delivered = await poll/)
|
||||
assert.match(crossRegionHttp, /deliveredRows\.length > 0/)
|
||||
assert.match(crossRegionHttp, /sameIds\(delivered\.response\.body\?\.data, deliveredAgain\.body\?\.data\)/)
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
adminPreservesOrderNumber,
|
||||
IMMUTABLE_TAILLEG_EVENT_FIXTURE,
|
||||
TAILLEG_ORDER_NUMBER,
|
||||
validContainerOrderGroup,
|
||||
validEventReplay,
|
||||
validOwnerSnapshots,
|
||||
} from './tailleg-owner-order-number-contract.mjs'
|
||||
|
||||
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||
const eventV2 = read('../../common-event-contract/src/main/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2.java')
|
||||
const producer = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java')
|
||||
const producerRows = read('../../firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingCompletedEventMapper.java')
|
||||
const consumer = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java')
|
||||
const orderEntity = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java')
|
||||
const orderService = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java')
|
||||
const generator = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderNoGenerator.java')
|
||||
const generatorTests = read('../../lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderNoGeneratorTest.java')
|
||||
const consumerTests = read('../../lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java')
|
||||
const adminApi = read('../../../apps/admin-web/src/modules/overseas-warehouse/api/lastMileOrders.ts')
|
||||
const adminPresentation = read('../../../apps/admin-web/src/modules/overseas-warehouse/lastMilePresentation.ts')
|
||||
const adminList = read('../../../apps/admin-web/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue')
|
||||
const adminDetail = read('../../../apps/admin-web/src/modules/overseas-warehouse/pages/LastMileOrderDetailView.vue')
|
||||
|
||||
test('new 15-order event requires one non-empty trusted owner snapshot', () => {
|
||||
const orders = Array.from({ length: 15 }, (_, index) => ({
|
||||
sourceInboundId: `inbound-${index + 1}`, customerId: 'customer-1', customerName: '验收货主',
|
||||
}))
|
||||
assert.equal(validOwnerSnapshots(orders, 15), true)
|
||||
assert.equal(validOwnerSnapshots(orders.map((order, index) => index === 7
|
||||
? { ...order, customerName: '' } : order), 15), false)
|
||||
assert.equal(validOwnerSnapshots(orders.map((order, index) => index === 7
|
||||
? { ...order, customerName: '另一货主' } : order), 15), false)
|
||||
})
|
||||
|
||||
test('the processed historical event is immutable and cannot be reused for acceptance', () => {
|
||||
assert.deepEqual(IMMUTABLE_TAILLEG_EVENT_FIXTURE, {
|
||||
eventId: 'b569c7f6-3493-4a0f-a944-dd42698f183b',
|
||||
status: 'PROCESSED', payloadMutable: false, reusableForAcceptance: false,
|
||||
})
|
||||
})
|
||||
|
||||
test('V2 producer freezes customerId and customerName and rejects a blank name', () => {
|
||||
assert.match(eventV2, /List<Item> items,\s*String customerId,\s*String customerName,\s*String orderType/)
|
||||
assert.match(producerRows, /b\.customer_name/)
|
||||
assert.match(producer, /first\.getCustomerName\(\)/)
|
||||
assert.match(producer, /(?:customerName|getCustomerName)[\s\S]{0,300}(?:isBlank|hasText)/)
|
||||
})
|
||||
|
||||
test('Lastleg rejects a missing owner snapshot, persists it, and never calls IAM to repair it', () => {
|
||||
assert.match(consumer, /source\.customerName\(\)/)
|
||||
assert.match(consumer, /customerName[\s\S]{0,240}(?:isBlank|hasText)/)
|
||||
assert.match(consumer, /order\.setCustomerName\(customerName\)/)
|
||||
assert.match(orderEntity, /private String customerName;/)
|
||||
assert.doesNotMatch(consumer, /(?:Iam|IAM|\/iam|CustomerClient|customer.*client)/)
|
||||
})
|
||||
|
||||
test('Lastleg query DTO and Admin list/detail use the frozen owner name', () => {
|
||||
assert.match(orderService, /o\.getCustomerName\(\)/)
|
||||
assert.match(adminApi, /customerName\?: string \| null/)
|
||||
assert.match(adminPresentation, /owner:\s*text\(dto\.customerName\)/)
|
||||
assert.match(adminDetail, /label:\s*'货主',\s*value:\s*text\(order\?\.customerName\)/)
|
||||
})
|
||||
|
||||
test('new tailleg number is Shanghai daily LMO-yyMMdd-#### with concurrency and cross-day tests', () => {
|
||||
assert.equal(TAILLEG_ORDER_NUMBER.pattern.test('LMO-260827-0001'), true)
|
||||
assert.equal(TAILLEG_ORDER_NUMBER.pattern.test('LMO2087379674555461633'), false)
|
||||
assert.equal(TAILLEG_ORDER_NUMBER.timeZone, 'Asia/Shanghai')
|
||||
assert.match(generator, /Asia\/Shanghai/)
|
||||
assert.match(generator, /yyMMdd/)
|
||||
assert.match(generator, /LMO-/)
|
||||
assert.match(generator, /%04d|padStart\(4|String\.format/)
|
||||
assert.match(generatorTests, /(?:concurrent|并发)/i)
|
||||
assert.match(generatorTests, /(?:cross.?day|next.?day|跨天)/i)
|
||||
assert.match(generatorTests, /\^LMO-\\d\{6\}-\\d\{4\}\$/)
|
||||
})
|
||||
|
||||
test('one container with 15 inbound rows shares one new LMO number', () => {
|
||||
const rows = Array.from({ length: 15 }, (_, index) => ({
|
||||
containerNo: 'CONT-15', orderNo: 'LMO-260827-0042', sourceInboundId: `inbound-${index + 1}`,
|
||||
}))
|
||||
assert.equal(validContainerOrderGroup(rows), true)
|
||||
assert.equal(validContainerOrderGroup(rows.map((row, index) => index === 14
|
||||
? { ...row, orderNo: 'LMO-260827-0043' } : row)), false)
|
||||
assert.match(consumer, /String orderNo\s*=\s*orderNoGenerator\.[^(]+\(payload\.containerNo\(\)\);[\s\S]*for \(int index/)
|
||||
assert.match(consumer, /createOrder\(envelope, payload, source, orderNo, rawSource\)/)
|
||||
})
|
||||
|
||||
test('same event replay adds nothing and preserves the allocated number', () => {
|
||||
const before = { orderCount: 15, orderNo: 'LMO-260827-0042', detailCount: 30, historyCount: 15 }
|
||||
assert.equal(validEventReplay(before, { ...before }), true)
|
||||
assert.equal(validEventReplay(before, { ...before, orderCount: 30 }), false)
|
||||
assert.match(consumer, /if\s*\(existing\s*!=\s*null\)[\s\S]{0,400}return countOrders/)
|
||||
assert.match(consumerTests, /(?:replay|duplicate|重复)[\s\S]{0,900}(?:orderNo|OrderNo)/i)
|
||||
})
|
||||
|
||||
test('Admin displays and copies the exact queried number without reformatting', () => {
|
||||
const value = 'LMO-260827-0042'
|
||||
assert.equal(adminPreservesOrderNumber(value, value, value), true)
|
||||
assert.equal(adminPreservesOrderNumber(value, value.replaceAll('-', ''), value), false)
|
||||
assert.match(adminPresentation, /orderNo:\s*text\(dto\.orderNo\)/)
|
||||
assert.match(adminList, /prop:\s*'orderNo'/)
|
||||
assert.match(adminDetail, /reference-value="referenceValue"/)
|
||||
const adminOrderSources = `${adminList}\n${adminDetail}`
|
||||
assert.match(adminOrderSources, /(?:clipboard\.writeText|copyText|copyToClipboard)[\s\S]{0,240}(?:orderNo|referenceValue)|(?:orderNo|referenceValue)[\s\S]{0,240}(?:clipboard\.writeText|copyText|copyToClipboard)/)
|
||||
})
|
||||
Reference in New Issue
Block a user