GitNexus/gitnexus/test/unit/result-merge.test.ts
azizur100389 4f16bd8023
fix(impact): report scope extraction omissions (#3071)
* fix(impact): surface scope extraction omissions

* fix(impact): preserve complete index fixtures

* fix(impact): preserve scope completeness evidence

* test(analyze): model successful scope extraction in harnesses

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-29 08:38:20 +01:00

157 lines
5.5 KiB
TypeScript

/**
* #2135 tri-review (R12): the parse-worker result merge unions the clone-safety
* `skippedPaths` across sub-batch results. mergeResult was extracted from the
* parse-worker entry module into result-merge.ts so it can be unit-tested here
* (a main-thread import of parse-worker.ts would run its MessagePort setup).
*/
import { describe, it, expect } from 'vitest';
import { mergeResult } from '../../src/core/ingestion/workers/result-merge.js';
import type { ParseWorkerResult } from '../../src/core/ingestion/workers/parse-worker.js';
function emptyResult(): ParseWorkerResult {
return {
nodes: [],
relationships: [],
symbols: [],
calls: [],
assignments: [],
routes: [],
fetchCalls: [],
fetchWrapperDefs: [],
decoratorRoutes: [],
routerIncludes: [],
routerImports: [],
toolDefs: [],
ormQueries: [],
constructorBindings: [],
fileScopeBindings: [],
parsedFiles: [],
skippedLanguages: {},
fileCount: 0,
};
}
describe('mergeResult', () => {
it('unions skippedPaths across sub-batch results, initializing the target when absent', () => {
const target = emptyResult(); // no skippedPaths on the target (the `??=` path)
mergeResult(target, {
...emptyResult(),
skippedPaths: [{ path: 'a.ts', reason: 'r1' }],
fileCount: 1,
});
mergeResult(target, {
...emptyResult(),
skippedPaths: [{ path: 'b.ts', reason: 'r2' }],
fileCount: 1,
});
expect(target.skippedPaths).toEqual([
{ path: 'a.ts', reason: 'r1' },
{ path: 'b.ts', reason: 'r2' },
]);
expect(target.fileCount).toBe(2);
});
it('leaves skippedPaths undefined when no source carries any (no spurious empty array)', () => {
const target = emptyResult();
mergeResult(target, { ...emptyResult(), fileCount: 1 });
expect(target.skippedPaths).toBeUndefined();
});
it('unions scope-extraction failures across worker sub-batches', () => {
const target = emptyResult();
mergeResult(target, {
...emptyResult(),
scopeExtractionFailures: ['src/a.ts'],
});
mergeResult(target, {
...emptyResult(),
scopeExtractionFailures: ['src/b.ts'],
});
expect(target.scopeExtractionFailures).toEqual(['src/a.ts', 'src/b.ts']);
});
it('leaves scope-extraction failures absent for backward-compatible results', () => {
const target = emptyResult();
mergeResult(target, emptyResult());
expect(target.scopeExtractionFailures).toBeUndefined();
});
it('merges failure sets larger than the JavaScript argument limit', () => {
const target = emptyResult();
const scopeExtractionFailures = Array.from(
{ length: 70_000 },
(_, index) => `src/failure-${index}.ts`,
);
expect(() => mergeResult(target, { ...emptyResult(), scopeExtractionFailures })).not.toThrow();
expect(target.scopeExtractionFailures).toHaveLength(70_000);
expect(target.scopeExtractionFailures?.at(-1)).toBe('src/failure-69999.ts');
});
it('unions springTypes across sub-batch results, initializing the target when absent (#2288)', () => {
const mkType = (name: string, filePath: string) => ({
filePath,
kind: 'interface' as const,
name,
classPrefixes: [],
implementedInterfaces: [],
isController: false,
methods: [],
});
const target = emptyResult(); // no springTypes on the target (the `??=` path)
mergeResult(target, { ...emptyResult(), springTypes: [mkType('A', 'A.java')], fileCount: 1 });
mergeResult(target, { ...emptyResult(), springTypes: [mkType('B', 'B.java')], fileCount: 1 });
expect(target.springTypes?.map((t) => t.name)).toEqual(['A', 'B']);
});
it('leaves springTypes undefined when no source carries any (#2288)', () => {
const target = emptyResult();
mergeResult(target, { ...emptyResult(), fileCount: 1 });
expect(target.springTypes).toBeUndefined();
});
it('unions moduleConstants across sub-batch results, initializing the target when absent (#2391)', () => {
const mkConst = (filePath: string, name: string) => ({
filePath,
constants: {
literals: new Map([[name, '/a']]),
exprs: new Map(),
imports: new Map(),
},
});
// Regression: the worker-side accumulator dropped this field, so composed
// FastAPI route constants never reached parse-impl and resolved to `POST /`.
const target = emptyResult(); // no moduleConstants on the target (the `??=` path)
mergeResult(target, {
...emptyResult(),
moduleConstants: [mkConst('a.py', 'A')],
fileCount: 1,
});
mergeResult(target, {
...emptyResult(),
moduleConstants: [mkConst('b.py', 'B')],
fileCount: 1,
});
expect(target.moduleConstants?.map((m) => m.filePath)).toEqual(['a.py', 'b.py']);
});
it('leaves moduleConstants undefined when no source carries any (#2391)', () => {
const target = emptyResult();
mergeResult(target, { ...emptyResult(), fileCount: 1 });
expect(target.moduleConstants).toBeUndefined();
});
it('also sums skippedLanguages and appends node arrays (sanity of the rest of the merge)', () => {
const target = { ...emptyResult(), skippedLanguages: { rust: 1 } };
mergeResult(target, {
...emptyResult(),
nodes: [
{ id: 'n', label: 'Function', properties: { name: 'n' } },
] as ParseWorkerResult['nodes'],
skippedLanguages: { rust: 2, go: 1 },
});
expect(target.skippedLanguages).toEqual({ rust: 3, go: 1 });
expect(target.nodes).toHaveLength(1);
});
});