mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
import {
|
|
detectFrameworkFromPath,
|
|
detectFrameworkFromAST,
|
|
} from '../../src/core/ingestion/framework-detection.js';
|
|
import { providers } from '../../src/core/ingestion/languages/index.js';
|
|
|
|
describe('detectFrameworkFromPath', () => {
|
|
describe('Next.js', () => {
|
|
it('detects Pages Router pages', () => {
|
|
const result = detectFrameworkFromPath('pages/users.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('nextjs-pages');
|
|
expect(result!.entryPointMultiplier).toBe(3.0);
|
|
});
|
|
|
|
it('ignores _app and _document pages', () => {
|
|
expect(detectFrameworkFromPath('pages/_app.tsx')).toBeNull();
|
|
});
|
|
|
|
it('detects App Router page.tsx', () => {
|
|
const result = detectFrameworkFromPath('app/dashboard/page.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('nextjs-app');
|
|
});
|
|
|
|
it('detects API routes in pages', () => {
|
|
const result = detectFrameworkFromPath('pages/api/users.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('nextjs-api');
|
|
});
|
|
|
|
it('detects App Router API route.ts', () => {
|
|
const result = detectFrameworkFromPath('app/api/users/route.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('nextjs-api');
|
|
});
|
|
|
|
it('detects layout files', () => {
|
|
const result = detectFrameworkFromPath('app/layout.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.entryPointMultiplier).toBe(2.0);
|
|
});
|
|
});
|
|
|
|
describe('Expo Router', () => {
|
|
it('detects _layout files', () => {
|
|
const result = detectFrameworkFromPath('app/_layout.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
expect(result!.reason).toBe('expo-layout');
|
|
});
|
|
it('detects +api routes', () => {
|
|
const result = detectFrameworkFromPath('app/users+api.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
expect(result!.reason).toBe('expo-api-route');
|
|
});
|
|
it('detects screen files', () => {
|
|
const result = detectFrameworkFromPath('app/(tabs)/settings.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
expect(result!.reason).toBe('expo-screen');
|
|
expect(result!.entryPointMultiplier).toBe(2.5);
|
|
});
|
|
});
|
|
|
|
describe('Express / Node.js', () => {
|
|
it('detects route files', () => {
|
|
const result = detectFrameworkFromPath('routes/auth.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('express');
|
|
expect(result!.entryPointMultiplier).toBe(2.5);
|
|
});
|
|
});
|
|
|
|
describe('MVC controllers', () => {
|
|
it('detects controller folder', () => {
|
|
const result = detectFrameworkFromPath('controllers/UserController.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('mvc');
|
|
});
|
|
|
|
it('detects handlers folder', () => {
|
|
const result = detectFrameworkFromPath('handlers/auth.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('handlers');
|
|
});
|
|
});
|
|
|
|
describe('React', () => {
|
|
it('has React component detection rule for views/components folders', () => {
|
|
const result = detectFrameworkFromPath('views/Button.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('react');
|
|
expect(result!.entryPointMultiplier).toBe(1.5);
|
|
expect(result!.reason).toBe('react-component');
|
|
});
|
|
});
|
|
|
|
describe('Python frameworks', () => {
|
|
it('detects Django views', () => {
|
|
const result = detectFrameworkFromPath('myapp/views.py');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('django');
|
|
expect(result!.entryPointMultiplier).toBe(3.0);
|
|
});
|
|
|
|
it('detects Django URLs', () => {
|
|
const result = detectFrameworkFromPath('myapp/urls.py');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('django');
|
|
});
|
|
|
|
it('detects FastAPI routers', () => {
|
|
const result = detectFrameworkFromPath('routers/users.py');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('fastapi');
|
|
});
|
|
});
|
|
|
|
describe('Java frameworks', () => {
|
|
it('detects Spring controllers folder', () => {
|
|
const result = detectFrameworkFromPath('controller/UserController.java');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('spring');
|
|
});
|
|
|
|
it('detects Spring controller by filename', () => {
|
|
const result = detectFrameworkFromPath('src/UserController.java');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('spring');
|
|
});
|
|
|
|
it('detects Java service layer', () => {
|
|
const result = detectFrameworkFromPath('service/UserService.java');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('java-service');
|
|
});
|
|
});
|
|
|
|
describe('C# / .NET', () => {
|
|
it('detects ASP.NET controllers', () => {
|
|
const result = detectFrameworkFromPath('controllers/UsersController.cs');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('aspnet');
|
|
});
|
|
|
|
it('detects Blazor pages', () => {
|
|
const result = detectFrameworkFromPath('pages/Index.razor');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('blazor');
|
|
});
|
|
});
|
|
|
|
describe('Go frameworks', () => {
|
|
it('detects Go handlers', () => {
|
|
const result = detectFrameworkFromPath('handlers/user.go');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('go-http');
|
|
});
|
|
|
|
it('detects Go main.go', () => {
|
|
const result = detectFrameworkFromPath('cmd/server/main.go');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.entryPointMultiplier).toBe(3.0);
|
|
});
|
|
|
|
it('does NOT treat Go helper files under cmd/ as entry points', () => {
|
|
expect(detectFrameworkFromPath('cmd/server/internal/util.go')).toBeNull();
|
|
expect(detectFrameworkFromPath('cmd/foo/config/setup.go')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Rust frameworks', () => {
|
|
it('detects Rust handlers', () => {
|
|
const result = detectFrameworkFromPath('handlers/auth.rs');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('rust-web');
|
|
});
|
|
|
|
it('detects main.rs', () => {
|
|
const result = detectFrameworkFromPath('src/main.rs');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('rust');
|
|
expect(result!.entryPointMultiplier).toBe(3.0);
|
|
});
|
|
|
|
it('detects bin folder', () => {
|
|
const result = detectFrameworkFromPath('src/bin/cli.rs');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('rust');
|
|
});
|
|
});
|
|
|
|
describe('C / C++', () => {
|
|
it('detects main.c', () => {
|
|
const result = detectFrameworkFromPath('src/main.c');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('c-cpp');
|
|
});
|
|
|
|
it('detects main.cpp', () => {
|
|
const result = detectFrameworkFromPath('src/main.cpp');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('c-cpp');
|
|
});
|
|
});
|
|
|
|
describe('PHP / Laravel', () => {
|
|
it('detects Laravel routes', () => {
|
|
const result = detectFrameworkFromPath('routes/web.php');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('laravel');
|
|
expect(result!.entryPointMultiplier).toBe(3.0);
|
|
});
|
|
|
|
it('detects Laravel controllers', () => {
|
|
const result = detectFrameworkFromPath('http/controllers/UserController.php');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('laravel');
|
|
});
|
|
|
|
it('detects Laravel jobs', () => {
|
|
const result = detectFrameworkFromPath('jobs/SendEmail.php');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.reason).toBe('laravel-job');
|
|
});
|
|
|
|
it('detects Laravel middleware', () => {
|
|
const result = detectFrameworkFromPath('http/middleware/Auth.php');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.reason).toBe('laravel-middleware');
|
|
});
|
|
|
|
it('detects Laravel models', () => {
|
|
const result = detectFrameworkFromPath('models/User.php');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.entryPointMultiplier).toBe(1.5);
|
|
});
|
|
});
|
|
|
|
describe('Swift / iOS', () => {
|
|
it('detects AppDelegate', () => {
|
|
const result = detectFrameworkFromPath('Sources/AppDelegate.swift');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('ios');
|
|
});
|
|
|
|
it('detects ViewControllers folder', () => {
|
|
const result = detectFrameworkFromPath('ViewControllers/LoginVC.swift');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('uikit');
|
|
});
|
|
|
|
it('detects Coordinator pattern', () => {
|
|
const result = detectFrameworkFromPath('Coordinators/AppCoordinator.swift');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('ios-coordinator');
|
|
});
|
|
|
|
it('detects SwiftUI views folder', () => {
|
|
const result = detectFrameworkFromPath('views/ContentView.swift');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('swiftui');
|
|
});
|
|
});
|
|
|
|
describe('generic patterns', () => {
|
|
it('returns null for unknown paths', () => {
|
|
expect(detectFrameworkFromPath('src/internal/crypto.ts')).toBeNull();
|
|
});
|
|
|
|
it('normalizes Windows backslashes', () => {
|
|
const result = detectFrameworkFromPath('routes\\auth.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('express');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('detectFrameworkFromAST', () => {
|
|
it('returns null for empty inputs', () => {
|
|
expect(detectFrameworkFromAST('', '')).toBeNull();
|
|
expect(detectFrameworkFromAST('typescript', '')).toBeNull();
|
|
expect(detectFrameworkFromAST('', 'some code')).toBeNull();
|
|
});
|
|
|
|
it('detects NestJS decorators in TypeScript', () => {
|
|
const result = detectFrameworkFromAST('typescript', '@Controller("/users")');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('nestjs');
|
|
expect(result!.entryPointMultiplier).toBe(3.2);
|
|
});
|
|
|
|
it('detects NestJS decorators in JavaScript', () => {
|
|
const result = detectFrameworkFromAST('javascript', '@Get("/")');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('nestjs');
|
|
});
|
|
|
|
it('detects FastAPI decorators in Python', () => {
|
|
const result = detectFrameworkFromAST('python', '@app.get("/users")');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('fastapi');
|
|
});
|
|
|
|
it('detects Flask decorators in Python', () => {
|
|
const result = detectFrameworkFromAST('python', '@app.route("/users")');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('flask');
|
|
});
|
|
|
|
it('detects Spring annotations in Java', () => {
|
|
const result = detectFrameworkFromAST('java', '@RestController');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('spring');
|
|
});
|
|
|
|
it('detects ASP.NET attributes in C#', () => {
|
|
const result = detectFrameworkFromAST('csharp', '[ApiController]');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('aspnet');
|
|
});
|
|
|
|
it('detects Laravel route definitions in PHP', () => {
|
|
const result = detectFrameworkFromAST(
|
|
'php',
|
|
"Route::get('/users', [UserController::class, 'index'])",
|
|
);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('laravel');
|
|
});
|
|
|
|
it('detects Actix-web route attributes in Rust', () => {
|
|
const result = detectFrameworkFromAST('rust', '#[get("/")]');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('actix-web');
|
|
});
|
|
|
|
it('returns null for language with no matching pattern', () => {
|
|
expect(detectFrameworkFromAST('c', 'int main() { return 0; }')).toBeNull();
|
|
});
|
|
|
|
it('is case-insensitive', () => {
|
|
const result = detectFrameworkFromAST('TypeScript', '@controller("/")');
|
|
expect(result).not.toBeNull();
|
|
});
|
|
|
|
it('detects Expo Router useRouter hook', () => {
|
|
const result = detectFrameworkFromAST('typescript', 'const router = useRouter()');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
});
|
|
it('detects Expo Router router.push', () => {
|
|
const result = detectFrameworkFromAST('javascript', "router.push('/settings')");
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
});
|
|
});
|
|
|
|
describe('provider registry exhaustiveness', () => {
|
|
// Compile-time exhaustiveness is enforced by `satisfies Record<SupportedLanguages, ...>`
|
|
// in languages/index.ts. This runtime guard catches drift if the enum and the
|
|
// providers map ever diverge at runtime (e.g. via build-artifact mismatch),
|
|
// since both shared lookup maps are built from `Object.entries(providers)`.
|
|
it('has one provider per SupportedLanguages enum member', () => {
|
|
expect(Object.keys(providers).sort()).toEqual(Object.values(SupportedLanguages).sort());
|
|
});
|
|
});
|
|
|
|
describe('provider astFrameworkPatterns', () => {
|
|
it('preserves multiplier and reason for high-priority frameworks', () => {
|
|
// Pin the exact multiplier and reason strings to catch silent drift
|
|
// during the per-language pattern relocation. These three frameworks
|
|
// are the most-used decorator-driven entry-point boosters.
|
|
const expectedConfigs: Record<string, { multiplier: number; reason: string }> = {
|
|
nestjs: { multiplier: 3.2, reason: 'nestjs-decorator' },
|
|
spring: { multiplier: 3.2, reason: 'spring-annotation' },
|
|
fastapi: { multiplier: 3.0, reason: 'fastapi-decorator' },
|
|
};
|
|
const seen = new Set<string>();
|
|
for (const provider of Object.values(providers)) {
|
|
for (const cfg of provider.astFrameworkPatterns ?? []) {
|
|
const expected = expectedConfigs[cfg.framework];
|
|
if (!expected) continue;
|
|
expect(cfg.entryPointMultiplier).toBe(expected.multiplier);
|
|
expect(cfg.reason).toBe(expected.reason);
|
|
seen.add(cfg.framework);
|
|
}
|
|
}
|
|
for (const fw of Object.keys(expectedConfigs)) {
|
|
expect(seen.has(fw), `missing framework config: ${fw}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('covers all expected frameworks across providers', () => {
|
|
const expectedFrameworks = new Set([
|
|
'nestjs',
|
|
'expo-router',
|
|
'fastapi',
|
|
'flask',
|
|
'spring',
|
|
'jaxrs',
|
|
'aspnet',
|
|
'signalr',
|
|
'blazor',
|
|
'efcore',
|
|
'go-http',
|
|
'gin',
|
|
'echo',
|
|
'fiber',
|
|
'go-grpc',
|
|
'laravel',
|
|
'actix-web',
|
|
'axum',
|
|
'rocket',
|
|
'tokio',
|
|
'qt',
|
|
'uikit',
|
|
'swiftui',
|
|
'vapor',
|
|
'rails',
|
|
'sinatra',
|
|
'spring-kotlin',
|
|
'ktor',
|
|
'android-kotlin',
|
|
'flutter',
|
|
'riverpod',
|
|
]);
|
|
const foundFrameworks = new Set<string>();
|
|
for (const provider of Object.values(providers)) {
|
|
for (const cfg of provider.astFrameworkPatterns ?? []) {
|
|
foundFrameworks.add(cfg.framework);
|
|
expect(cfg.patterns.length).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
for (const fw of expectedFrameworks) {
|
|
expect(foundFrameworks.has(fw), `missing framework: ${fw}`).toBe(true);
|
|
}
|
|
});
|
|
});
|