GitNexus/gitnexus/test/integration/spring-bean-metadata-roundtrip.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

209 lines
7.5 KiB
TypeScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { expect, it } from 'vitest';
import { buildTestGraph } from '../helpers/test-graph.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
import { streamAllCSVsToDisk } from '../../src/core/lbug/csv-generator.js';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
const CLASS_ID = 'Class:src/BillingService.java:BillingService';
const FRAMEWORK_MARKER = 'com.acme.FrameworkMarker';
const itLbugReopen = process.platform === 'win32' ? it.skip : it;
withTestLbugDB('spring-bean-metadata-roundtrip', (handle) => {
it('preserves Class framework annotations through all write paths', async () => {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
const graph = buildTestGraph([
{
id: CLASS_ID,
label: 'Class',
name: 'BillingService',
filePath: 'src/BillingService.java',
extra: {
frameworkAnnotations: ['org.springframework.stereotype.Service', FRAMEWORK_MARKER],
},
},
]);
const csvDir = path.join(handle.tmpHandle.dbPath, 'csv-spring-bean');
const repoDir = path.join(handle.tmpHandle.dbPath, 'repo-spring-bean');
await fs.mkdir(repoDir, { recursive: true });
await streamAllCSVsToDisk(graph, repoDir, csvDir);
const classCsvPath = path.join(csvDir, 'class.csv');
const classCsv = await fs.readFile(classCsvPath, 'utf8');
expect(classCsv.split('\n')[0]).toBe(
'id,name,filePath,startLine,endLine,isExported,content,description,frameworkAnnotations',
);
expect(classCsv).toContain('org.springframework.stereotype.Service');
expect(classCsv).toContain(FRAMEWORK_MARKER);
await adapter.executeQuery(adapter.getCopyQuery('Class', classCsvPath.replace(/\\/g, '/')));
expect(
await adapter.executeQuery(
`MATCH (c:Class {id: '${CLASS_ID}'}) RETURN c.frameworkAnnotations AS frameworkAnnotations`,
),
).toEqual([
{
frameworkAnnotations: ['org.springframework.stereotype.Service', FRAMEWORK_MARKER],
},
]);
expect(
await adapter.insertNodeToLbug('Class', {
id: 'Class:src/Widget.java:Widget',
name: 'Widget',
filePath: 'src/Widget.java',
frameworkAnnotations: ['org.springframework.stereotype.Component'],
}),
).toBe(true);
expect(
await adapter.executeQuery(
`MATCH (c:Class {id: 'Class:src/Widget.java:Widget'}) RETURN c.frameworkAnnotations AS frameworkAnnotations`,
),
).toEqual([
{
frameworkAnnotations: ['org.springframework.stereotype.Component'],
},
]);
});
itLbugReopen('preserves Class framework annotations through batch upserts', async () => {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
// The batch helper owns its connection, so release the singleton lock for
// the call and restore it before the shared fixture tears down.
await adapter.closeLbug();
let upsertResult: { inserted: number; failed: number };
try {
upsertResult = await adapter.batchInsertNodesToLbug(
[
{
label: 'Class',
properties: {
id: CLASS_ID,
name: 'BillingService',
filePath: 'src/BillingService.java',
frameworkAnnotations: ['org.springframework.stereotype.Repository', FRAMEWORK_MARKER],
},
},
],
handle.dbPath,
);
} finally {
await adapter.initLbug(handle.dbPath);
}
expect(upsertResult).toEqual({ inserted: 1, failed: 0 });
expect(
await adapter.executeQuery(
`MATCH (c:Class {id: '${CLASS_ID}'}) RETURN c.frameworkAnnotations AS frameworkAnnotations`,
),
).toEqual([
{
frameworkAnnotations: ['org.springframework.stereotype.Repository', FRAMEWORK_MARKER],
},
]);
});
it('rejects framework annotation items that COPY cannot encode losslessly', async () => {
const graph = buildTestGraph([
{
id: 'Class:src/Unsafe.java:Unsafe',
label: 'Class',
name: 'Unsafe',
filePath: 'src/Unsafe.java',
extra: { frameworkAnnotations: ['com.acme.Has,Comma'] },
},
]);
const csvDir = path.join(handle.tmpHandle.dbPath, 'csv-unsafe-framework-annotation');
const repoDir = path.join(handle.tmpHandle.dbPath, 'repo-unsafe-framework-annotation');
await fs.mkdir(repoDir, { recursive: true });
await expect(streamAllCSVsToDisk(graph, repoDir, csvDir)).rejects.toThrow(
'Cannot safely encode CSV string-list item',
);
});
it('persists pipeline-produced Class and Method INJECTS edges to Bean declarations', async () => {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
const root = path.join(handle.tmpHandle.dbPath, 'spring-bean-injects-roundtrip');
const repoDir = path.join(root, 'repo');
const storageDir = path.join(root, 'storage');
await fs.mkdir(repoDir, { recursive: true });
await fs.mkdir(storageDir, { recursive: true });
await Promise.all([
fs.writeFile(
path.join(repoDir, 'Gateway.java'),
`package com.persisted;
public interface Gateway {}
`,
),
fs.writeFile(
path.join(repoDir, 'DefaultGateway.java'),
`package com.persisted;
public class DefaultGateway implements Gateway {}
`,
),
fs.writeFile(
path.join(repoDir, 'PersistedConfig.java'),
`package com.persisted;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PersistedConfig {
@Bean Gateway persistedGateway() { return new DefaultGateway(); }
@Bean Object persistedAggregate(Gateway gateway) { return new Object(); }
}
`,
),
fs.writeFile(
path.join(repoDir, 'PersistedConsumer.java'),
`package com.persisted;
import jakarta.annotation.Resource;
public class PersistedConsumer {
@Resource(name = "persistedGateway") Gateway selected;
}
`,
),
]);
const { graph } = await runPipelineFromRepo(repoDir, () => {}, {});
expect(
graph.relationships.some(
(relationship) =>
relationship.type === 'INJECTS' &&
graph.getNode(relationship.sourceId)?.properties.name === 'PersistedConsumer' &&
graph.getNode(relationship.targetId)?.properties.name === 'persistedGateway',
),
).toBe(true);
expect(
graph.relationships.some(
(relationship) =>
relationship.type === 'INJECTS' &&
graph.getNode(relationship.sourceId)?.properties.name === 'persistedAggregate' &&
graph.getNode(relationship.targetId)?.properties.name === 'persistedGateway',
),
).toBe(true);
await adapter.loadGraphToLbug(graph, repoDir, storageDir);
expect(
await adapter.executeQuery(
`MATCH (source:Class)-[r:CodeRelation]->(target:CodeElement)
WHERE r.type = 'INJECTS'
AND source.name = 'PersistedConsumer'
AND target.name = 'persistedGateway'
RETURN source.name AS source, target.name AS target`,
),
).toEqual([{ source: 'PersistedConsumer', target: 'persistedGateway' }]);
expect(
await adapter.executeQuery(
`MATCH (source:Method)-[r:CodeRelation]->(target:CodeElement)
WHERE r.type = 'INJECTS'
AND source.name = 'persistedAggregate'
AND target.name = 'persistedGateway'
RETURN source.name AS source, target.name AS target`,
),
).toEqual([{ source: 'persistedAggregate', target: 'persistedGateway' }]);
}, 90_000);
});