diff --git a/scripts/synchronous-dependency-baseline.mjs b/scripts/synchronous-dependency-baseline.mjs index dbfbb24ec..1dda4fdbf 100644 --- a/scripts/synchronous-dependency-baseline.mjs +++ b/scripts/synchronous-dependency-baseline.mjs @@ -15,6 +15,30 @@ const http = (path, source, target, contexts, access, idempotency, note) => ({ }) export const SYNCHRONOUS_CLIENTS = Object.freeze([ + feign('lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentForecastClient.java', + 'lastleg-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', + '预约影响刷新按柜次批量读取头程不可变船期事实;POST仅查询,刷新写入仅在Lastleg本域,不写Firstleg'), + feign('lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentWarehouseClient.java', + 'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', + '预约货源、整柜及外接候选读取权威启用仓库目录与主档,不创建仓库或预留资源'), + feign('lastleg-service/src/main/java/com/baiyue/lastleg/service/reference/ReturnCustomerReferenceClient.java', + 'lastleg-service', 'iam-service', ['REQUEST'], 'READ', 'SAFE_READ', + '退货订单补全客户快照时按客户ID读取IAM有效客户;历史读取失败保留未知,不调用车队业务或客户资格写操作'), + feign('lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/TransferWarehouseCatalogClient.java', + 'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', + '转仓实收按冻结FBA编码读取权威目录,拒绝缺失或歧义;资源预留仍由独立WarehouseResourceGateway处理'), + feign('order/src/main/java/com/baiyue/order/client/LastlegOrderProgressClient.java', + 'order-service', 'lastleg-service', ['REQUEST'], 'READ', 'SAFE_READ', + '客户订单工作台按服务端客户ID批量读取尾程进度;POST无业务写入,失败不伪造待办或回退成功'), + http('lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/RestLingxingAnalyticsClient.java', + 'lastleg-service', 'integration-service', ['BACKGROUND'], 'READ', 'NONE', + '持久任务后台逐页/逐单读取领星适配接口;每次调用先预留账号额度并冻结attempt上下文,真实尝试耗费预算,未知结果持久暂停,禁止传输层自动重试;人工命令仅入队,报表查询不调用此客户端'), + http('integration-service/src/main/java/com/baiyue/integration/provider/lingxing/LingxingHttpBudgetAuthority.java', + 'integration-service', 'lastleg-service', ['BACKGROUND'], 'READ_WRITE', 'MIXED', + '后台采集出口向Lastleg申请一次性许可并确认真实尝试;authorize消费持久预算,不可重复获得许可,complete按attempt/operation/hash及结果摘要幂等,冲突拒绝;不递归执行采集,不自动重试或跟随重定向'), + http('integration-service/src/main/java/com/baiyue/integration/provider/lingxing/LingxingWmsClient.java', + 'integration-service', 'external-lingxing-wms', ['EXTERNAL'], 'READ', 'NONE', + '受控WMS OMP查询不写供应商业务,但每次HTTP消耗账号额度;一许可仅允许一次尝试,OFF/无独占窗口拒绝,429或结果不确定停止新请求,无自动重试,不等同可无成本重放的SAFE_READ'), feign('lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripFinanceClient.java', 'lastleg-service', 'finance-service', ['REQUEST'], 'READ', 'SAFE_READ', '车次财务按不可变发车客户及派送来源批量只读查询,POST无业务写入;内部服务令牌保护,实收另按公网权限裁剪,不逐行查询或推算未分配收款'), diff --git a/scripts/synchronous-dependency-contract.test.mjs b/scripts/synchronous-dependency-contract.test.mjs index 94cdf6b22..6675777cd 100644 --- a/scripts/synchronous-dependency-contract.test.mjs +++ b/scripts/synchronous-dependency-contract.test.mjs @@ -33,6 +33,7 @@ const expectedOnlineEdges = [ 'iam-service->warehouse-service', 'lastleg-service->file-service', 'lastleg-service->finance-service', + 'lastleg-service->firstleg-service', 'lastleg-service->iam-service', 'lastleg-service->print-service', 'lastleg-service->warehouse-overseas-service', @@ -41,12 +42,15 @@ const expectedOnlineEdges = [ 'order-service->finance-service', 'order-service->firstleg-service', 'order-service->iam-service', + 'order-service->lastleg-service', 'order-service->warehouse-service', 'warehouse-overseas-service->print-service', 'warehouse-overseas-service->warehouse-service' ] const expectedBackgroundEdges = [ + 'integration-service->lastleg-service', + 'lastleg-service->integration-service', 'file-service->finance-service', 'file-service->firstleg-service', 'file-service->iam-service', @@ -103,7 +107,7 @@ test('all Feign clients have reviewed context, access and idempotency metadata', const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN') .map(({ path, source, target }) => ({ path, source, target })) .sort((left, right) => left.path.localeCompare(right.path)) - assert.equal(reviewed.length, 72) + assert.equal(reviewed.length, 77) assert.deepEqual(await discoverFeignEdges(), reviewed) }) @@ -119,7 +123,7 @@ test('all raw HTTP transports have reviewed context, access and idempotency meta } const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP') .map(client => client.path).sort() - assert.equal(reviewed.length, 25) + assert.equal(reviewed.length, 28) assert.deepEqual(discovered.sort(), reviewed) }) @@ -194,14 +198,14 @@ test('REQUEST, BACKGROUND and full graphs remain separate executable baselines', ]) assert.deepEqual(stronglyConnectedComponents(graphEdges('REQUEST')), [ ['auth-service', 'iam-service'], - ['finance-service', 'firstleg-service', 'order-service'] + ['finance-service', 'firstleg-service', 'lastleg-service', 'order-service'] ]) assert.deepEqual(stronglyConnectedComponents(graphEdges('BACKGROUND')), [[ - 'finance-service', 'firstleg-service', 'lastleg-service', 'order-service' + 'finance-service', 'firstleg-service', 'integration-service', 'lastleg-service', 'order-service' ]]) assert.deepEqual(stronglyConnectedComponents(graphEdges('ALL')), [[ 'auth-service', 'file-service', 'finance-service', - 'firstleg-service', 'iam-service', 'lastleg-service', 'order-service' + 'firstleg-service', 'iam-service', 'integration-service', 'lastleg-service', 'order-service' ]]) }) @@ -222,7 +226,7 @@ test('unified authority checks remain infrastructure edges and direct events rem assert.ok(!SYNCHRONOUS_CLIENTS.some(client => ['event-bridge-service','gateway-overseas'].includes(client.source) || ['event-bridge-service','gateway-overseas'].includes(client.target))) }) -test('Order, Firstleg and Finance form the reviewed REQUEST SCC', () => { +test('Order, Firstleg, Finance and Lastleg form the reviewed REQUEST SCC', () => { const requestClients = SYNCHRONOUS_CLIENTS.filter(client => client.contexts.includes('REQUEST')) const orderToFirstleg = requestClients.filter(client => client.source === 'order-service' && client.target === 'firstleg-service') @@ -260,17 +264,17 @@ test('known user entries expose every longest simple REQUEST path over the two-h 'iam-service->auth-service', 'iam-service->warehouse-service' ] }, - 'order-service': { hops: 5, paths: [ - 'order-service->finance-service->firstleg-service->file-service->iam-service->auth-service', - 'order-service->finance-service->firstleg-service->file-service->iam-service->warehouse-service' + 'order-service': { hops: 6, paths: [ + 'order-service->lastleg-service->finance-service->firstleg-service->file-service->iam-service->auth-service', + 'order-service->lastleg-service->finance-service->firstleg-service->file-service->iam-service->warehouse-service' ] }, - 'firstleg-service': { hops: 5, paths: [ - 'firstleg-service->order-service->finance-service->file-service->iam-service->auth-service', - 'firstleg-service->order-service->finance-service->file-service->iam-service->warehouse-service' + 'firstleg-service': { hops: 6, paths: [ + 'firstleg-service->order-service->lastleg-service->finance-service->file-service->iam-service->auth-service', + 'firstleg-service->order-service->lastleg-service->finance-service->file-service->iam-service->warehouse-service' ] }, - 'finance-service': { hops: 5, paths: [ - 'finance-service->firstleg-service->order-service->file-service->iam-service->auth-service', - 'finance-service->firstleg-service->order-service->file-service->iam-service->warehouse-service' + 'finance-service': { hops: 6, paths: [ + 'finance-service->firstleg-service->order-service->lastleg-service->file-service->iam-service->auth-service', + 'finance-service->firstleg-service->order-service->lastleg-service->file-service->iam-service->warehouse-service' ] }, 'file-service': { hops: 2, paths: [ 'file-service->iam-service->auth-service', @@ -278,7 +282,9 @@ test('known user entries expose every longest simple REQUEST path over the two-h ] }, 'lastleg-service': { hops: 6, paths: [ 'lastleg-service->finance-service->firstleg-service->order-service->file-service->iam-service->auth-service', - 'lastleg-service->finance-service->firstleg-service->order-service->file-service->iam-service->warehouse-service' + 'lastleg-service->finance-service->firstleg-service->order-service->file-service->iam-service->warehouse-service', + 'lastleg-service->firstleg-service->order-service->finance-service->file-service->iam-service->auth-service', + 'lastleg-service->firstleg-service->order-service->finance-service->file-service->iam-service->warehouse-service' ] }, 'warehouse-service': { hops: 0, paths: ['warehouse-service'] }, 'warehouse-overseas-service': { hops: 1, paths: [ @@ -292,6 +298,44 @@ test('known user entries expose every longest simple REQUEST path over the two-h } }) +test('self-operated collection and quota callbacks stay outside the REQUEST graph', async () => { + const find = suffix => SYNCHRONOUS_CLIENTS.find(client => client.path.endsWith(suffix)) + const collection = find('/selfoperated/analytics/client/RestLingxingAnalyticsClient.java') + const authority = find('/provider/lingxing/LingxingHttpBudgetAuthority.java') + const supplier = find('/provider/lingxing/LingxingWmsClient.java') + assert.deepEqual([collection.contexts, collection.access, collection.idempotency], [['BACKGROUND'], 'READ', 'NONE']) + assert.deepEqual([authority.contexts, authority.access, authority.idempotency], [['BACKGROUND'], 'READ_WRITE', 'MIXED']) + assert.deepEqual([supplier.contexts, supplier.access, supplier.idempotency], [['EXTERNAL'], 'READ', 'NONE']) + assert.equal(graphEdges('REQUEST').includes('lastleg-service->integration-service'), false) + assert.equal(graphEdges('REQUEST').includes('integration-service->lastleg-service'), false) + // The background service graph is bidirectional; the permit callback must not re-enter collection. + const egress = await readFile(path.join(root, + 'lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressService.java'), 'utf8') + assert.doesNotMatch(egress, /RestLingxingAnalyticsClient|LingxingAnalyticsClient|SelfopExecutionService/) + const callback = await readFile(path.join(root, authority.path), 'utf8') + assert.match(callback, /followRedirects\(HttpClient\.Redirect\.NEVER\)/) + assert.match(callback, /timeout\(Duration\.ofSeconds\(5\)\)/) + assert.doesNotMatch(callback, /RetryTemplate|Retryer|@Retryable|\.retry\(/) +}) + +test('new domain reference clients are exact readonly internal dependencies', async () => { + const reviewed = [ + ['lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentForecastClient.java', 'firstleg-service', '/internal/firstleg/appointment-forecasts/batch'], + ['lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentWarehouseClient.java', 'warehouse-service', '/internal/warehouses/order-catalog'], + ['lastleg-service/src/main/java/com/baiyue/lastleg/service/reference/ReturnCustomerReferenceClient.java', 'iam-service', '/internal/iam/finance-customers'], + ['lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/TransferWarehouseCatalogClient.java', 'warehouse-service', '/internal/warehouses/order-catalog'], + ['order/src/main/java/com/baiyue/order/client/LastlegOrderProgressClient.java', 'lastleg-service', '/internal/lastleg/v1/customer-order-progress'] + ] + for (const [file, target, endpoint] of reviewed) { + const client = SYNCHRONOUS_CLIENTS.find(row => row.path === file) + assert.deepEqual([client.target, client.contexts, client.access, client.idempotency], [target, ['REQUEST'], 'READ', 'SAFE_READ']) + const source = await readFile(path.join(root, file), 'utf8') + assert.ok(source.includes(endpoint)) + assert.ok(source.includes('X-Internal-Service-Token')) + assert.doesNotMatch(source, /@(?:PutMapping|DeleteMapping|PatchMapping)/) + } +}) + test('delivery fee coverage is a single readonly Finance hop and cannot write financial state', async () => { const client = await readFile(path.join(root, 'lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceFeeCoverageClient.java'), 'utf8') const service = await readFile(path.join(root, 'finance/src/main/java/com/baiyue/finance/service/DeliveryFeeCoverageQueryService.java'), 'utf8')