From 1aa4d34fcbd3301cbf348f082a8fe85b30d71fd5 Mon Sep 17 00:00:00 2001 From: liyipeng06 Date: Tue, 28 Apr 2026 16:27:44 +0800 Subject: [PATCH] feat(group): run thrift extractor in group sync --- gitnexus/src/core/group/sync.ts | 13 ++++ gitnexus/test/unit/group/sync.test.ts | 107 +++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/group/sync.ts b/gitnexus/src/core/group/sync.ts index f083a190e..0a70ae333 100644 --- a/gitnexus/src/core/group/sync.ts +++ b/gitnexus/src/core/group/sync.ts @@ -6,6 +6,7 @@ import { readRegistry, type RegistryEntry } from '../../storage/repo-manager.js' import type { GroupConfig, RepoHandle, RepoSnapshot, StoredContract, CrossLink } from './types.js'; import { HttpRouteExtractor } from './extractors/http-route-extractor.js'; import { GrpcExtractor } from './extractors/grpc-extractor.js'; +import { ThriftExtractor } from './extractors/thrift-extractor.js'; import { TopicExtractor } from './extractors/topic-extractor.js'; import { ManifestExtractor } from './extractors/manifest-extractor.js'; import { buildProviderIndex, runExactMatch, runWildcardMatch } from './matching.js'; @@ -93,6 +94,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis const resolve = opts?.resolveRepoHandle ?? defaultResolveHandle(entries); const httpEx = new HttpRouteExtractor(); const grpcEx = new GrpcExtractor(); + const thriftEx = new ThriftExtractor(); const topicEx = new TopicExtractor(); dbExecutors = new Map(); const openPoolIds: string[] = []; @@ -140,6 +142,17 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis } } + if (config.detect.thrift) { + const extracted = await thriftEx.extract(executor, handle.repoPath, handle); + for (const c of extracted) { + autoContracts.push({ + ...c, + repo: groupPath, + service: assignService(c.symbolRef.filePath, boundaries), + }); + } + } + if (config.detect.topics) { const extracted = await topicEx.extract(executor, handle.repoPath, handle); for (const c of extracted) { diff --git a/gitnexus/test/unit/group/sync.test.ts b/gitnexus/test/unit/group/sync.test.ts index 6ba969c2f..dc24b0b7f 100644 --- a/gitnexus/test/unit/group/sync.test.ts +++ b/gitnexus/test/unit/group/sync.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -22,6 +22,7 @@ describe('syncGroup', () => { detect: { http: true, grpc: false, + thrift: false, topics: false, shared_libs: false, embedding_fallback: false, @@ -228,6 +229,7 @@ describe('syncGroup', () => { detect: { http: true, grpc: false, + thrift: false, topics: false, shared_libs: false, embedding_fallback: false, @@ -339,6 +341,109 @@ describe('syncGroup', () => { expect(result.unmatched).toHaveLength(0); }); + it('extracts thrift contracts during real sync when thrift detection is enabled', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-sync-thrift-')); + const storageDir = path.join(tmpDir, '.gitnexus'); + fs.mkdirSync(path.join(tmpDir, 'services', 'billing', 'idl'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, 'services', 'billing', 'src'), { recursive: true }); + fs.mkdirSync(storageDir, { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'services', 'billing', 'package.json'), '{}'); + fs.writeFileSync( + path.join(tmpDir, 'services', 'billing', 'src', 'BillingWorkflow.java'), + 'package example; class BillingWorkflow {}', + ); + fs.writeFileSync( + path.join(tmpDir, 'services', 'billing', 'idl', 'order.thrift'), + `namespace java billing.v1 + +service OrderService { + PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request) +}`, + ); + + const config = makeConfig({ 'services/billing': 'billing-repo' }); + config.detect.http = false; + config.detect.thrift = true; + + const poolAdapter = await import('../../../src/core/lbug/pool-adapter.js'); + const initSpy = vi.spyOn(poolAdapter, 'initLbug').mockResolvedValue(undefined); + const closeSpy = vi.spyOn(poolAdapter, 'closeLbug').mockResolvedValue(undefined); + + try { + const result = await syncGroup(config, { + resolveRepoHandle: async (_name, groupPath) => ({ + id: 'billing-repo', + path: groupPath, + repoPath: tmpDir, + storagePath: storageDir, + }), + skipWrite: true, + }); + + expect(result.missingRepos).toHaveLength(0); + expect(result.contracts).toHaveLength(1); + expect(result.contracts[0]).toMatchObject({ + contractId: 'thrift::billing.v1.OrderService/PlaceOrder', + type: 'thrift', + role: 'provider', + repo: 'services/billing', + service: 'services/billing', + symbolRef: { + filePath: 'services/billing/idl/order.thrift', + name: 'OrderService.PlaceOrder', + }, + }); + expect(initSpy).toHaveBeenCalledWith('billing-repo', path.join(storageDir, 'lbug')); + expect(closeSpy).toHaveBeenCalledWith('billing-repo'); + } finally { + initSpy.mockRestore(); + closeSpy.mockRestore(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('does not extract thrift contracts during real sync when thrift detection is disabled', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-sync-thrift-off-')); + const storageDir = path.join(tmpDir, '.gitnexus'); + fs.mkdirSync(path.join(tmpDir, 'services', 'billing', 'idl'), { recursive: true }); + fs.mkdirSync(storageDir, { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, 'services', 'billing', 'idl', 'order.thrift'), + `namespace java billing.v1 + +service OrderService { + PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request) +}`, + ); + + const config = makeConfig({ 'services/billing': 'billing-repo' }); + config.detect.http = false; + config.detect.thrift = false; + + const poolAdapter = await import('../../../src/core/lbug/pool-adapter.js'); + const initSpy = vi.spyOn(poolAdapter, 'initLbug').mockResolvedValue(undefined); + const closeSpy = vi.spyOn(poolAdapter, 'closeLbug').mockResolvedValue(undefined); + + try { + const result = await syncGroup(config, { + resolveRepoHandle: async (_name, groupPath) => ({ + id: 'billing-repo', + path: groupPath, + repoPath: tmpDir, + storagePath: storageDir, + }), + skipWrite: true, + }); + + expect(result.missingRepos).toHaveLength(0); + expect(result.contracts).toHaveLength(0); + } finally { + initSpy.mockRestore(); + closeSpy.mockRestore(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it('dedupes duplicate wildcard cross-links during sync', async () => { const config = makeConfig({ 'app/provider': 'provider-repo', 'app/consumer': 'consumer-repo' }); const provider: StoredContract = {