GitNexus/gitnexus/test/unit/scope-resolution/node-lookup-determinism.test.ts
Gergő Magyar 52924ef12c
perf(emit): drop six equivalent redundancies on Java-scale index path (#3129)
* perf(scope): reuse deferred sites and signatures for callable-value-flow

Pass the Phase 4 deferred-site collection and signature map into emitCallableValueFlow so the emit path does not rescan the same call sites.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(ingestion): nest owner registries instead of composite string keys

Look up methods, fields, and nested types via Map<owner, Map<name, defs>> while keeping EMPTY identity and TypeRegistry miss [] semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(scope): memoize resolveDefGraphId per nodeLookup

Cache graph ids on a WeakMap keyed by lookup identity so heritage rebuild invalidates, with an env opt-out that skips the cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(csv): prepare file content once and LRU-touch via Map order

Cache split lines and binary flags per source file so snippet and FTS extraction do not re-scan the same bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(identity): allow in-process cache guards only when opted in or unwritable

Keep the ≥128-guard subprocess default on writable installs; use direct snapshots when the env flag is set or W_OK fails on the analyzer tree.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: share empty CSV payload and restore spacing after memo key (U4, U3)

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(scope): assert resolveDefGraphId memo skips a second lookup walk

The ID-equality checks still passed with the memo disabled. Count Map#get on the lookup so a repeat call must hit the WeakMap cache. Also apply prettier on the remaining emit-path files CI flagged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: pin identity W_OK probes and memo env isolation

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: document emit-path env knobs and fix registry headers

CONTRIBUTING requires README rows for new GITNEXUS_* variables; also drop stale owner\0name comments after the nested-map change.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 11:22:55 +00:00

284 lines
8.6 KiB
TypeScript

import type { NodeLabel } from 'gitnexus-shared';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
import { mergeChunkResults } from '../../../src/core/ingestion/parsing-processor.js';
import {
buildGraphNodeLookup,
qualifiedKey,
simpleKey,
} from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
import { resolveDefGraphId } from '../../../src/core/ingestion/scope-resolution/graph-bridge/ids.js';
import type { ParseWorkerResult } from '../../../src/core/ingestion/workers/parse-worker.js';
const FILE = 'src/service.ts';
interface Candidate {
id: string;
label?: NodeLabel;
name?: string;
qualifiedName?: string;
startLine?: number;
startColumn?: number;
}
function buildLookup(candidates: readonly Candidate[]) {
const graph = createKnowledgeGraph();
const nodes = candidates.map(
(candidate) =>
({
id: candidate.id,
label: candidate.label ?? ('Method' as NodeLabel),
properties: {
name: candidate.name ?? 'save',
qualifiedName: candidate.qualifiedName ?? 'Service.save',
filePath: FILE,
...(candidate.startLine !== undefined ? { startLine: candidate.startLine } : {}),
...(candidate.startColumn !== undefined ? { startColumn: candidate.startColumn } : {}),
},
}) satisfies ParseWorkerResult['nodes'][number],
);
const result: ParseWorkerResult = {
nodes,
relationships: [],
symbols: [],
calls: [],
assignments: [],
routes: [],
fetchCalls: [],
fetchWrapperDefs: [],
decoratorRoutes: [],
routerIncludes: [],
routerImports: [],
toolDefs: [],
ormQueries: [],
constructorBindings: [],
fileScopeBindings: [],
parsedFiles: [],
skippedLanguages: {},
fileCount: 1,
};
mergeChunkResults(graph, createSemanticModel().symbols, [result]);
return buildGraphNodeLookup(graph);
}
describe('parse-result graph insertion determinism', () => {
it('selects the earliest source definition regardless of worker result order', () => {
const early = { id: `Method:${FILE}:Service.save#1`, startLine: 10 };
const late = { id: `Method:${FILE}:Service.save#2`, startLine: 20 };
const lateFirst = buildLookup([late, early]);
const earlyFirst = buildLookup([early, late]);
for (const key of [simpleKey(FILE, 'save'), qualifiedKey(FILE, 'Method', 'Service.save')]) {
expect(lateFirst.get(key)).toBe(early.id);
expect(earlyFirst.get(key)).toBe(early.id);
}
});
it('uses the stable node id when source positions are identical', () => {
const first = { id: `Method:${FILE}:Service.save#1`, startLine: 10 };
const second = { id: `Method:${FILE}:Service.save#2`, startLine: 10 };
const firstLookup = buildLookup([second, first]);
const secondLookup = buildLookup([first, second]);
expect(firstLookup.get(simpleKey(FILE, 'save'))).toBe(first.id);
expect(secondLookup.get(simpleKey(FILE, 'save'))).toBe(first.id);
});
it('uses the stable node id when source positions are unavailable', () => {
const first = { id: `Method:${FILE}:Service.save#1` };
const second = { id: `Method:${FILE}:Service.save#2` };
const lookup = buildLookup([second, first]);
expect(lookup.get(simpleKey(FILE, 'save'))).toBe(first.id);
});
it('uses exact columns to distinguish same-line owner-qualified callables', () => {
const first = {
id: `Function:${FILE}:first.handler`,
label: 'Function' as const,
name: 'handler',
qualifiedName: 'first.handler',
startLine: 4,
startColumn: 24,
};
const second = {
id: `Function:${FILE}:second.handler`,
label: 'Function' as const,
name: 'handler',
qualifiedName: 'second.handler',
startLine: 4,
startColumn: 73,
};
const lookup = buildLookup([second, first]);
expect(
resolveDefGraphId(
FILE,
{
nodeId: `def:${FILE}#5:24:Function:handler`,
type: 'Function',
qualifiedName: 'handler',
},
lookup,
),
).toBe(first.id);
expect(
resolveDefGraphId(
FILE,
{
nodeId: `def:${FILE}#5:73:Function:handler`,
type: 'Function',
qualifiedName: 'handler',
},
lookup,
),
).toBe(second.id);
});
it('uses exact position before parsing dotted member names as qualifiers', () => {
const dotted = {
id: `Function:${FILE}:service.q.r`,
label: 'Function' as const,
name: 'q.r',
qualifiedName: 'service.q.r',
startLine: 8,
startColumn: 31,
};
const lookup = buildLookup([dotted]);
expect(
resolveDefGraphId(
FILE,
{
nodeId: `def:${FILE}#9:31:Function:q.r`,
type: 'Function',
qualifiedName: 'q.r',
},
lookup,
),
).toBe(dotted.id);
});
it('resolves a Record definition to its Record node instead of a same-named fallback', () => {
const record = {
id: `Record:${FILE}:Person`,
label: 'Record' as const,
name: 'Person',
qualifiedName: 'Person',
startLine: 10,
};
const sameNamedMethod = {
id: `Method:${FILE}:Factory.Person#0`,
label: 'Method' as const,
name: 'Person',
qualifiedName: 'Factory.Person',
startLine: 20,
};
const lookup = buildLookup([sameNamedMethod, record]);
expect(lookup.get(qualifiedKey(FILE, 'Record', 'Person'))).toBe(record.id);
expect(
resolveDefGraphId(
FILE,
{
type: 'Record',
qualifiedName: 'Person',
},
lookup,
),
).toBe(record.id);
});
});
function countingLookup(inner: ReturnType<typeof buildLookup>): {
lookup: ReturnType<typeof buildLookup>;
gets: () => number;
} {
let n = 0;
const lookup = new Proxy(inner, {
get(target, prop, receiver) {
if (prop === 'get') {
return (key: string) => {
n += 1;
return target.get(key);
};
}
const value = Reflect.get(target, prop, receiver) as unknown;
return typeof value === 'function'
? (value as (...args: never[]) => unknown).bind(target)
: value;
},
});
return { lookup, gets: () => n };
}
describe('resolveDefGraphId memo', () => {
const MEMO_ENV = 'GITNEXUS_RESOLVE_DEF_GRAPH_ID_MEMO';
let previousMemoEnv: string | undefined;
beforeEach(() => {
previousMemoEnv = process.env[MEMO_ENV];
delete process.env[MEMO_ENV];
});
afterEach(() => {
if (previousMemoEnv === undefined) delete process.env[MEMO_ENV];
else process.env[MEMO_ENV] = previousMemoEnv;
});
it('returns the same id on a repeated lookup and does not leak across rebuilt lookups', () => {
const method = {
id: `Method:${FILE}:Service.save#1`,
label: 'Method' as const,
name: 'save',
qualifiedName: 'Service.save',
startLine: 10,
};
const countedA = countingLookup(buildLookup([method]));
const countedB = countingLookup(buildLookup([method]));
const def = {
type: 'Method' as const,
qualifiedName: 'Service.save',
nodeId: 'def:src/service.ts#11:0:Method:Service.save',
};
const first = resolveDefGraphId(FILE, def, countedA.lookup);
const getsAfterFirst = countedA.gets();
const second = resolveDefGraphId(FILE, def, countedA.lookup);
expect(first).toBe(method.id);
expect(second).toBe(first);
expect(getsAfterFirst).toBeGreaterThan(0);
expect(countedA.gets()).toBe(getsAfterFirst);
const otherLookup = resolveDefGraphId(FILE, def, countedB.lookup);
expect(otherLookup).toBe(method.id);
expect(countedB.gets()).toBeGreaterThan(0);
expect(countedA.lookup).not.toBe(countedB.lookup);
});
it('walks the lookup again when the memo env opt-out is set', () => {
process.env[MEMO_ENV] = '0';
const method = {
id: `Method:${FILE}:Service.save#1`,
label: 'Method' as const,
name: 'save',
qualifiedName: 'Service.save',
startLine: 10,
};
const counted = countingLookup(buildLookup([method]));
const def = {
type: 'Method' as const,
qualifiedName: 'Service.save',
nodeId: 'def:src/service.ts#11:0:Method:Service.save',
};
expect(resolveDefGraphId(FILE, def, counted.lookup)).toBe(method.id);
const getsAfterFirst = counted.gets();
expect(resolveDefGraphId(FILE, def, counted.lookup)).toBe(method.id);
expect(counted.gets()).toBeGreaterThan(getsAfterFirst);
});
});