test(ingestion): add unit tests for heritage extraction configs and factory

- 36 tests covering factory construction, capture-based extraction,
  Go named field filtering, Ruby call-based heritage, and all 14 config validations

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b55df80d-758a-48cd-96de-a7657bdc2acb

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-16 19:03:59 +00:00 • committed by GitHub
parent 835e69c53e
commit 9530655650
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,455 @@
import { describe, it, expect } from 'vitest';
import { createHeritageExtractor } from '../../src/core/ingestion/heritage-extractors/generic.js';
import {
javaHeritageConfig,
kotlinHeritageConfig,
} from '../../src/core/ingestion/heritage-extractors/configs/jvm.js';
import { csharpHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/csharp.js';
import {
typescriptHeritageConfig,
javascriptHeritageConfig,
} from '../../src/core/ingestion/heritage-extractors/configs/typescript-javascript.js';
import {
cHeritageConfig,
cppHeritageConfig,
} from '../../src/core/ingestion/heritage-extractors/configs/c-cpp.js';
import { pythonHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/python.js';
import { rubyHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/ruby.js';
import { rustHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/rust.js';
import { dartHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/dart.js';
import { goHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/go.js';
import { phpHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/php.js';
import { swiftHeritageConfig } from '../../src/core/ingestion/heritage-extractors/configs/swift.js';
import type {
HeritageExtractionConfig,
HeritageExtractorContext,
} from '../../src/core/ingestion/heritage-types.js';
import type { CaptureMap } from '../../src/core/ingestion/language-provider.js';
import { SupportedLanguages } from 'gitnexus-shared';
// ---------------------------------------------------------------------------
// Mock AST node helpers
// ---------------------------------------------------------------------------
interface MockNode {
type: string;
text: string;
parent?: MockNode | null;
children?: MockNode[];
childForFieldName?: (name: string) => MockNode | undefined;
}
/** Create a minimal mock SyntaxNode for capture map entries. */
function makeNode(text: string, type = 'identifier', parent?: MockNode | null): MockNode {
return { type, text, parent: parent ?? null };
}
/** Create a mock field_declaration node (Go struct fields). */
function makeGoFieldDecl(opts: { hasName: boolean; typeName: string }): MockNode {
const nameNode = opts.hasName ? makeNode('MyField', 'field_identifier') : undefined;
const fieldDecl: MockNode = {
type: 'field_declaration',
text: '',
childForFieldName: (name: string) => (name === 'name' ? nameNode : undefined),
};
const typeNode = makeNode(opts.typeName, 'type_identifier', fieldDecl);
return typeNode;
}
/** Build a CaptureMap from partial entries. */
function buildCaptureMap(entries: Record<string, MockNode | undefined>): CaptureMap {
return entries as unknown as CaptureMap;
}
/** Default context for testing. */
function ctx(
filePath = 'Test.java',
language = SupportedLanguages.Java,
): HeritageExtractorContext {
return { filePath, language };
}
// ---------------------------------------------------------------------------
// Factory construction
// ---------------------------------------------------------------------------
describe('createHeritageExtractor', () => {
it('creates an extractor from a minimal config', () => {
const extractor = createHeritageExtractor(javaHeritageConfig);
expect(extractor).toBeDefined();
expect(extractor.language).toBe(SupportedLanguages.Java);
expect(typeof extractor.extract).toBe('function');
});
it('accepts all currently registered language configs', () => {
const configs: HeritageExtractionConfig[] = [
javaHeritageConfig,
kotlinHeritageConfig,
csharpHeritageConfig,
typescriptHeritageConfig,
javascriptHeritageConfig,
cppHeritageConfig,
cHeritageConfig,
pythonHeritageConfig,
rubyHeritageConfig,
rustHeritageConfig,
dartHeritageConfig,
goHeritageConfig,
phpHeritageConfig,
swiftHeritageConfig,
];
for (const cfg of configs) {
expect(
() => createHeritageExtractor(cfg),
`config for ${cfg.language} must construct cleanly`,
).not.toThrow();
}
});
it('sets extractFromCall when callBasedHeritage is configured', () => {
const extractor = createHeritageExtractor(rubyHeritageConfig);
expect(extractor.extractFromCall).toBeDefined();
expect(typeof extractor.extractFromCall).toBe('function');
});
it('does not set extractFromCall when callBasedHeritage is absent', () => {
const extractor = createHeritageExtractor(javaHeritageConfig);
expect(extractor.extractFromCall).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Generic extraction from captures
// ---------------------------------------------------------------------------
describe('HeritageExtractor.extract', () => {
const extractor = createHeritageExtractor(javaHeritageConfig);
it('returns empty array when heritage.class is not present', () => {
const captures = buildCaptureMap({});
expect(extractor.extract(captures, ctx())).toEqual([]);
});
it('returns empty array when heritage.class is present but no extends/implements/trait', () => {
const captures = buildCaptureMap({
'heritage.class': makeNode('MyClass'),
});
expect(extractor.extract(captures, ctx())).toEqual([]);
});
it('extracts extends heritage', () => {
const captures = buildCaptureMap({
'heritage.class': makeNode('Child'),
'heritage.extends': makeNode('Parent'),
});
const result = extractor.extract(captures, ctx());
expect(result).toEqual([
{ className: 'Child', parentName: 'Parent', kind: 'extends' },
]);
});
it('extracts implements heritage', () => {
const captures = buildCaptureMap({
'heritage.class': makeNode('MyClass'),
'heritage.implements': makeNode('MyInterface'),
});
const result = extractor.extract(captures, ctx());
expect(result).toEqual([
{ className: 'MyClass', parentName: 'MyInterface', kind: 'implements' },
]);
});
it('extracts trait-impl heritage', () => {
const rustExtractor = createHeritageExtractor(rustHeritageConfig);
const captures = buildCaptureMap({
'heritage.class': makeNode('MyStruct'),
'heritage.trait': makeNode('Display'),
});
const result = rustExtractor.extract(captures, ctx('main.rs', SupportedLanguages.Rust));
expect(result).toEqual([
{ className: 'MyStruct', parentName: 'Display', kind: 'trait-impl' },
]);
});
it('extracts both extends and implements from same match', () => {
const captures = buildCaptureMap({
'heritage.class': makeNode('MyClass'),
'heritage.extends': makeNode('BaseClass'),
'heritage.implements': makeNode('ISerializable'),
});
const result = extractor.extract(captures, ctx());
expect(result).toEqual([
{ className: 'MyClass', parentName: 'BaseClass', kind: 'extends' },
{ className: 'MyClass', parentName: 'ISerializable', kind: 'implements' },
]);
});
it('extracts all three heritage kinds from same match', () => {
const captures = buildCaptureMap({
'heritage.class': makeNode('MyStruct'),
'heritage.extends': makeNode('Base'),
'heritage.implements': makeNode('IFace'),
'heritage.trait': makeNode('Trait'),
});
const result = extractor.extract(captures, ctx());
expect(result).toEqual([
{ className: 'MyStruct', parentName: 'Base', kind: 'extends' },
{ className: 'MyStruct', parentName: 'IFace', kind: 'implements' },
{ className: 'MyStruct', parentName: 'Trait', kind: 'trait-impl' },
]);
});
});
// ---------------------------------------------------------------------------
// Go: shouldSkipExtends (named field detection)
// ---------------------------------------------------------------------------
describe('Go HeritageExtractor — shouldSkipExtends', () => {
const extractor = createHeritageExtractor(goHeritageConfig);
it('extracts anonymous struct embedding (no field name)', () => {
const typeNode = makeGoFieldDecl({ hasName: false, typeName: 'Animal' });
const captures = buildCaptureMap({
'heritage.class': makeNode('Dog'),
'heritage.extends': typeNode as unknown as CaptureMap[string],
});
const result = extractor.extract(captures, ctx('main.go', SupportedLanguages.Go));
expect(result).toEqual([
{ className: 'Dog', parentName: 'Animal', kind: 'extends' },
]);
});
it('skips named struct fields (Breed string)', () => {
const typeNode = makeGoFieldDecl({ hasName: true, typeName: 'string' });
const captures = buildCaptureMap({
'heritage.class': makeNode('Dog'),
'heritage.extends': typeNode as unknown as CaptureMap[string],
});
const result = extractor.extract(captures, ctx('main.go', SupportedLanguages.Go));
expect(result).toEqual([]);
});
it('extracts heritage when extends node has no parent', () => {
const orphanNode = makeNode('Embedded', 'type_identifier');
orphanNode.parent = null;
const captures = buildCaptureMap({
'heritage.class': makeNode('Foo'),
'heritage.extends': orphanNode as unknown as CaptureMap[string],
});
const result = extractor.extract(captures, ctx('main.go', SupportedLanguages.Go));
expect(result).toEqual([
{ className: 'Foo', parentName: 'Embedded', kind: 'extends' },
]);
});
});
// ---------------------------------------------------------------------------
// Ruby: call-based heritage (include/extend/prepend)
// ---------------------------------------------------------------------------
describe('Ruby HeritageExtractor — call-based heritage', () => {
const extractor = createHeritageExtractor(rubyHeritageConfig);
/** Build a mock call node for include/extend/prepend. */
function makeCallNode(
argNodes: MockNode[],
enclosingType: 'class' | 'module' | null,
enclosingName: string | null,
): MockNode {
const argList: MockNode = {
type: 'argument_list',
text: '',
children: argNodes,
};
const classNameNode = enclosingName ? makeNode(enclosingName, 'constant') : undefined;
const enclosingNode: MockNode | null =
enclosingType && enclosingName
? {
type: enclosingType,
text: '',
parent: null,
childForFieldName: (name: string) =>
name === 'name' ? classNameNode : undefined,
}
: null;
const bodyNode: MockNode = {
type: 'body_statement',
text: '',
parent: enclosingNode,
};
if (enclosingNode) enclosingNode.children = [bodyNode];
const callNode: MockNode = {
type: 'call',
text: '',
parent: bodyNode,
childForFieldName: (name: string) =>
name === 'arguments' ? argList : undefined,
};
return callNode;
}
function makeConstantArg(name: string): MockNode {
return makeNode(name, 'constant');
}
function makeScopeResolutionArg(name: string): MockNode {
return makeNode(name, 'scope_resolution');
}
const rubyCtx = ctx('app.rb', SupportedLanguages.Ruby);
it('returns null for non-heritage call names', () => {
const callNode = makeCallNode([makeConstantArg('Foo')], 'class', 'Bar');
expect(extractor.extractFromCall!('puts', callNode as any, rubyCtx)).toBeNull();
});
it('extracts include heritage with single constant arg', () => {
const callNode = makeCallNode([makeConstantArg('Serializable')], 'class', 'User');
const result = extractor.extractFromCall!('include', callNode as any, rubyCtx);
expect(result).toEqual([
{ className: 'User', parentName: 'Serializable', kind: 'include' },
]);
});
it('extracts extend heritage with scope_resolution arg', () => {
const callNode = makeCallNode(
[makeScopeResolutionArg('ActiveSupport::Concern')],
'class',
'Post',
);
const result = extractor.extractFromCall!('extend', callNode as any, rubyCtx);
expect(result).toEqual([
{ className: 'Post', parentName: 'ActiveSupport::Concern', kind: 'extend' },
]);
});
it('extracts prepend heritage', () => {
const callNode = makeCallNode([makeConstantArg('Instrumented')], 'class', 'Service');
const result = extractor.extractFromCall!('prepend', callNode as any, rubyCtx);
expect(result).toEqual([
{ className: 'Service', parentName: 'Instrumented', kind: 'prepend' },
]);
});
it('extracts include inside a module', () => {
const callNode = makeCallNode([makeConstantArg('Helpers')], 'module', 'AppHelper');
const result = extractor.extractFromCall!('include', callNode as any, rubyCtx);
expect(result).toEqual([
{ className: 'AppHelper', parentName: 'Helpers', kind: 'include' },
]);
});
it('extracts multiple constant args as separate heritage items', () => {
const args = [makeConstantArg('Mod1'), makeConstantArg('Mod2'), makeConstantArg('Mod3')];
const callNode = makeCallNode(args, 'class', 'MyClass');
const result = extractor.extractFromCall!('include', callNode as any, rubyCtx);
expect(result).toEqual([
{ className: 'MyClass', parentName: 'Mod1', kind: 'include' },
{ className: 'MyClass', parentName: 'Mod2', kind: 'include' },
{ className: 'MyClass', parentName: 'Mod3', kind: 'include' },
]);
});
it('returns empty array when no enclosing class/module', () => {
const argList: MockNode = {
type: 'argument_list',
text: '',
children: [makeConstantArg('Foo')],
};
const callNode: MockNode = {
type: 'call',
text: '',
parent: null,
childForFieldName: (name: string) =>
name === 'arguments' ? argList : undefined,
};
const result = extractor.extractFromCall!('include', callNode as any, rubyCtx);
expect(result).toEqual([]);
});
it('skips non-constant/non-scope_resolution args', () => {
const args = [
makeConstantArg('Mod1'),
makeNode('some_var', 'identifier'), // not constant or scope_resolution
makeConstantArg('Mod2'),
];
const callNode = makeCallNode(args, 'class', 'MyClass');
const result = extractor.extractFromCall!('include', callNode as any, rubyCtx);
expect(result).toEqual([
{ className: 'MyClass', parentName: 'Mod1', kind: 'include' },
{ className: 'MyClass', parentName: 'Mod2', kind: 'include' },
]);
});
});
// ---------------------------------------------------------------------------
// Language-specific config validation
// ---------------------------------------------------------------------------
describe('HeritageExtraction language configs', () => {
it('Java config has correct language', () => {
expect(javaHeritageConfig.language).toBe(SupportedLanguages.Java);
expect(javaHeritageConfig.shouldSkipExtends).toBeUndefined();
expect(javaHeritageConfig.callBasedHeritage).toBeUndefined();
});
it('Kotlin config has correct language', () => {
expect(kotlinHeritageConfig.language).toBe(SupportedLanguages.Kotlin);
});
it('Go config has shouldSkipExtends hook', () => {
expect(goHeritageConfig.language).toBe(SupportedLanguages.Go);
expect(goHeritageConfig.shouldSkipExtends).toBeDefined();
expect(typeof goHeritageConfig.shouldSkipExtends).toBe('function');
});
it('Ruby config has callBasedHeritage', () => {
expect(rubyHeritageConfig.language).toBe(SupportedLanguages.Ruby);
expect(rubyHeritageConfig.callBasedHeritage).toBeDefined();
expect(rubyHeritageConfig.callBasedHeritage!.callNames).toEqual(
new Set(['include', 'extend', 'prepend']),
);
});
it('Swift config has correct language', () => {
expect(swiftHeritageConfig.language).toBe(SupportedLanguages.Swift);
});
it('Rust config has correct language', () => {
expect(rustHeritageConfig.language).toBe(SupportedLanguages.Rust);
});
it('TypeScript config has correct language', () => {
expect(typescriptHeritageConfig.language).toBe(SupportedLanguages.TypeScript);
});
it('JavaScript config has correct language', () => {
expect(javascriptHeritageConfig.language).toBe(SupportedLanguages.JavaScript);
});
it('C config has correct language', () => {
expect(cHeritageConfig.language).toBe(SupportedLanguages.C);
});
it('C++ config has correct language', () => {
expect(cppHeritageConfig.language).toBe(SupportedLanguages.CPlusPlus);
});
it('Python config has correct language', () => {
expect(pythonHeritageConfig.language).toBe(SupportedLanguages.Python);
});
it('PHP config has correct language', () => {
expect(phpHeritageConfig.language).toBe(SupportedLanguages.PHP);
});
it('Dart config has correct language', () => {
expect(dartHeritageConfig.language).toBe(SupportedLanguages.Dart);
});
it('C# config has correct language', () => {
expect(csharpHeritageConfig.language).toBe(SupportedLanguages.CSharp);
});
});