GitNexus/gitnexus/test/integration/spring-bean-mcp.test.ts
MyShining de84ad6297
feat(spring): index @Bean factories and @Resource injection (#2740)
* feat(spring): index Bean factories and Resource injection

* fix(spring): address Bean and Resource review findings

* refactor(lbug): keep relation pair parsing in router

* test(lbug): preserve schema exports in WAL mocks

* test(cache): align schema bump pin

---------

Co-authored-by: Shining <xuenning@qiyi.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-07-31 10:33:42 +01:00

122 lines
6.4 KiB
TypeScript

import { beforeAll, describe, expect, it, vi } from 'vitest';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
vi.mock('../../src/storage/repo-manager.js', () => ({
listRegisteredRepos: vi.fn().mockResolvedValue([]),
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
findSiblingClones: vi.fn().mockResolvedValue([]),
}));
const BEAN_ID = 'Class:src/BillingService.java:BillingService';
const KOTLIN_BEAN_ID = 'Class:src/KotlinBillingService.kt:KotlinBillingService';
const PLAIN_ID = 'Class:src/PlainUtility.java:PlainUtility';
const NON_JAVA_ID = 'Class:src/AppProvider.ts:AppProvider';
const CONFLICT_ID = 'Class:src/ConflictingBean.java:ConflictingBean';
const FACTORY_METHOD_ID = 'Method:src/AppConfiguration.java:AppConfiguration.billingService#0';
const FACTORY_BEAN_ID = `CodeElement:spring-bean:${FACTORY_METHOD_ID}`;
const FACTORY_REASON =
'spring-bean-factory:{"names":["billingService","billingAlias"],"namesKnown":true,"providedType":"BillingService"}';
const SEED = [
`CREATE (c:Class {id:'${BEAN_ID}', name:'BillingService', filePath:'src/BillingService.java', startLine:0, endLine:3, isExported:false, content:'class BillingService {}', description:'', frameworkAnnotations:['org.springframework.stereotype.Service']})`,
`CREATE (c:Class {id:'${KOTLIN_BEAN_ID}', name:'KotlinBillingService', filePath:'src/KotlinBillingService.kt', startLine:0, endLine:3, isExported:false, content:'class KotlinBillingService', description:'', frameworkAnnotations:['org.springframework.stereotype.Service']})`,
`CREATE (c:Class {id:'${PLAIN_ID}', name:'PlainUtility', filePath:'src/PlainUtility.java', startLine:0, endLine:1, isExported:false, content:'class PlainUtility {}', description:'', frameworkAnnotations:[]})`,
`CREATE (c:Class {id:'${NON_JAVA_ID}', name:'AppProvider', filePath:'src/AppProvider.ts', startLine:0, endLine:1, isExported:true, content:'class AppProvider {}', description:'', frameworkAnnotations:['@nestjs/common.Injectable']})`,
`CREATE (c:Class {id:'${CONFLICT_ID}', name:'ConflictingBean', filePath:'src/ConflictingBean.java', startLine:0, endLine:1, isExported:false, content:'class ConflictingBean {}', description:'', frameworkAnnotations:['org.springframework.stereotype.Service', 'org.springframework.stereotype.Component']})`,
`CREATE (m:Method {id:'${FACTORY_METHOD_ID}', name:'billingService', filePath:'src/AppConfiguration.java', startLine:4, endLine:6, isExported:false, content:'@Bean BillingService billingService()', description:'', parameterCount:0, returnType:'BillingService'})`,
`CREATE (b:CodeElement {id:'${FACTORY_BEAN_ID}', name:'billingService', filePath:'src/AppConfiguration.java', startLine:4, endLine:6, isExported:false, content:'', description:'Spring Bean factory declaration'})`,
`MATCH (m:Method {id:'${FACTORY_METHOD_ID}'}), (b:CodeElement {id:'${FACTORY_BEAN_ID}'}) CREATE (m)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'${FACTORY_REASON}', step:0}]->(b)`,
];
withTestLbugDB(
'spring-bean-mcp',
(handle) => {
let backend: LocalBackend;
beforeAll(() => {
backend = (handle as typeof handle & { _backend: LocalBackend })._backend;
});
describe('Bean metadata MCP enrichment', () => {
it('returns the same nested Bean shape for Java and Kotlin from context and impact', async () => {
const javaContext = await backend.callTool('context', { uid: BEAN_ID });
const kotlinContext = await backend.callTool('context', { uid: KOTLIN_BEAN_ID });
const kotlinImpact = await backend.callTool('impact', {
target: 'KotlinBillingService',
direction: 'upstream',
});
const javaImpact = await backend.callTool('impact', {
target: 'BillingService',
direction: 'upstream',
});
const expectedBean = {
framework: 'spring',
role: 'service',
annotation: 'org.springframework.stereotype.Service',
};
expect(javaContext.symbol.bean).toEqual(expectedBean);
expect(kotlinContext.symbol.bean).toEqual(expectedBean);
expect(javaImpact.target.bean).toEqual(expectedBean);
expect(kotlinImpact.target.bean).toEqual(expectedBean);
});
it('omits Bean metadata for an ordinary Class', async () => {
const context = await backend.callTool('context', { uid: PLAIN_ID });
const impact = await backend.callTool('impact', {
target: 'PlainUtility',
direction: 'upstream',
});
expect(context.symbol).not.toHaveProperty('bean');
expect(impact.target).not.toHaveProperty('bean');
});
it('omits Spring Bean metadata for non-Spring and conflicting evidence', async () => {
const nonJava = await backend.callTool('context', { uid: NON_JAVA_ID });
const conflict = await backend.callTool('impact', {
target: 'ConflictingBean',
direction: 'upstream',
});
expect(nonJava.symbol).not.toHaveProperty('bean');
expect(conflict.target).not.toHaveProperty('bean');
});
it('enriches both Bean factory methods and their synthetic declarations', async () => {
const methodContext = await backend.callTool('context', { uid: FACTORY_METHOD_ID });
const declarationContext = await backend.callTool('context', { uid: FACTORY_BEAN_ID });
const expectedFactoryBean = {
framework: 'spring',
role: 'factory-method',
annotation: 'org.springframework.context.annotation.Bean',
names: ['billingService', 'billingAlias'],
providedType: 'BillingService',
};
expect(methodContext.symbol.bean).toEqual(expectedFactoryBean);
expect(declarationContext.symbol.bean).toEqual(expectedFactoryBean);
});
});
},
{
seed: SEED,
poolAdapter: true,
afterSetup: async (handle) => {
vi.mocked(listRegisteredRepos).mockResolvedValue([
{
name: 'test-repo',
path: '/test/repo',
storagePath: handle.tmpHandle.dbPath,
indexedAt: new Date().toISOString(),
lastCommit: 'abc123',
stats: { files: 4, nodes: 4, communities: 0, processes: 0 },
},
]);
const backend = new LocalBackend();
await backend.init();
(handle as typeof handle & { _backend?: LocalBackend })._backend = backend;
},
},
);