diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 957be8eab..1828c326a 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -47,7 +47,7 @@ export const VALID_NODE_LABELS = new Set([ ]); /** Valid relation types for impact analysis filtering */ -export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']); +export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']); /** Regex to detect write operations in user-supplied Cypher queries */ export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i; diff --git a/gitnexus/test/fixtures/local-backend-seed.ts b/gitnexus/test/fixtures/local-backend-seed.ts index f99311068..6958fe5b5 100644 --- a/gitnexus/test/fixtures/local-backend-seed.ts +++ b/gitnexus/test/fixtures/local-backend-seed.ts @@ -10,6 +10,10 @@ export const LOCAL_BACKEND_SEED_DATA = [ `CREATE (fn:Function {id: 'func:hash', name: 'hash', filePath: 'src/utils.ts', startLine: 1, endLine: 8, isExported: true, content: 'function hash() {}', description: 'Hash utility'})`, // Class `CREATE (c:Class {id: 'class:AuthService', name: 'AuthService', filePath: 'src/auth.ts', startLine: 30, endLine: 60, isExported: true, content: 'class AuthService {}', description: 'Authentication service'})`, + `CREATE (c:Class {id: 'class:BaseService', name: 'BaseService', filePath: 'src/base.ts', startLine: 1, endLine: 20, isExported: true, content: 'class BaseService {}', description: 'Base service class'})`, + // Methods + `CREATE (m:Method {id: 'method:AuthService.authenticate', name: 'authenticate', filePath: 'src/auth.ts', startLine: 35, endLine: 45, isExported: false, content: 'authenticate() {}', description: 'Authenticate user'})`, + `CREATE (m:Method {id: 'method:BaseService.authenticate', name: 'authenticate', filePath: 'src/base.ts', startLine: 5, endLine: 10, isExported: false, content: 'authenticate() {}', description: 'Base authenticate'})`, // Community `CREATE (c:Community {id: 'comm:auth', label: 'Auth', heuristicLabel: 'Authentication', keywords: ['auth', 'login'], description: 'Auth module', enrichedBy: 'heuristic', cohesion: 0.8, symbolCount: 3})`, // Process @@ -25,10 +29,20 @@ export const LOCAL_BACKEND_SEED_DATA = [ CREATE (a)-[:CodeRelation {type: 'STEP_IN_PROCESS', confidence: 1.0, reason: '', step: 1}]->(p)`, `MATCH (a:Function), (p:Process) WHERE a.id = 'func:validate' AND p.id = 'proc:login-flow' CREATE (a)-[:CodeRelation {type: 'STEP_IN_PROCESS', confidence: 1.0, reason: '', step: 2}]->(p)`, + // HAS_METHOD: AuthService -> authenticate + `MATCH (c:Class), (m:Method) WHERE c.id = 'class:AuthService' AND m.id = 'method:AuthService.authenticate' + CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`, + // OVERRIDES: AuthService.authenticate -> BaseService.authenticate + `MATCH (a:Method), (b:Method) WHERE a.id = 'method:AuthService.authenticate' AND b.id = 'method:BaseService.authenticate' + CREATE (a)-[:CodeRelation {type: 'OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`, + // HAS_METHOD: BaseService -> authenticate + `MATCH (c:Class), (m:Method) WHERE c.id = 'class:BaseService' AND m.id = 'method:BaseService.authenticate' + CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`, ]; export const LOCAL_BACKEND_FTS_INDEXES: FTSIndexDef[] = [ { table: 'Function', indexName: 'function_fts', columns: ['name', 'content', 'description'] }, { table: 'Class', indexName: 'class_fts', columns: ['name', 'content', 'description'] }, + { table: 'Method', indexName: 'method_fts', columns: ['name', 'content', 'description'] }, { table: 'File', indexName: 'file_fts', columns: ['name', 'content'] }, ]; diff --git a/gitnexus/test/integration/local-backend-calltool.test.ts b/gitnexus/test/integration/local-backend-calltool.test.ts index e0b61bb62..f4912f9d8 100644 --- a/gitnexus/test/integration/local-backend-calltool.test.ts +++ b/gitnexus/test/integration/local-backend-calltool.test.ts @@ -106,6 +106,59 @@ withTestLbugDB('local-backend-calltool', (handle) => { }); }); + describe('impact tool relationTypes filtering', () => { + let backend: LocalBackend; + + beforeAll(async () => { + const ext = handle as typeof handle & { _backend?: LocalBackend }; + if (!ext._backend) { + throw new Error('LocalBackend not initialized — afterSetup did not attach _backend to handle'); + } + backend = ext._backend; + }); + + it('filters by HAS_METHOD only', async () => { + const result = await backend.callTool('impact', { + target: 'AuthService', + direction: 'downstream', + relationTypes: ['HAS_METHOD'], + }); + expect(result).not.toHaveProperty('error'); + expect(result.impactedCount).toBeGreaterThanOrEqual(1); + const d1 = result.byDepth[1] || result.byDepth['1'] || []; + const names = d1.map((d: any) => d.name); + expect(names).toContain('authenticate'); + // Should NOT include CALLS-reachable symbols like validate/hash + expect(names).not.toContain('validate'); + expect(names).not.toContain('hash'); + }); + + it('filters by OVERRIDES only', async () => { + const result = await backend.callTool('impact', { + target: 'authenticate', + direction: 'downstream', + relationTypes: ['OVERRIDES'], + }); + expect(result).not.toHaveProperty('error'); + // AuthService.authenticate overrides BaseService.authenticate + expect(result.impactedCount).toBeGreaterThanOrEqual(1); + const d1 = result.byDepth[1] || result.byDepth['1'] || []; + const names = d1.map((d: any) => d.name); + expect(names).toContain('authenticate'); + }); + + it('does not return HAS_METHOD results when filtering by CALLS only', async () => { + const result = await backend.callTool('impact', { + target: 'AuthService', + direction: 'downstream', + relationTypes: ['CALLS'], + }); + expect(result).not.toHaveProperty('error'); + // AuthService has no outgoing CALLS edges, only HAS_METHOD + expect(result.impactedCount).toBe(0); + }); + }); + describe('tool parameter edge cases', () => { let backend: LocalBackend; diff --git a/gitnexus/test/integration/local-backend.test.ts b/gitnexus/test/integration/local-backend.test.ts index 21f18070e..4e0cd39a8 100644 --- a/gitnexus/test/integration/local-backend.test.ts +++ b/gitnexus/test/integration/local-backend.test.ts @@ -76,7 +76,7 @@ withTestLbugDB('local-backend', (handle) => { describe('relation type filtering', () => { it('only allows valid relation types in queries', () => { - const validTypes = ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']; + const validTypes = ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']; const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE']; for (const t of validTypes) { diff --git a/gitnexus/test/unit/security.test.ts b/gitnexus/test/unit/security.test.ts index b8e83c560..f4344c37d 100644 --- a/gitnexus/test/unit/security.test.ts +++ b/gitnexus/test/unit/security.test.ts @@ -96,12 +96,14 @@ describe('isWriteQuery', () => { // ─── Relation type allowlist ────────────────────────────────────────── describe('VALID_RELATION_TYPES', () => { - it('contains exactly the expected 4 types', () => { - expect(VALID_RELATION_TYPES.size).toBe(4); + it('contains exactly the expected 6 types', () => { + expect(VALID_RELATION_TYPES.size).toBe(6); expect(VALID_RELATION_TYPES.has('CALLS')).toBe(true); expect(VALID_RELATION_TYPES.has('IMPORTS')).toBe(true); expect(VALID_RELATION_TYPES.has('EXTENDS')).toBe(true); expect(VALID_RELATION_TYPES.has('IMPLEMENTS')).toBe(true); + expect(VALID_RELATION_TYPES.has('HAS_METHOD')).toBe(true); + expect(VALID_RELATION_TYPES.has('OVERRIDES')).toBe(true); }); it('rejects invalid relation types', () => {