mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
221 lines
7.5 KiB
TypeScript
221 lines
7.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { ParsedFile, ReferenceSite, ScopeId, Scope, SymbolDefinition } from 'gitnexus-shared';
|
|
import {
|
|
runScopeResolution,
|
|
type ScopeResolutionSubPhase,
|
|
} from '../../../src/core/ingestion/scope-resolution/pipeline/run.js';
|
|
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
|
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
|
|
import type { ScopeResolver } from '../../../src/core/ingestion/scope-resolution/contract/scope-resolver.js';
|
|
import type { LanguageProvider } from '../../../src/core/ingestion/language-provider.js';
|
|
|
|
const mkScope = (id: ScopeId, filePath: string): Scope => ({
|
|
id,
|
|
parent: null,
|
|
kind: 'Module',
|
|
range: { startLine: 1, startCol: 0, endLine: 10, endCol: 0 },
|
|
filePath,
|
|
bindings: new Map(),
|
|
ownedDefs: [],
|
|
imports: [],
|
|
typeBindings: new Map(),
|
|
});
|
|
|
|
const mkFile = (filePath: string): ParsedFile => ({
|
|
filePath,
|
|
moduleScope: `scope:${filePath}#module`,
|
|
scopes: [mkScope(`scope:${filePath}#module`, filePath)],
|
|
parsedImports: [],
|
|
localDefs: [],
|
|
referenceSites: [],
|
|
});
|
|
|
|
const stubProvider = {
|
|
language: 'python' as const,
|
|
languageProvider: {} as ScopeResolver['languageProvider'],
|
|
importEdgeReason: 'test',
|
|
populateOwners: () => {},
|
|
resolveImportTarget: () => null,
|
|
mergeBindings: (existing: unknown) => existing,
|
|
buildMro: () => new Map(),
|
|
propagatesReturnTypesAcrossImports: false,
|
|
} as unknown as ScopeResolver;
|
|
|
|
const providerWithEmitter = (emitScopeCaptures: LanguageProvider['emitScopeCaptures']) =>
|
|
({
|
|
...stubProvider,
|
|
languageProvider: { emitScopeCaptures } as LanguageProvider,
|
|
}) as ScopeResolver;
|
|
|
|
describe('runScopeResolution onProgress', () => {
|
|
it('exposes every source binding while preserving graph output with observation disabled', () => {
|
|
const run = (observe: boolean) => {
|
|
const graph = createKnowledgeGraph();
|
|
const definition: SymbolDefinition = {
|
|
nodeId: 'def:helper',
|
|
type: 'Function',
|
|
filePath: 'a.py',
|
|
qualifiedName: 'helper',
|
|
};
|
|
const parsed = mkFile('a.py');
|
|
const sites: ReferenceSite[] = [3, 4].map((line) => ({
|
|
name: 'helper',
|
|
kind: 'call',
|
|
callForm: 'free',
|
|
inScope: parsed.moduleScope,
|
|
atRange: { startLine: line, startCol: 0, endLine: line, endCol: 8 },
|
|
}));
|
|
const source = {
|
|
...parsed,
|
|
localDefs: [definition],
|
|
referenceSites: sites,
|
|
scopes: [
|
|
{
|
|
...parsed.scopes[0]!,
|
|
ownedDefs: [definition],
|
|
bindings: new Map([['helper', [{ def: definition, origin: 'local' as const }]]]),
|
|
},
|
|
],
|
|
};
|
|
graph.addNode({
|
|
id: 'function:helper',
|
|
label: 'Function',
|
|
properties: { name: 'helper', qualifiedName: 'helper', filePath: 'a.py' },
|
|
});
|
|
graph.addNode({
|
|
id: 'file:a.py',
|
|
label: 'File',
|
|
properties: { name: 'a.py', filePath: 'a.py' },
|
|
});
|
|
const observed: unknown[] = [];
|
|
runScopeResolution(
|
|
{
|
|
graph,
|
|
model: createSemanticModel(),
|
|
files: [{ path: 'a.py', content: 'def helper(): pass\nhelper()\nhelper()' }],
|
|
preExtractedParsedFiles: new Map([['a.py', source]]),
|
|
...(observe
|
|
? {
|
|
onResolved: (result) => {
|
|
for (const site of result.parsedFiles[0]!.referenceSites)
|
|
observed.push(result.referenceBindings.get(site));
|
|
},
|
|
}
|
|
: {}),
|
|
},
|
|
stubProvider,
|
|
);
|
|
return { graph, observed };
|
|
};
|
|
const plain = run(false);
|
|
const observed = run(true);
|
|
expect(observed.graph.relationships).toEqual(plain.graph.relationships);
|
|
expect(observed.observed).toHaveLength(2);
|
|
for (const binding of observed.observed)
|
|
expect(binding).toMatchObject({ status: 'resolved', targetDefIds: ['def:helper'] });
|
|
});
|
|
|
|
it('emits sub-phases in order for a 3-file input', () => {
|
|
const files = [
|
|
{ path: 'a.py', content: '' },
|
|
{ path: 'b.py', content: '' },
|
|
{ path: 'c.py', content: '' },
|
|
];
|
|
const preExtracted = new Map<string, ParsedFile>();
|
|
for (const f of files) preExtracted.set(f.path, mkFile(f.path));
|
|
|
|
const calls: { subPhase: ScopeResolutionSubPhase; current: number; total: number }[] = [];
|
|
const onProgress = (subPhase: ScopeResolutionSubPhase, current: number, total: number) => {
|
|
calls.push({ subPhase, current, total });
|
|
};
|
|
|
|
runScopeResolution(
|
|
{
|
|
graph: createKnowledgeGraph(),
|
|
model: createSemanticModel(),
|
|
files,
|
|
preExtractedParsedFiles: preExtracted,
|
|
onProgress,
|
|
},
|
|
stubProvider,
|
|
);
|
|
|
|
const subPhases = calls.map((c) => c.subPhase);
|
|
expect(subPhases).toContain('extracting');
|
|
expect(subPhases).toContain('analyzing types');
|
|
expect(subPhases).toContain('resolving references');
|
|
expect(subPhases).toContain('linking symbols');
|
|
|
|
const extractCalls = calls.filter((c) => c.subPhase === 'extracting');
|
|
expect(extractCalls.length).toBeGreaterThan(0);
|
|
expect(extractCalls[0].total).toBe(3);
|
|
expect(extractCalls[0].current).toBe(0);
|
|
expect(extractCalls[extractCalls.length - 1].current).toBe(3);
|
|
|
|
const analyzeIdx = subPhases.indexOf('analyzing types');
|
|
const resolveIdx = subPhases.indexOf('resolving references');
|
|
const linkIdx = subPhases.indexOf('linking symbols');
|
|
expect(analyzeIdx).toBeLessThan(resolveIdx);
|
|
expect(resolveIdx).toBeLessThan(linkIdx);
|
|
});
|
|
|
|
it('emits only extracting (0, 0) then returns early for 0-file input', () => {
|
|
const calls: { subPhase: ScopeResolutionSubPhase; current: number; total: number }[] = [];
|
|
const onProgress = (subPhase: ScopeResolutionSubPhase, current: number, total: number) => {
|
|
calls.push({ subPhase, current, total });
|
|
};
|
|
|
|
const stats = runScopeResolution(
|
|
{
|
|
graph: createKnowledgeGraph(),
|
|
model: createSemanticModel(),
|
|
files: [],
|
|
onProgress,
|
|
},
|
|
stubProvider,
|
|
);
|
|
|
|
expect(stats.filesProcessed).toBe(0);
|
|
expect(calls).toEqual([{ subPhase: 'extracting', current: 0, total: 0 }]);
|
|
});
|
|
|
|
it('counts empty migrated files as skipped without recording extraction failures', () => {
|
|
const stats = runScopeResolution(
|
|
{
|
|
graph: createKnowledgeGraph(),
|
|
model: createSemanticModel(),
|
|
files: [
|
|
{ path: 'empty.py', content: '' },
|
|
{ path: 'whitespace.py', content: ' \n\t' },
|
|
],
|
|
},
|
|
providerWithEmitter(() => {
|
|
throw new Error('empty files must short-circuit before capture');
|
|
}),
|
|
);
|
|
|
|
expect(stats.filesProcessed).toBe(0);
|
|
expect(stats.filesSkipped).toBe(2);
|
|
expect(stats.scopeExtractionFailedPaths).toEqual([]);
|
|
});
|
|
|
|
it('records a warned emitter failure as a final extraction omission', () => {
|
|
const warnings: string[] = [];
|
|
const stats = runScopeResolution(
|
|
{
|
|
graph: createKnowledgeGraph(),
|
|
model: createSemanticModel(),
|
|
files: [{ path: 'broken.py', content: 'value = 1' }],
|
|
onWarn: (warning) => warnings.push(warning),
|
|
},
|
|
providerWithEmitter(() => {
|
|
throw new Error('capture failed');
|
|
}),
|
|
);
|
|
|
|
expect(stats.filesProcessed).toBe(0);
|
|
expect(stats.filesSkipped).toBe(1);
|
|
expect(stats.scopeExtractionFailedPaths).toEqual(['broken.py']);
|
|
expect(warnings).toEqual([expect.stringContaining('capture failed')]);
|
|
});
|
|
});
|