mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* feat(spring): model AOP advice and proxy behavior * fix(spring): address AOP review findings --------- Co-authored-by: Shining <xuenning@qiyi.com>
457 lines
26 KiB
TypeScript
457 lines
26 KiB
TypeScript
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
|
import { querySpringAopMetadata } from '../../src/mcp/local/aop-metadata.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 SERVICE_ID = 'Class:src/PaymentService.java:PaymentService';
|
|
const ASPECT_CLASS_ID = 'Class:src/AuditAspect.java:AuditAspect';
|
|
const NOISY_ASPECT_CLASS_ID = 'Class:src/NoisyAspect.java:NoisyAspect';
|
|
const PAY_ID = 'Method:src/PaymentService.java:PaymentService.pay#0';
|
|
const CLASS_LEVEL_METHOD_ID = 'Method:src/PaymentService.java:PaymentService.list#0';
|
|
const AUDIT_ID = 'Method:src/AuditAspect.java:AuditAspect.audit#0';
|
|
const UNKNOWN_ADVICE_ID = 'Method:src/AuditAspect.java:AuditAspect.authorize#0';
|
|
const RESOLVED_ADVICE_ID = 'Method:src/AuditAspect.java:AuditAspect.resolvedPointcut#0';
|
|
const HIGH_FAN_IN_ADVICE_ID = 'Method:src/AuditAspect.java:AuditAspect.hotAdvice#0';
|
|
const NOISY_ADVICE_ID = 'Method:src/NoisyAspect.java:NoisyAspect.noisyAdvice#0';
|
|
const NOISY_ADVISED_SOURCE_ID = 'Method:src/NoisySource.java:NoisySource.run#0';
|
|
const NOISY_ADVICE_TARGET_ID = 'Method:src/NoisyTarget.java:NoisyTarget.advise#0';
|
|
const FOREIGN_ADVISED_SOURCE_ID = 'Method:src/ForeignSource.java:ForeignSource.run#0';
|
|
const FOREIGN_ADVICE_TARGET_ID = 'Method:src/ForeignTarget.java:ForeignTarget.advise#0';
|
|
const TRUNCATED_SOURCE_ID = 'Method:src/TruncatedSource.java:TruncatedSource.run#0';
|
|
const TRUNCATED_TARGET_ID = 'Method:src/TruncatedTarget.java:TruncatedTarget.advise#0';
|
|
const PLAIN_ID = 'Method:src/Plain.java:Plain.run#0';
|
|
const CLASS_BEHAVIOR_ID = `CodeElement:spring-aop:${SERVICE_ID}:transactional`;
|
|
const METHOD_BEHAVIOR_ID = `CodeElement:spring-aop:${PAY_ID}:cacheable`;
|
|
const UNKNOWN_POINTCUT_ID = `CodeElement:spring-aop:${UNKNOWN_ADVICE_ID}:pointcut`;
|
|
const RESOLVED_POINTCUT_ID = `CodeElement:spring-aop:${RESOLVED_ADVICE_ID}:pointcut`;
|
|
const UNRELATED_DECLARATION_ID = `CodeElement:spring-bean:${PLAIN_ID}`;
|
|
const ASPECT_EVIDENCE_ID = `CodeElement:spring-aop:${ASPECT_CLASS_ID}:aspect`;
|
|
const NOISY_ASPECT_EVIDENCE_ID = `CodeElement:spring-aop:${NOISY_ASPECT_CLASS_ID}:aspect`;
|
|
const NOISY_POINTCUT_ID = `CodeElement:spring-aop:${NOISY_ADVICE_ID}:pointcut`;
|
|
|
|
const springReason = (value: object): string => `spring-aop:v1:${JSON.stringify(value)}`;
|
|
|
|
const CLASS_BEHAVIOR_REASON = springReason({
|
|
kind: 'behavior',
|
|
annotation: 'org.springframework.transaction.annotation.Transactional',
|
|
behavior: 'transactional',
|
|
declaredOn: 'class',
|
|
activation: 'unknown',
|
|
proxy: 'possible',
|
|
});
|
|
const METHOD_BEHAVIOR_REASON = springReason({
|
|
kind: 'behavior',
|
|
annotation: 'org.springframework.cache.annotation.Cacheable',
|
|
behavior: 'cacheable',
|
|
declaredOn: 'method',
|
|
activation: 'unknown',
|
|
proxy: 'possible',
|
|
});
|
|
const ADVICE_REASON = springReason({
|
|
kind: 'advice',
|
|
annotation: 'org.aspectj.lang.annotation.Around',
|
|
advice: 'around',
|
|
pointcut: 'execution(* com.example.PaymentService.pay(..))',
|
|
match: 'static',
|
|
activation: 'unknown',
|
|
proxy: 'possible',
|
|
});
|
|
const UNKNOWN_POINTCUT_REASON = springReason({
|
|
kind: 'pointcut',
|
|
annotation: 'org.aspectj.lang.annotation.Before',
|
|
pointcut: 'securedOperation()',
|
|
match: 'unresolved',
|
|
resolution: 'unknown',
|
|
});
|
|
const RESOLVED_POINTCUT_REASON = springReason({
|
|
kind: 'pointcut',
|
|
annotation: 'org.aspectj.lang.annotation.Pointcut',
|
|
pointcut: 'execution(* com.example.PaymentService.pay(..))',
|
|
match: 'static',
|
|
resolution: 'resolved',
|
|
});
|
|
const ASPECT_REASON = springReason({
|
|
kind: 'aspect',
|
|
annotation: 'org.aspectj.lang.annotation.Aspect',
|
|
activation: 'unknown',
|
|
registration: 'unknown',
|
|
});
|
|
|
|
const HIGH_FAN_IN_ADVISED_IDS = Array.from(
|
|
{ length: 31 },
|
|
(_, index) => `Method:src/FanInService.java:FanInService.advised${index}#0`,
|
|
);
|
|
const HIGH_FAN_IN_CALLER_IDS = Array.from(
|
|
{ length: 31 },
|
|
(_, index) => `Method:src/LegacyCaller.java:LegacyCaller.call${index}#0`,
|
|
);
|
|
|
|
const SEED = [
|
|
`CREATE (c:Class {id:'${SERVICE_ID}', name:'PaymentService', filePath:'src/PaymentService.java', startLine:0, endLine:20, isExported:false, content:'class PaymentService {}', description:'', frameworkAnnotations:[]})`,
|
|
`CREATE (c:Class {id:'${ASPECT_CLASS_ID}', name:'AuditAspect', filePath:'src/AuditAspect.java', startLine:0, endLine:20, isExported:false, content:'class AuditAspect {}', description:'', frameworkAnnotations:[]})`,
|
|
`CREATE (c:Class {id:'${NOISY_ASPECT_CLASS_ID}', name:'NoisyAspect', filePath:'src/NoisyAspect.java', startLine:0, endLine:20, isExported:false, content:'class NoisyAspect {}', description:'', frameworkAnnotations:[]})`,
|
|
`CREATE (m:Method {id:'${PAY_ID}', name:'pay', filePath:'src/PaymentService.java', startLine:4, endLine:8, isExported:false, content:'void pay() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${CLASS_LEVEL_METHOD_ID}', name:'list', filePath:'src/PaymentService.java', startLine:10, endLine:12, isExported:false, content:'void list() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${AUDIT_ID}', name:'audit', filePath:'src/AuditAspect.java', startLine:4, endLine:8, isExported:false, content:'Object audit() {}', description:'', parameterCount:0, returnType:'Object'})`,
|
|
`CREATE (m:Method {id:'${UNKNOWN_ADVICE_ID}', name:'authorize', filePath:'src/AuditAspect.java', startLine:10, endLine:12, isExported:false, content:'void authorize() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${RESOLVED_ADVICE_ID}', name:'resolvedPointcut', filePath:'src/AuditAspect.java', startLine:12, endLine:13, isExported:false, content:'void resolvedPointcut() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${HIGH_FAN_IN_ADVICE_ID}', name:'hotAdvice', filePath:'src/AuditAspect.java', startLine:14, endLine:16, isExported:false, content:'void hotAdvice() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${NOISY_ADVICE_ID}', name:'noisyAdvice', filePath:'src/NoisyAspect.java', startLine:4, endLine:8, isExported:false, content:'void noisyAdvice() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${NOISY_ADVISED_SOURCE_ID}', name:'run', filePath:'src/NoisySource.java', startLine:1, endLine:2, isExported:false, content:'void run() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${NOISY_ADVICE_TARGET_ID}', name:'advise', filePath:'src/NoisyTarget.java', startLine:1, endLine:2, isExported:false, content:'void advise() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${FOREIGN_ADVISED_SOURCE_ID}', name:'run', filePath:'src/ForeignSource.java', startLine:1, endLine:2, isExported:false, content:'void run() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${FOREIGN_ADVICE_TARGET_ID}', name:'advise', filePath:'src/ForeignTarget.java', startLine:1, endLine:2, isExported:false, content:'void advise() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${TRUNCATED_SOURCE_ID}', name:'run', filePath:'src/TruncatedSource.java', startLine:1, endLine:2, isExported:false, content:'void run() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (m:Method {id:'${TRUNCATED_TARGET_ID}', name:'advise', filePath:'src/TruncatedTarget.java', startLine:1, endLine:2, isExported:false, content:'void advise() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
...HIGH_FAN_IN_ADVISED_IDS.map(
|
|
(id, index) =>
|
|
`CREATE (m:Method {id:'${id}', name:'advised${index}', filePath:'src/FanInService.java', startLine:${index}, endLine:${index}, isExported:false, content:'void advised${index}() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
),
|
|
...HIGH_FAN_IN_CALLER_IDS.map(
|
|
(id, index) =>
|
|
`CREATE (m:Method {id:'${id}', name:'call${index}', filePath:'src/LegacyCaller.java', startLine:${index}, endLine:${index}, isExported:false, content:'void call${index}() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
),
|
|
`CREATE (m:Method {id:'${PLAIN_ID}', name:'run', filePath:'src/Plain.java', startLine:1, endLine:2, isExported:false, content:'void run() {}', description:'', parameterCount:0, returnType:'void'})`,
|
|
`CREATE (e:CodeElement {id:'${CLASS_BEHAVIOR_ID}', name:'Transactional', filePath:'src/PaymentService.java', startLine:0, endLine:0, isExported:false, content:'', description:'Spring AOP behavior evidence'})`,
|
|
`CREATE (e:CodeElement {id:'${METHOD_BEHAVIOR_ID}', name:'Cacheable', filePath:'src/PaymentService.java', startLine:4, endLine:4, isExported:false, content:'', description:'Spring AOP behavior evidence'})`,
|
|
`CREATE (e:CodeElement {id:'${UNKNOWN_POINTCUT_ID}', name:'securedOperation()', filePath:'src/AuditAspect.java', startLine:10, endLine:10, isExported:false, content:'', description:'Spring AOP unresolved pointcut evidence'})`,
|
|
`CREATE (e:CodeElement {id:'${RESOLVED_POINTCUT_ID}', name:'pay()', filePath:'src/AuditAspect.java', startLine:12, endLine:12, isExported:false, content:'', description:'Spring AOP resolved pointcut evidence'})`,
|
|
`CREATE (e:CodeElement {id:'${UNRELATED_DECLARATION_ID}', name:'plain', filePath:'src/Plain.java', startLine:1, endLine:1, isExported:false, content:'', description:'Spring Bean factory declaration'})`,
|
|
`CREATE (e:CodeElement {id:'${ASPECT_EVIDENCE_ID}', name:'Aspect', filePath:'src/AuditAspect.java', startLine:0, endLine:0, isExported:false, content:'', description:'Spring AOP aspect evidence'})`,
|
|
`CREATE (e:CodeElement {id:'${NOISY_ASPECT_EVIDENCE_ID}', name:'Aspect', filePath:'src/NoisyAspect.java', startLine:0, endLine:0, isExported:false, content:'', description:'Spring AOP aspect evidence'})`,
|
|
`CREATE (e:CodeElement {id:'${NOISY_POINTCUT_ID}', name:'securedOperation()', filePath:'src/NoisyAspect.java', startLine:4, endLine:4, isExported:false, content:'', description:'Spring AOP unresolved pointcut evidence'})`,
|
|
`MATCH (c:Class {id:'${SERVICE_ID}'}), (e:CodeElement {id:'${CLASS_BEHAVIOR_ID}'}) CREATE (c)-[:CodeRelation {type:'ADVISED_BY', confidence:1.0, reason:'${CLASS_BEHAVIOR_REASON}', step:0}]->(e)`,
|
|
`MATCH (m:Method {id:'${CLASS_LEVEL_METHOD_ID}'}), (e:CodeElement {id:'${CLASS_BEHAVIOR_ID}'}) CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:1.0, reason:'${CLASS_BEHAVIOR_REASON}', step:0}]->(e)`,
|
|
`MATCH (m:Method {id:'${PAY_ID}'}), (e:CodeElement {id:'${METHOD_BEHAVIOR_ID}'}) CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:1.0, reason:'${METHOD_BEHAVIOR_REASON}', step:0}]->(e)`,
|
|
// Duplicate evidence exercises read-side deduplication for indexes produced
|
|
// by an interrupted/retried incremental write.
|
|
`MATCH (m:Method {id:'${PAY_ID}'}), (e:CodeElement {id:'${METHOD_BEHAVIOR_ID}'}) CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:1.0, reason:'${METHOD_BEHAVIOR_REASON}', step:0}]->(e)`,
|
|
`MATCH (m:Method {id:'${PAY_ID}'}), (a:Method {id:'${AUDIT_ID}'}) CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:0.9, reason:'${ADVICE_REASON}', step:0}]->(a)`,
|
|
`MATCH (a:Method {id:'${UNKNOWN_ADVICE_ID}'}), (e:CodeElement {id:'${UNKNOWN_POINTCUT_ID}'}) CREATE (a)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'${UNKNOWN_POINTCUT_REASON}', step:0}]->(e)`,
|
|
`MATCH (a:Method {id:'${RESOLVED_ADVICE_ID}'}), (e:CodeElement {id:'${RESOLVED_POINTCUT_ID}'}) CREATE (a)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'${RESOLVED_POINTCUT_REASON}', step:0}]->(e)`,
|
|
`MATCH (c:Class {id:'${ASPECT_CLASS_ID}'}), (e:CodeElement {id:'${ASPECT_EVIDENCE_ID}'}) CREATE (c)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'${ASPECT_REASON}', step:0}]->(e)`,
|
|
`MATCH (m:Method {id:'${PLAIN_ID}'}), (e:CodeElement {id:'${UNRELATED_DECLARATION_ID}'}) CREATE (m)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'spring-bean-factory:{"names":["plain"],"namesKnown":true}', step:0}]->(e)`,
|
|
`MATCH (c:Class {id:'${NOISY_ASPECT_CLASS_ID}'}), (e:CodeElement {id:'${UNRELATED_DECLARATION_ID}'}) UNWIND range(1, 1001) AS ignored CREATE (c)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'spring-bean-factory:{"names":["noise"],"namesKnown":true}', step:ignored}]->(e)`,
|
|
`MATCH (c:Class {id:'${NOISY_ASPECT_CLASS_ID}'}), (e:CodeElement {id:'${NOISY_ASPECT_EVIDENCE_ID}'}) CREATE (c)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'${ASPECT_REASON}', step:0}]->(e)`,
|
|
`MATCH (m:Method {id:'${PLAIN_ID}'}), (e:CodeElement {id:'${NOISY_POINTCUT_ID}'}) UNWIND range(1, 1001) AS ignored CREATE (m)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'spring-bean-factory:{"names":["noise"],"namesKnown":true}', step:ignored}]->(e)`,
|
|
`MATCH (m:Method {id:'${NOISY_ADVICE_ID}'}), (e:CodeElement {id:'${NOISY_POINTCUT_ID}'}) CREATE (m)-[:CodeRelation {type:'DECLARES', confidence:1.0, reason:'${UNKNOWN_POINTCUT_REASON}', step:0}]->(e)`,
|
|
`MATCH (m:Method {id:'${NOISY_ADVISED_SOURCE_ID}'}), (a:Method {id:'${NOISY_ADVICE_TARGET_ID}'}) CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:0.9, reason:'${ADVICE_REASON}', step:0}]->(a)`,
|
|
`MATCH (m:Method {id:'${NOISY_ADVISED_SOURCE_ID}'}), (a:Method {id:'${FOREIGN_ADVICE_TARGET_ID}'}) UNWIND range(1, 1001) AS ignored CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:0.1, reason:'foreign-advice', step:ignored}]->(a)`,
|
|
`MATCH (m:Method {id:'${FOREIGN_ADVISED_SOURCE_ID}'}), (a:Method {id:'${NOISY_ADVICE_TARGET_ID}'}) UNWIND range(1, 1001) AS ignored CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:0.1, reason:'foreign-advice', step:ignored}]->(a)`,
|
|
`MATCH (m:Method {id:'${TRUNCATED_SOURCE_ID}'}), (a:Method {id:'${TRUNCATED_TARGET_ID}'}) UNWIND range(1, 1001) AS item CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:0.9, reason:'${ADVICE_REASON}', step:item}]->(a)`,
|
|
...HIGH_FAN_IN_ADVISED_IDS.map(
|
|
(id) =>
|
|
`MATCH (m:Method {id:'${id}'}), (a:Method {id:'${HIGH_FAN_IN_ADVICE_ID}'}) CREATE (m)-[:CodeRelation {type:'ADVISED_BY', confidence:0.9, reason:'${ADVICE_REASON}', step:0}]->(a)`,
|
|
),
|
|
...HIGH_FAN_IN_CALLER_IDS.map(
|
|
(id) =>
|
|
`MATCH (m:Method {id:'${id}'}), (a:Method {id:'${HIGH_FAN_IN_ADVICE_ID}'}) CREATE (m)-[:CodeRelation {type:'CALLS', confidence:1.0, reason:'test fixture', step:0}]->(a)`,
|
|
),
|
|
];
|
|
|
|
withTestLbugDB(
|
|
'spring-aop-mcp',
|
|
(handle) => {
|
|
let backend: LocalBackend;
|
|
|
|
beforeAll(() => {
|
|
backend = (handle as typeof handle & { _backend: LocalBackend })._backend;
|
|
});
|
|
|
|
describe('Spring AOP metadata enrichment', () => {
|
|
it('normalizes declarative behavior and explicit advice for an advised method', async () => {
|
|
const metadata = await querySpringAopMetadata(handle.repoId, PAY_ID, 'Method');
|
|
|
|
expect(metadata).toEqual({
|
|
framework: 'spring',
|
|
proxied: 'possible',
|
|
behaviors: [
|
|
{
|
|
annotation: 'org.springframework.cache.annotation.Cacheable',
|
|
behavior: 'cacheable',
|
|
declaredOn: 'method',
|
|
activation: 'unknown',
|
|
evidenceId: METHOD_BEHAVIOR_ID,
|
|
},
|
|
],
|
|
advices: [
|
|
{
|
|
annotation: 'org.aspectj.lang.annotation.Around',
|
|
advice: 'around',
|
|
pointcut: 'execution(* com.example.PaymentService.pay(..))',
|
|
match: 'static',
|
|
activation: 'unknown',
|
|
adviceId: AUDIT_ID,
|
|
adviceName: 'audit',
|
|
adviceFilePath: 'src/AuditAspect.java',
|
|
advisedId: PAY_ID,
|
|
advisedName: 'pay',
|
|
advisedFilePath: 'src/PaymentService.java',
|
|
},
|
|
],
|
|
resolvedPointcuts: [],
|
|
unresolvedPointcuts: [],
|
|
});
|
|
});
|
|
|
|
it('returns the same canonical advice direction from the advice method', async () => {
|
|
const metadata = await querySpringAopMetadata(handle.repoId, AUDIT_ID, 'Method');
|
|
|
|
expect(metadata?.advices).toEqual([
|
|
expect.objectContaining({
|
|
adviceId: AUDIT_ID,
|
|
advisedId: PAY_ID,
|
|
advice: 'around',
|
|
}),
|
|
]);
|
|
expect(metadata).not.toHaveProperty('proxied');
|
|
});
|
|
|
|
it('exposes the same AOP metadata through context and impact', async () => {
|
|
const [context, adviceContext, impact, adviceImpact] = await Promise.all([
|
|
backend.callTool('context', { uid: PAY_ID }),
|
|
backend.callTool('context', { uid: AUDIT_ID }),
|
|
backend.callTool('impact', {
|
|
target: 'pay',
|
|
direction: 'upstream',
|
|
}),
|
|
backend.callTool('impact', {
|
|
target: 'audit',
|
|
direction: 'upstream',
|
|
relationTypes: ['ADVISED_BY'],
|
|
includeTests: true,
|
|
}),
|
|
]);
|
|
|
|
expect(context.symbol.aop).toEqual(
|
|
expect.objectContaining({
|
|
framework: 'spring',
|
|
proxied: 'possible',
|
|
behaviors: [expect.objectContaining({ behavior: 'cacheable' })],
|
|
advices: [expect.objectContaining({ advice: 'around', adviceId: AUDIT_ID })],
|
|
}),
|
|
);
|
|
expect(context.outgoing.advised_by).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ uid: AUDIT_ID, name: 'audit' })]),
|
|
);
|
|
expect(adviceContext.incoming.advised_by).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ uid: PAY_ID, name: 'pay' })]),
|
|
);
|
|
expect(adviceContext.symbol.aop).not.toHaveProperty('proxied');
|
|
expect(impact.target.aop).toEqual(context.symbol.aop);
|
|
expect(adviceImpact.target.aop).toEqual(adviceContext.symbol.aop);
|
|
expect(adviceImpact.byDepth[1]).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: PAY_ID,
|
|
name: 'pay',
|
|
relationType: 'ADVISED_BY',
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('keeps legacy context relations when advice fan-in exceeds the context window', async () => {
|
|
const context = await backend.callTool('context', { uid: HIGH_FAN_IN_ADVICE_ID });
|
|
|
|
expect(context.incoming.calls).toHaveLength(30);
|
|
expect(context.incoming.advised_by).toHaveLength(30);
|
|
expect(context.symbol.aop.advices).toHaveLength(31);
|
|
});
|
|
|
|
it('supports Class and CodeElement behavior evidence', async () => {
|
|
const classMetadata = await querySpringAopMetadata(handle.repoId, SERVICE_ID, 'Class');
|
|
const classLevelMethodMetadata = await querySpringAopMetadata(
|
|
handle.repoId,
|
|
CLASS_LEVEL_METHOD_ID,
|
|
'Method',
|
|
);
|
|
const evidenceMetadata = await querySpringAopMetadata(
|
|
handle.repoId,
|
|
METHOD_BEHAVIOR_ID,
|
|
'CodeElement',
|
|
);
|
|
|
|
expect(classMetadata?.behaviors).toEqual([
|
|
expect.objectContaining({ behavior: 'transactional', declaredOn: 'class' }),
|
|
]);
|
|
expect(classMetadata?.proxied).toBe('possible');
|
|
expect(classLevelMethodMetadata?.behaviors).toEqual([
|
|
expect.objectContaining({ behavior: 'transactional', declaredOn: 'class' }),
|
|
]);
|
|
expect(classLevelMethodMetadata?.proxied).toBe('possible');
|
|
expect(evidenceMetadata?.behaviors).toEqual([
|
|
expect.objectContaining({ behavior: 'cacheable', evidenceId: METHOD_BEHAVIOR_ID }),
|
|
]);
|
|
expect(evidenceMetadata).not.toHaveProperty('proxied');
|
|
});
|
|
|
|
it('surfaces standalone Aspect declarations without claiming proxy activation', async () => {
|
|
const [classMetadata, evidenceMetadata, context] = await Promise.all([
|
|
querySpringAopMetadata(handle.repoId, ASPECT_CLASS_ID, 'Class'),
|
|
querySpringAopMetadata(handle.repoId, ASPECT_EVIDENCE_ID, 'CodeElement'),
|
|
backend.callTool('context', { uid: ASPECT_CLASS_ID }),
|
|
]);
|
|
const expectedAspect = {
|
|
annotation: 'org.aspectj.lang.annotation.Aspect',
|
|
activation: 'unknown',
|
|
registration: 'unknown',
|
|
evidenceId: ASPECT_EVIDENCE_ID,
|
|
};
|
|
|
|
expect(classMetadata?.aspect).toEqual(expectedAspect);
|
|
expect(classMetadata).not.toHaveProperty('proxied');
|
|
expect(evidenceMetadata?.aspect).toEqual(expectedAspect);
|
|
expect(evidenceMetadata).not.toHaveProperty('proxied');
|
|
expect(context.symbol.aop.aspect).toEqual(expectedAspect);
|
|
expect(context.symbol.aop).not.toHaveProperty('proxied');
|
|
});
|
|
|
|
it('surfaces unresolved pointcuts without guessing an advised target', async () => {
|
|
const adviceMetadata = await querySpringAopMetadata(
|
|
handle.repoId,
|
|
UNKNOWN_ADVICE_ID,
|
|
'Method',
|
|
);
|
|
const evidenceMetadata = await querySpringAopMetadata(
|
|
handle.repoId,
|
|
UNKNOWN_POINTCUT_ID,
|
|
'CodeElement',
|
|
);
|
|
|
|
const expected = [
|
|
{
|
|
annotation: 'org.aspectj.lang.annotation.Before',
|
|
pointcut: 'securedOperation()',
|
|
adviceId: UNKNOWN_ADVICE_ID,
|
|
adviceName: 'authorize',
|
|
adviceFilePath: 'src/AuditAspect.java',
|
|
evidenceId: UNKNOWN_POINTCUT_ID,
|
|
},
|
|
];
|
|
expect(adviceMetadata).toEqual({
|
|
framework: 'spring',
|
|
behaviors: [],
|
|
advices: [],
|
|
resolvedPointcuts: [],
|
|
unresolvedPointcuts: expected,
|
|
});
|
|
expect(evidenceMetadata?.unresolvedPointcuts).toEqual(expected);
|
|
});
|
|
|
|
it('surfaces resolved standalone pointcut declarations from both endpoints', async () => {
|
|
const [adviceMetadata, evidenceMetadata] = await Promise.all([
|
|
querySpringAopMetadata(handle.repoId, RESOLVED_ADVICE_ID, 'Method'),
|
|
querySpringAopMetadata(handle.repoId, RESOLVED_POINTCUT_ID, 'CodeElement'),
|
|
]);
|
|
const expected = [
|
|
{
|
|
annotation: 'org.aspectj.lang.annotation.Pointcut',
|
|
pointcut: 'execution(* com.example.PaymentService.pay(..))',
|
|
match: 'static',
|
|
resolution: 'resolved',
|
|
adviceId: RESOLVED_ADVICE_ID,
|
|
adviceName: 'resolvedPointcut',
|
|
adviceFilePath: 'src/AuditAspect.java',
|
|
evidenceId: RESOLVED_POINTCUT_ID,
|
|
},
|
|
];
|
|
|
|
expect(adviceMetadata?.resolvedPointcuts).toEqual(expected);
|
|
expect(evidenceMetadata?.resolvedPointcuts).toEqual(expected);
|
|
});
|
|
|
|
it('orders capped rows deterministically and reports positive truncation', async () => {
|
|
const first = await querySpringAopMetadata(handle.repoId, TRUNCATED_SOURCE_ID, 'Method');
|
|
const second = await querySpringAopMetadata(handle.repoId, TRUNCATED_SOURCE_ID, 'Method');
|
|
|
|
expect(first?.truncated).toBe(true);
|
|
expect(first?.advices).toEqual([
|
|
expect.objectContaining({
|
|
advisedId: TRUNCATED_SOURCE_ID,
|
|
adviceId: TRUNCATED_TARGET_ID,
|
|
}),
|
|
]);
|
|
expect(second).toEqual(first);
|
|
});
|
|
|
|
it('does not let unrelated DECLARES exhaust the Spring AOP query budget', async () => {
|
|
const [aspectMetadata, pointcutMetadata] = await Promise.all([
|
|
querySpringAopMetadata(handle.repoId, NOISY_ASPECT_CLASS_ID, 'Class'),
|
|
querySpringAopMetadata(handle.repoId, NOISY_POINTCUT_ID, 'CodeElement'),
|
|
]);
|
|
|
|
expect(aspectMetadata?.aspect).toEqual({
|
|
annotation: 'org.aspectj.lang.annotation.Aspect',
|
|
activation: 'unknown',
|
|
registration: 'unknown',
|
|
evidenceId: NOISY_ASPECT_EVIDENCE_ID,
|
|
});
|
|
expect(aspectMetadata).not.toHaveProperty('truncated');
|
|
expect(pointcutMetadata?.unresolvedPointcuts).toEqual([
|
|
{
|
|
annotation: 'org.aspectj.lang.annotation.Before',
|
|
pointcut: 'securedOperation()',
|
|
adviceId: NOISY_ADVICE_ID,
|
|
adviceName: 'noisyAdvice',
|
|
adviceFilePath: 'src/NoisyAspect.java',
|
|
evidenceId: NOISY_POINTCUT_ID,
|
|
},
|
|
]);
|
|
expect(pointcutMetadata).not.toHaveProperty('truncated');
|
|
});
|
|
|
|
it('does not let unrelated ADVISED_BY exhaust either AOP query direction', async () => {
|
|
const [sourceMetadata, targetMetadata] = await Promise.all([
|
|
querySpringAopMetadata(handle.repoId, NOISY_ADVISED_SOURCE_ID, 'Method'),
|
|
querySpringAopMetadata(handle.repoId, NOISY_ADVICE_TARGET_ID, 'Method'),
|
|
]);
|
|
|
|
expect(sourceMetadata?.advices).toEqual([
|
|
expect.objectContaining({
|
|
advisedId: NOISY_ADVISED_SOURCE_ID,
|
|
adviceId: NOISY_ADVICE_TARGET_ID,
|
|
}),
|
|
]);
|
|
expect(targetMetadata?.advices).toEqual(sourceMetadata?.advices);
|
|
expect(sourceMetadata).not.toHaveProperty('truncated');
|
|
expect(targetMetadata).not.toHaveProperty('truncated');
|
|
});
|
|
|
|
it('ignores unrelated DECLARES evidence and unsupported symbol kinds', async () => {
|
|
await expect(
|
|
querySpringAopMetadata(handle.repoId, PLAIN_ID, 'Method'),
|
|
).resolves.toBeUndefined();
|
|
await expect(
|
|
querySpringAopMetadata(handle.repoId, 'Function:src/plain.ts:run', 'Function'),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
},
|
|
{
|
|
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: 3, nodes: 12, communities: 0, processes: 0 },
|
|
},
|
|
]);
|
|
const backend = new LocalBackend();
|
|
await backend.init();
|
|
(handle as typeof handle & { _backend?: LocalBackend })._backend = backend;
|
|
},
|
|
},
|
|
);
|