GitNexus/gitnexus/test/unit/entry-point-scoring.test.ts
Christian C. Berclaz 7f8b01d506
refactor(ingestion): consolidate per-language patterns into LanguageProvider (#1279)
* refactor(ingestion): consolidate per-language patterns into LanguageProvider

Move entry-point name patterns and AST framework detection patterns from
shared maps in entry-point-scoring.ts and framework-detection.ts into each
LanguageProvider. The shared files now build their lookup tables dynamically
from the provider registry at module load.

This aligns with the architecture principle that shared pipeline code must
not name languages. Adding a new language no longer requires modifying
entry-point-scoring.ts or framework-detection.ts — the provider file is
the single source of truth for all language-specific data.

New LanguageProvider fields:
  - entryPointPatterns: RegExp[] (default: [])
  - astFrameworkPatterns: AstFrameworkPatternConfig[] (default: [])

* test(ingestion): add provider-registry, multiplier/reason, and Kotlin/Dart/Ruby entry-point coverage

Addresses review feedback on the per-language pattern consolidation:

- Runtime guard that providers map covers every SupportedLanguages member,
  catching enum/registry drift that the compile-time `satisfies` cannot.
- Multiplier/reason parity assertions for nestjs (3.2/nestjs-decorator),
  spring (3.2/spring-annotation), and fastapi (3.0/fastapi-decorator) so a
  silent value change during future relocations would fail loudly.
- Entry-point pattern coverage for Kotlin (Android lifecycle, ViewModel,
  Service), Dart (Flutter widget lifecycle), and Ruby (call/perform/execute)
  — the three providers whose patterns moved without representative tests.

* refactor(ingestion): apply satisfies AstFrameworkPatternConfig[] to remaining providers

The c-cpp, dart, php, ruby, and swift providers imported AstFrameworkPatternConfig
but never used it, which the root ESLint config flagged as a hard error in the
quality / lint CI gate.

Use the type the same way csharp/go/java/kotlin/python/rust/typescript already do —
as a satisfies assertion on the astFrameworkPatterns array. This both clears the
unused-import error and gives every provider compile-time validation of pattern
shape, narrowing the gap that the original review flagged about lost exhaustiveness
on the optional astFrameworkPatterns field.

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-05-03 10:34:00 +01:00

348 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
calculateEntryPointScore,
isTestFile,
isUtilityFile,
} from '../../src/core/ingestion/entry-point-scoring.js';
describe('calculateEntryPointScore', () => {
describe('base scoring', () => {
it('returns 0 for functions with no outgoing calls', () => {
const result = calculateEntryPointScore('handler', 'typescript', true, 0, 0);
expect(result.score).toBe(0);
expect(result.reasons).toContain('no-outgoing-calls');
});
it('calculates base score as calleeCount / (callerCount + 1)', () => {
const result = calculateEntryPointScore('doStuff', 'typescript', false, 0, 5);
// base = 5 / (0 + 1) = 5, no export bonus, no name bonus
expect(result.score).toBe(5);
});
it('reduces score for functions with many callers', () => {
const few = calculateEntryPointScore('doStuff', 'typescript', false, 1, 5);
const many = calculateEntryPointScore('doStuff', 'typescript', false, 10, 5);
expect(few.score).toBeGreaterThan(many.score);
});
});
describe('export multiplier', () => {
it('applies 2.0 multiplier for exported functions', () => {
const exported = calculateEntryPointScore('doStuff', 'typescript', true, 0, 4);
const notExported = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
expect(exported.score).toBe(notExported.score * 2);
expect(exported.reasons).toContain('exported');
});
it('does not add exported reason when not exported', () => {
const result = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
expect(result.reasons).not.toContain('exported');
});
});
describe('universal name patterns', () => {
it.each(['main', 'init', 'bootstrap', 'start', 'run', 'setup', 'configure'])(
'recognizes "%s" as entry point pattern',
(name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
expect(result.reasons).toContain('entry-pattern');
},
);
it.each([
'handleLogin',
'handleSubmit',
'onClick',
'onSubmit',
'RequestHandler',
'UserController',
'processPayment',
'executeQuery',
'performAction',
'dispatchEvent',
'triggerAction',
'fireEvent',
'emitEvent',
])('recognizes "%s" as entry point pattern', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
expect(result.reasons).toContain('entry-pattern');
});
it('applies 1.5x name multiplier for entry patterns', () => {
const matching = calculateEntryPointScore('handleLogin', 'typescript', false, 0, 4);
const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
// matching gets 1.5x, plain gets 1.0x
expect(matching.score).toBe(plain.score * 1.5);
});
});
describe('language-specific patterns', () => {
it('recognizes React hooks for TypeScript', () => {
const result = calculateEntryPointScore('useEffect', 'typescript', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes React hooks for JavaScript', () => {
const result = calculateEntryPointScore('useState', 'javascript', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Python REST patterns', () => {
const result = calculateEntryPointScore('get_users', 'python', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Java servlet patterns', () => {
const result = calculateEntryPointScore('doGet', 'java', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Go handler patterns', () => {
const result = calculateEntryPointScore('NewServer', 'go', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Rust entry patterns', () => {
const result = calculateEntryPointScore('handle_request', 'rust', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Swift UIKit lifecycle', () => {
const result = calculateEntryPointScore('viewDidLoad', 'swift', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Swift SwiftUI body', () => {
const result = calculateEntryPointScore('body', 'swift', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes PHP Laravel patterns', () => {
// __invoke starts with '_' which matches utility pattern first
const result = calculateEntryPointScore('handle', 'php', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes PHP RESTful resource methods', () => {
const result = calculateEntryPointScore('index', 'php', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes C# ASP.NET patterns', () => {
const result = calculateEntryPointScore('GetUsers', 'csharp', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes C main entry point', () => {
const result = calculateEntryPointScore('main', 'c', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
// C-specific patterns
it.each([
'init_server',
'server_init',
'start_server',
'handle_request',
'signal_handler',
'event_callback',
'cmd_new_window',
'server_start',
'client_connect',
'session_create',
'window_resize',
])('recognizes C pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'c', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
// C++-specific patterns
it.each([
'CreateInstance',
'create_session',
'Run',
'run',
'Start',
'start',
'OnEventReceived',
'on_click',
])('recognizes C++ pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'cpp', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
// Kotlin-specific patterns (Android lifecycle, ViewModel, Service)
it.each([
'onCreate',
'onStart',
'onResume',
'onPause',
'onStop',
'onDestroy',
'doWork',
'createComponent',
'buildGraph',
'UserViewModel',
'module',
'AuthService',
])('recognizes Kotlin pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'kotlin', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
// Dart-specific patterns (Flutter widget lifecycle, BLoC)
// Note: didChangeDependencies/didUpdateWidget/mapEventToState are listed in
// the Dart provider but pre-empted by UTILITY_PATTERNS (`did[A-Z]`, `^map`)
// before the entry-pattern check, so they are not asserted here.
it.each(['main', 'build', 'createState', 'initState', 'dispose', 'runApp', 'onEvent'])(
'recognizes Dart pattern "%s"',
(name) => {
const result = calculateEntryPointScore(name, 'dart', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
},
);
// Ruby-specific patterns (Rails callable/job/service objects)
it.each(['call', 'perform', 'execute'])('recognizes Ruby pattern "%s"', (name) => {
const result = calculateEntryPointScore(name, 'ruby', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
});
describe('utility pattern penalty', () => {
it.each([
'getUser',
'setName',
'isValid',
'hasPermission',
'canEdit',
'formatDate',
'parseJSON',
'validateInput',
'toString',
'fromJSON',
'encodeBase64',
'serializeData',
'cloneDeep',
'mergeObjects',
])('penalizes utility function "%s"', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
expect(result.reasons).toContain('utility-pattern');
// 0.3 multiplier
const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 3);
expect(result.score).toBeLessThan(plain.score);
});
it('penalizes private-by-convention functions', () => {
const result = calculateEntryPointScore('_internal', 'typescript', false, 0, 3);
expect(result.reasons).toContain('utility-pattern');
});
});
describe('framework detection from path', () => {
it('boosts Next.js page entry points', () => {
const result = calculateEntryPointScore(
'render',
'typescript',
true,
0,
3,
'pages/users.tsx',
);
expect(result.reasons.some((r) => r.includes('framework:'))).toBe(true);
expect(result.score).toBeGreaterThan(0);
});
it('does not apply framework bonus for non-framework paths', () => {
const result = calculateEntryPointScore(
'render',
'typescript',
true,
0,
3,
'src/lib/utils.ts',
);
expect(result.reasons.every((r) => !r.includes('framework:'))).toBe(true);
});
});
describe('combined scoring', () => {
it('multiplies all factors together', () => {
// handleLogin: entry pattern (1.5x) + exported (2.0x) + base
const result = calculateEntryPointScore(
'handleLogin',
'typescript',
true,
0,
4,
'routes/auth.ts',
);
expect(result.score).toBeGreaterThan(0);
expect(result.reasons).toContain('exported');
expect(result.reasons).toContain('entry-pattern');
});
});
});
describe('isTestFile', () => {
it.each([
'src/utils.test.ts',
'src/utils.spec.ts',
'__tests__/utils.ts',
'__mocks__/api.ts',
'src/test/integration/db.ts',
'src/tests/unit/helper.ts',
'src/testing/setup.ts',
'lib/test_utils.py',
'pkg/handler_test.go',
'src/test/java/com/example/Test.java',
'MyViewTests.swift',
'MyViewTest.swift',
'UITests/LoginTest.swift',
'App.Tests/MyTest.cs',
'tests/Feature/UserTest.php',
'tests/Unit/AuthSpec.php',
])('returns true for test file "%s"', (filePath) => {
expect(isTestFile(filePath)).toBe(true);
});
it.each([
'src/utils.ts',
'src/controllers/auth.ts',
'src/main.py',
'cmd/server.go',
'src/main/java/App.java',
])('returns false for non-test file "%s"', (filePath) => {
expect(isTestFile(filePath)).toBe(false);
});
it('normalizes Windows backslashes', () => {
expect(isTestFile('src\\__tests__\\utils.ts')).toBe(true);
});
});
describe('isUtilityFile', () => {
it.each([
'src/utils/format.ts',
'src/util/helpers.ts',
'src/helpers/date.ts',
'src/helper/string.ts',
'src/common/types.ts',
'src/shared/constants.ts',
'src/lib/crypto.ts',
'src/utils.ts',
'src/utils.js',
'src/helpers.ts',
'lib/date_utils.py',
'lib/date_helpers.py',
])('returns true for utility file "%s"', (filePath) => {
expect(isUtilityFile(filePath)).toBe(true);
});
it.each(['src/controllers/auth.ts', 'src/routes/api.ts', 'src/main.ts', 'src/app.ts'])(
'returns false for non-utility file "%s"',
(filePath) => {
expect(isUtilityFile(filePath)).toBe(false);
},
);
});