GitNexus/gitnexus/test/integration/class-impact-all-languages.test.ts
Copilot 962f22482b
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
feat(cli): Fingerprint indexed repos by remote URL to detect sibling-clone graph drift (#982)
* Initial plan

* feat: detect sibling-clone graph drift via remote URL fingerprint

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: address review feedback — fake commit, same-commit case, regex docs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(mcp): address review feedback — CI green, perf, dead branch, one-shot test

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc2259f7-94e4-4243-aaa9-e03b7c632d32

* Merge branch 'main' into copilot/fix-single-path-indexing-issue

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5840b3dd-e879-4854-a067-d1622bec2634

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* Merge branch 'main' into copilot/fix-single-path-indexing-issue

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9025262f-4dd4-4774-8f32-e14434100004

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style: prettier format run-analyze.ts after merge with main

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a7be18dd-102f-4a7b-ac56-53fbd414fe3b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: realpath both sides of cwdGitRoot assertion for Windows 8.3 short-name compat

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b2a1c6a3-e454-4b87-b0e4-69d7c0d9a51b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(test): use path-agnostic assertion for cwdGitRoot on Windows (#1015)

git rev-parse --show-toplevel returns long path names on Windows
while os.tmpdir() returns 8.3 short names. fs.realpathSync does not
expand short names, so exact path comparison always fails on Windows
CI runners. Replace with behavioral assertions instead.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: evolution <wjc163@sina.cn>
2026-04-21 21:58:54 +01:00

459 lines
13 KiB
TypeScript

/**
* Integration Tests: Class impact/context traversal across all supported languages (#480)
*
* Ensures the fix for Java class traversal (CALLS->Constructor, IMPORTS->File)
* does not regress for any supported language, and that each language's class
* topology is handled correctly by impact() and context().
*
* Language topologies:
* JVM (Java, Kotlin): CALLS -> Constructor, IMPORTS -> File
* Non-JVM with classes: CALLS -> Class directly, IMPORTS -> File
* (TypeScript, JavaScript, Python, C#, Ruby, PHP, Rust, Go, Swift, C, C++)
*
* All languages share a single DB instance (withTestLbugDB clears+reseeds per
* file). Node IDs are namespaced by language to avoid collisions.
*/
import { describe, it, expect, beforeAll, 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([]),
}));
// ─── Seed builders ───────────────────────────────────────────────────────────
/** JVM topology: Class -HAS_METHOD-> Constructor, File -DEFINES-> Class.
* Callers CALL the Constructor; importers IMPORT the owning File. */
function jvmNodes(
lang: string,
ext: string,
cls: string,
caller: string,
importer: string,
clsPath: string,
callerPath: string,
importerPath: string,
): string[] {
return [
`CREATE (f:File {id:'${lang}:file:${cls}', name:'${cls}.${ext}', filePath:'${clsPath}', content:''})`,
`CREATE (c:Class {id:'${lang}:class:${cls}', name:'${cls}', filePath:'${clsPath}', startLine:1, endLine:50, isExported:true, content:'', description:''})`,
`CREATE (ctor:Constructor {id:'${lang}:ctor:${cls}', name:'${cls}', filePath:'${clsPath}', startLine:5, endLine:10, content:'', description:''})`,
`CREATE (caller:Method {id:'${lang}:method:${caller}', name:'${caller}', filePath:'${callerPath}', startLine:1, endLine:10, isExported:false, content:'', description:''})`,
`CREATE (imp:File {id:'${lang}:file:${importer}', name:'${importer}.${ext}', filePath:'${importerPath}', content:''})`,
`MATCH (c:Class {id:'${lang}:class:${cls}'}), (ctor:Constructor {id:'${lang}:ctor:${cls}'}) CREATE (c)-[:CodeRelation {type:'HAS_METHOD', confidence:1.0, reason:'class-method', step:0}]->(ctor)`,
`MATCH (f:File {id:'${lang}:file:${cls}'}), (c:Class {id:'${lang}:class:${cls}'}) CREATE (f)-[:CodeRelation {type:'DEFINES', confidence:1.0, reason:'', step:0}]->(c)`,
`MATCH (a:Method {id:'${lang}:method:${caller}'}), (b:Constructor {id:'${lang}:ctor:${cls}'}) CREATE (a)-[:CodeRelation {type:'CALLS', confidence:0.9, reason:'direct', step:0}]->(b)`,
`MATCH (a:File {id:'${lang}:file:${importer}'}), (b:File {id:'${lang}:file:${cls}'}) CREATE (a)-[:CodeRelation {type:'IMPORTS', confidence:0.9, reason:'import', step:0}]->(b)`,
];
}
/** Non-JVM topology: CALLS -> Class directly, IMPORTS -> File. */
function nonJvmNodes(
lang: string,
ext: string,
cls: string,
caller: string,
callerLabel: string,
importer: string,
clsPath: string,
callerPath: string,
importerPath: string,
): string[] {
return [
`CREATE (f:File {id:'${lang}:file:${cls}', name:'${cls}.${ext}', filePath:'${clsPath}', content:''})`,
`CREATE (c:Class {id:'${lang}:class:${cls}', name:'${cls}', filePath:'${clsPath}', startLine:1, endLine:50, isExported:true, content:'', description:''})`,
`CREATE (caller:${callerLabel} {id:'${lang}:fn:${caller}', name:'${caller}', filePath:'${callerPath}', startLine:1, endLine:10, isExported:false, content:'', description:''})`,
`CREATE (imp:File {id:'${lang}:file:${importer}', name:'${importer}.${ext}', filePath:'${importerPath}', content:''})`,
`MATCH (f:File {id:'${lang}:file:${cls}'}), (c:Class {id:'${lang}:class:${cls}'}) CREATE (f)-[:CodeRelation {type:'DEFINES', confidence:1.0, reason:'', step:0}]->(c)`,
`MATCH (a:${callerLabel} {id:'${lang}:fn:${caller}'}), (b:Class {id:'${lang}:class:${cls}'}) CREATE (a)-[:CodeRelation {type:'CALLS', confidence:0.9, reason:'direct', step:0}]->(b)`,
`MATCH (a:File {id:'${lang}:file:${importer}'}), (b:File {id:'${lang}:file:${cls}'}) CREATE (a)-[:CodeRelation {type:'IMPORTS', confidence:0.9, reason:'import', step:0}]->(b)`,
];
}
// ─── Combined seed for all languages ─────────────────────────────────────────
const SEED = [
// Java — JVM topology
...jvmNodes(
'java',
'java',
'PaymentService',
'processPayment',
'OrderController',
'src/main/java/payments/PaymentService.java',
'src/main/java/orders/OrderController.java',
'src/main/java/orders/OrderController.java',
),
// Kotlin — JVM topology
...jvmNodes(
'kotlin',
'kt',
'UserRepository',
'fetchUser',
'UserService',
'src/main/kotlin/data/UserRepository.kt',
'src/main/kotlin/service/UserService.kt',
'src/main/kotlin/service/UserService.kt',
),
// TypeScript — non-JVM
...nonJvmNodes(
'ts',
'ts',
'AuthService',
'loginUser',
'Function',
'app',
'src/auth/AuthService.ts',
'src/routes/auth.ts',
'src/app.ts',
),
// JavaScript — non-JVM
...nonJvmNodes(
'js',
'js',
'EventEmitter',
'subscribe',
'Function',
'index',
'src/events/EventEmitter.js',
'src/handlers/handler.js',
'src/index.js',
),
// Python — non-JVM
...nonJvmNodes(
'py',
'py',
'DatabaseClient',
'connect',
'Function',
'app',
'src/db/database_client.py',
'src/services/service.py',
'src/app.py',
),
// C# — non-JVM
...nonJvmNodes(
'cs',
'cs',
'OrderProcessor',
'ProcessOrder',
'Method',
'Startup',
'src/Orders/OrderProcessor.cs',
'src/Controllers/OrderController.cs',
'src/Startup.cs',
),
// Ruby — non-JVM
...nonJvmNodes(
'rb',
'rb',
'SessionManager',
'create_session',
'Function',
'application',
'lib/session/session_manager.rb',
'lib/controllers/auth_controller.rb',
'lib/application.rb',
),
// PHP — non-JVM
...nonJvmNodes(
'php',
'php',
'CacheService',
'getCache',
'Method',
'bootstrap',
'src/Cache/CacheService.php',
'src/Controllers/HomeController.php',
'src/bootstrap.php',
),
// Rust — non-JVM
...nonJvmNodes(
'rs',
'rs',
'HttpClient',
'send_request',
'Function',
'main',
'src/http/client.rs',
'src/api/handler.rs',
'src/main.rs',
),
// Go — non-JVM
...nonJvmNodes(
'go',
'go',
'Router',
'handleRequest',
'Function',
'main',
'pkg/router/router.go',
'pkg/handlers/handler.go',
'main.go',
),
// Swift — non-JVM
...nonJvmNodes(
'swift',
'swift',
'NetworkManager',
'fetchData',
'Method',
'AppDelegate',
'Sources/Network/NetworkManager.swift',
'Sources/ViewControllers/HomeVC.swift',
'Sources/AppDelegate.swift',
),
// C — non-JVM
...nonJvmNodes(
'c',
'c',
'MemoryPool',
'allocate',
'Function',
'main',
'src/memory/pool.c',
'src/runtime/runtime.c',
'src/main.c',
),
// C++ — non-JVM
...nonJvmNodes(
'cpp',
'cpp',
'ThreadPool',
'enqueue',
'Function',
'main',
'src/threading/thread_pool.cpp',
'src/workers/worker.cpp',
'src/main.cpp',
),
];
// ─── Shared assertion helper ──────────────────────────────────────────────────
function suiteFor(
lang: string,
topology: 'JVM (Constructor+File)' | 'direct CALLS',
getBackend: () => LocalBackend,
className: string,
callerName: string,
importerFileName: string,
classFilePath: string,
) {
describe(`${lang}: Class impact/context via ${topology}`, () => {
it(`impact(upstream) surfaces the caller`, async () => {
const result = await getBackend().callTool('impact', {
target: className,
direction: 'upstream',
includeTests: true,
});
expect(result).not.toHaveProperty('error');
expect(result.impactedCount).toBeGreaterThanOrEqual(1);
const allNames = Object.values(result.byDepth as Record<string, any[]>)
.flat()
.map((d: any) => d.name);
expect(allNames).toContain(callerName);
});
it(`impact(upstream) surfaces the file importer`, async () => {
const result = await getBackend().callTool('impact', {
target: className,
direction: 'upstream',
includeTests: true,
});
expect(result).not.toHaveProperty('error');
const allNames = Object.values(result.byDepth as Record<string, any[]>)
.flat()
.map((d: any) => d.name);
expect(allNames).toContain(importerFileName);
});
it(`context() returns found with kind Class`, async () => {
const result = await getBackend().callTool('context', {
name: className,
file_path: classFilePath,
});
expect(result.status).toBe('found');
expect(result.symbol.kind).toBe('Class');
});
it(`context() has non-empty incoming containing the caller`, async () => {
const result = await getBackend().callTool('context', {
name: className,
file_path: classFilePath,
});
expect(result.status).toBe('found');
const allIncoming = [...(result.incoming.calls || []), ...(result.incoming.imports || [])];
expect(allIncoming.length).toBeGreaterThanOrEqual(1);
expect(allIncoming.map((r: any) => r.name)).toContain(callerName);
});
});
}
// ─── Single DB instance, all languages ───────────────────────────────────────
withTestLbugDB(
'class-impact-all-languages',
(handle) => {
let backend: LocalBackend;
beforeAll(() => {
backend = (handle as any)._backend;
});
// JVM languages
suiteFor(
'Java',
'JVM (Constructor+File)',
() => backend,
'PaymentService',
'processPayment',
'OrderController.java',
'src/main/java/payments/PaymentService.java',
);
suiteFor(
'Kotlin',
'JVM (Constructor+File)',
() => backend,
'UserRepository',
'fetchUser',
'UserService.kt',
'src/main/kotlin/data/UserRepository.kt',
);
// Non-JVM languages
suiteFor(
'TypeScript',
'direct CALLS',
() => backend,
'AuthService',
'loginUser',
'app.ts',
'src/auth/AuthService.ts',
);
suiteFor(
'JavaScript',
'direct CALLS',
() => backend,
'EventEmitter',
'subscribe',
'index.js',
'src/events/EventEmitter.js',
);
suiteFor(
'Python',
'direct CALLS',
() => backend,
'DatabaseClient',
'connect',
'app.py',
'src/db/database_client.py',
);
suiteFor(
'C#',
'direct CALLS',
() => backend,
'OrderProcessor',
'ProcessOrder',
'Startup.cs',
'src/Orders/OrderProcessor.cs',
);
suiteFor(
'Ruby',
'direct CALLS',
() => backend,
'SessionManager',
'create_session',
'application.rb',
'lib/session/session_manager.rb',
);
suiteFor(
'PHP',
'direct CALLS',
() => backend,
'CacheService',
'getCache',
'bootstrap.php',
'src/Cache/CacheService.php',
);
suiteFor(
'Rust',
'direct CALLS',
() => backend,
'HttpClient',
'send_request',
'main.rs',
'src/http/client.rs',
);
suiteFor(
'Go',
'direct CALLS',
() => backend,
'Router',
'handleRequest',
'main.go',
'pkg/router/router.go',
);
suiteFor(
'Swift',
'direct CALLS',
() => backend,
'NetworkManager',
'fetchData',
'AppDelegate.swift',
'Sources/Network/NetworkManager.swift',
);
suiteFor(
'C',
'direct CALLS',
() => backend,
'MemoryPool',
'allocate',
'main.c',
'src/memory/pool.c',
);
suiteFor(
'C++',
'direct CALLS',
() => backend,
'ThreadPool',
'enqueue',
'main.cpp',
'src/threading/thread_pool.cpp',
);
},
{
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: 20, nodes: 60, communities: 0, processes: 0 },
},
]);
const backend = new LocalBackend();
await backend.init();
(handle as any)._backend = backend;
},
},
);