diff --git a/gitnexus/test/helpers/cfg-harness.ts b/gitnexus/test/helpers/cfg-harness.ts index e5d787836..eb7b5c04c 100644 --- a/gitnexus/test/helpers/cfg-harness.ts +++ b/gitnexus/test/helpers/cfg-harness.ts @@ -29,7 +29,7 @@ export interface CfgHarness { * CFG shape). The parser is created once and reused across parses. */ export function makeCfgHarness( - grammar: Parser.Language, + grammar: Parameters[0], visitor: CfgVisitor, filePath = 'fixture', ): CfgHarness { diff --git a/gitnexus/test/helpers/parse-source-safe-mock.ts b/gitnexus/test/helpers/parse-source-safe-mock.ts index 8cc0646a4..78a6df6d2 100644 --- a/gitnexus/test/helpers/parse-source-safe-mock.ts +++ b/gitnexus/test/helpers/parse-source-safe-mock.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest'; +import { vi, type Mock } from 'vitest'; import type * as SafeParseModule from '../../src/core/tree-sitter/safe-parse.js'; /** @@ -43,7 +43,7 @@ import type * as SafeParseModule from '../../src/core/tree-sitter/safe-parse.js' * }); */ export async function buildSafeParseMock( - spy: ReturnType, + spy: Mock, ): Promise { const actual = await vi.importActual( '../../src/core/tree-sitter/safe-parse.js', diff --git a/gitnexus/test/helpers/parse-typescript-source.ts b/gitnexus/test/helpers/parse-typescript-source.ts index d324d84d8..0935a240d 100644 --- a/gitnexus/test/helpers/parse-typescript-source.ts +++ b/gitnexus/test/helpers/parse-typescript-source.ts @@ -30,10 +30,8 @@ export interface ParsedSource { const PARSE_PLUGINS: NonNullable[1]>['plugins'] = [ 'typescript', - 'explicitResourceManagement', - 'importAttributes', 'decoratorAutoAccessors', - ['decorators', { decoratorsBeforeExport: true }], + 'decorators', ]; export function forEachChild(node: Node, visit: (child: AstNode) => void): void { diff --git a/gitnexus/test/helpers/perf-sequencer.ts b/gitnexus/test/helpers/perf-sequencer.ts index d5f8604a3..9dd6d1fb3 100644 --- a/gitnexus/test/helpers/perf-sequencer.ts +++ b/gitnexus/test/helpers/perf-sequencer.ts @@ -17,7 +17,19 @@ export default class PerfSequencer extends BaseSequencer { override async shard(specs: TestSpecification[]): Promise { const shard = this.ctx.config.shard; if (!shard) return specs; - const groups = assignShards(specs, shard.count, specWeight, (spec) => spec.moduleId); + const groups = assignShards( + specs, + shard.count, + (spec) => { + const config = spec.project.config; + const fileParallelism = + 'fileParallelism' in config && typeof config.fileParallelism === 'boolean' + ? config.fileParallelism + : undefined; + return specWeight({ moduleId: spec.moduleId, project: { config: { fileParallelism } } }); + }, + (spec) => spec.moduleId, + ); return groups[shard.index - 1] ?? []; } } diff --git a/gitnexus/test/helpers/test-graph.ts b/gitnexus/test/helpers/test-graph.ts index 834d0df9b..a0a589e97 100644 --- a/gitnexus/test/helpers/test-graph.ts +++ b/gitnexus/test/helpers/test-graph.ts @@ -5,7 +5,8 @@ * without touching the filesystem or LadybugDB. */ import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; -import type { KnowledgeGraph, NodeLabel, RelationshipType } from '../../src/core/graph/types.js'; +import type { KnowledgeGraph } from '../../src/core/graph/types.js'; +import type { NodeLabel, RelationshipType } from 'gitnexus-shared'; export interface TestNodeInput { id: string; diff --git a/gitnexus/test/integration/api-query.test.ts b/gitnexus/test/integration/api-query.test.ts index 70cc81287..caac05dcd 100644 --- a/gitnexus/test/integration/api-query.test.ts +++ b/gitnexus/test/integration/api-query.test.ts @@ -51,8 +51,8 @@ withTestLbugDB( }); expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body.result)).toBe(true); - expect(body.result[0].one).toBe(1); + expect(body).toHaveProperty('result', expect.any(Array)); + expect(body).toHaveProperty('result.0.one', 1); }); it('returns 403 for a write query on read-only HTTP path', async () => { @@ -65,7 +65,10 @@ withTestLbugDB( }); expect(response.status).toBe(403); const body = await response.json(); - expect(body.error).toContain('Write queries are not allowed'); + expect(body).toHaveProperty( + 'error', + expect.stringContaining('Write queries are not allowed'), + ); }); it('returns 400 for invalid params payload', async () => { @@ -76,7 +79,7 @@ withTestLbugDB( }); expect(response.status).toBe(400); const body = await response.json(); - expect(body.error).toContain('"params"'); + expect(body).toHaveProperty('error', expect.stringContaining('"params"')); }); it('returns 400 when cypher is missing', async () => { @@ -87,7 +90,7 @@ withTestLbugDB( }); expect(response.status).toBe(400); const body = await response.json(); - expect(body.error).toContain('Missing "cypher"'); + expect(body).toHaveProperty('error', expect.stringContaining('Missing "cypher"')); }); }); }, diff --git a/gitnexus/test/integration/cfg/cfg-emit.test.ts b/gitnexus/test/integration/cfg/cfg-emit.test.ts index 66f9a35fc..32ed86652 100644 --- a/gitnexus/test/integration/cfg/cfg-emit.test.ts +++ b/gitnexus/test/integration/cfg/cfg-emit.test.ts @@ -490,6 +490,7 @@ describe('U4 (#2085 M5) — emitFileCdg', () => { const unsound: FunctionCfg = { filePath: 'spin.ts', functionStartLine: 1, + functionEndLine: 4, functionStartColumn: 0, entryIndex: 0, exitIndex: 3, diff --git a/gitnexus/test/integration/clean-stale-branch-slots.test.ts b/gitnexus/test/integration/clean-stale-branch-slots.test.ts index b287da7e6..4cdf4605b 100644 --- a/gitnexus/test/integration/clean-stale-branch-slots.test.ts +++ b/gitnexus/test/integration/clean-stale-branch-slots.test.ts @@ -68,7 +68,7 @@ describe('clean --stale leftover branch slots (#3331)', () => { } vi.spyOn(process, 'cwd').mockReturnValue(repo); vi.spyOn(console, 'log').mockImplementation(() => {}); - return { dir, storagePath }; + return { repo, dir, storagePath }; } it('reclaims a slot after the git branch is deleted', async () => { diff --git a/gitnexus/test/integration/data-route-table-pipeline.test.ts b/gitnexus/test/integration/data-route-table-pipeline.test.ts index 0965a80b3..7bcaa18ec 100644 --- a/gitnexus/test/integration/data-route-table-pipeline.test.ts +++ b/gitnexus/test/integration/data-route-table-pipeline.test.ts @@ -3,7 +3,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; import { DATA_ROUTE_TABLE_SOURCE } from '../../src/core/ingestion/route-extractors/data-route-table.js'; import { loadParseCache, diff --git a/gitnexus/test/integration/dispatch-guard-route-pipeline.test.ts b/gitnexus/test/integration/dispatch-guard-route-pipeline.test.ts index a8fb9153a..1d693b33b 100644 --- a/gitnexus/test/integration/dispatch-guard-route-pipeline.test.ts +++ b/gitnexus/test/integration/dispatch-guard-route-pipeline.test.ts @@ -20,7 +20,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; import { DISPATCH_GUARD_SOURCE } from '../../src/core/ingestion/route-extractors/dispatch-guard.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'dispatch-guard-app'); diff --git a/gitnexus/test/integration/django-route-extraction-e2e.test.ts b/gitnexus/test/integration/django-route-extraction-e2e.test.ts index 3b11cc1bf..0ef6a13e6 100644 --- a/gitnexus/test/integration/django-route-extraction-e2e.test.ts +++ b/gitnexus/test/integration/django-route-extraction-e2e.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'django-subdir-app'); diff --git a/gitnexus/test/integration/doc-comment-description-e2e.test.ts b/gitnexus/test/integration/doc-comment-description-e2e.test.ts index 9424ae32e..dcdb5057e 100644 --- a/gitnexus/test/integration/doc-comment-description-e2e.test.ts +++ b/gitnexus/test/integration/doc-comment-description-e2e.test.ts @@ -59,7 +59,6 @@ describe('doc-comment description end-to-end (issue #2270)', () => { it('surfaces an exported function JSDoc as its node description through the pipeline', async () => { const result: PipelineResult = await runPipelineFromRepo(createTsRepo(), () => {}, { skipGraphPhases: true, - workerThresholdsForTest: { minFiles: 1, minBytes: 1 }, workerPoolSize: 2, }); @@ -79,7 +78,6 @@ describe('doc-comment description end-to-end (issue #2270)', () => { async () => { const result: PipelineResult = await runPipelineFromRepo(createSwiftRepo(), () => {}, { skipGraphPhases: true, - workerThresholdsForTest: { minFiles: 1, minBytes: 1 }, workerPoolSize: 2, }); diff --git a/gitnexus/test/integration/expo-routes.test.ts b/gitnexus/test/integration/expo-routes.test.ts index 74c63dc2a..066c4bcf9 100644 --- a/gitnexus/test/integration/expo-routes.test.ts +++ b/gitnexus/test/integration/expo-routes.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const EXPO_APP = path.resolve(__dirname, '..', 'fixtures', 'expo-app'); diff --git a/gitnexus/test/integration/fastapi-composed-route-constants.test.ts b/gitnexus/test/integration/fastapi-composed-route-constants.test.ts index 89e6bcb45..1c066154e 100644 --- a/gitnexus/test/integration/fastapi-composed-route-constants.test.ts +++ b/gitnexus/test/integration/fastapi-composed-route-constants.test.ts @@ -21,7 +21,7 @@ import * as os from 'node:os'; import Parser from 'tree-sitter'; import Python from 'tree-sitter-python'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; import { PYTHON_HTTP_PLUGIN } from '../../src/core/group/extractors/http-patterns/python.js'; import { loadParseCache, @@ -141,6 +141,7 @@ describe('FastAPI composed route constants — ingestion↔group parity (#2391 R return p.parse(src); }; const ctx = PYTHON_HTTP_PLUGIN.prepareRepo?.({ + repoPath: '/test-repo', files: Object.keys(files), parser, readFile: (r) => files[r] ?? null, diff --git a/gitnexus/test/integration/fastapi-prefix-pipeline.test.ts b/gitnexus/test/integration/fastapi-prefix-pipeline.test.ts index 0450f123b..07b269847 100644 --- a/gitnexus/test/integration/fastapi-prefix-pipeline.test.ts +++ b/gitnexus/test/integration/fastapi-prefix-pipeline.test.ts @@ -46,7 +46,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'fastapi-prefix-app'); diff --git a/gitnexus/test/integration/fts-repair-warm-session.test.ts b/gitnexus/test/integration/fts-repair-warm-session.test.ts index ac9ce2713..d6e5a3ed6 100644 --- a/gitnexus/test/integration/fts-repair-warm-session.test.ts +++ b/gitnexus/test/integration/fts-repair-warm-session.test.ts @@ -92,7 +92,7 @@ describe('warm MCP session observes an in-place --repair-fts rebuild (#2767)', ( await tmpHandle.cleanup(); }); - it.each(['all', 'Function'] as const)( + it.for(['all', 'Function'] as const)( 'a warm session repairs missing %s indexes without restarting', { timeout: 60_000 }, async (missing, ctx) => { diff --git a/gitnexus/test/integration/go-multi-name-worker-metadata.test.ts b/gitnexus/test/integration/go-multi-name-worker-metadata.test.ts index fbf97646f..4065772d4 100644 --- a/gitnexus/test/integration/go-multi-name-worker-metadata.test.ts +++ b/gitnexus/test/integration/go-multi-name-worker-metadata.test.ts @@ -32,7 +32,6 @@ function createGoRepo(): string { async function runWorker(): Promise { return runPipelineFromRepo(createGoRepo(), () => {}, { skipGraphPhases: true, - workerThresholdsForTest: { minFiles: 1, minBytes: 1 }, workerPoolSize: 2, }); } diff --git a/gitnexus/test/integration/go-pipeline-benchmark.test.ts b/gitnexus/test/integration/go-pipeline-benchmark.test.ts index ac3c0c4cf..095c1f330 100644 --- a/gitnexus/test/integration/go-pipeline-benchmark.test.ts +++ b/gitnexus/test/integration/go-pipeline-benchmark.test.ts @@ -32,7 +32,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared'; +import { makeScopeId, type ParsedFile, type SymbolDefinition } from 'gitnexus-shared'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; import { emitGoScopeCaptures, @@ -587,14 +587,17 @@ function generateSyntheticInterfaceData(interfaceCount: number, structCount: num return [ { filePath: 'repo.go', - language: 'go', + moduleScope: makeScopeId({ + filePath: 'repo.go', + range: { startLine: 0, startCol: 0, endLine: 0, endCol: 0 }, + kind: 'Module', + }), scopes: [], - imports: [], parsedImports: [], localDefs: defs, referenceSites: [], }, - ] as ParsedFile[]; + ]; } /** diff --git a/gitnexus/test/integration/impact-context-integrity.test.ts b/gitnexus/test/integration/impact-context-integrity.test.ts index 0e42de484..f26151faf 100644 --- a/gitnexus/test/integration/impact-context-integrity.test.ts +++ b/gitnexus/test/integration/impact-context-integrity.test.ts @@ -447,7 +447,14 @@ describe('native string projections after checkpointed deletion (#3354)', () => for (const order of ['', ' ORDER BY n.startLine']) { const rows = await read(`MATCH (n:Function) ${projection}${order}`); expect(new Set(rows.map((row) => row.id)).size).toBe(surviving.length); - expect(rows.sort((a, b) => a.startLine - b.startLine)).toEqual(surviving); + expect( + rows.sort((a, b) => { + if (typeof a.startLine !== 'number' || typeof b.startLine !== 'number') { + throw new Error('Expected numeric startLine values from the database'); + } + return a.startLine - b.startLine; + }), + ).toEqual(surviving); } } // Point lookups independently verify values in the affected segments; diff --git a/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts b/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts index 2ef3e6c78..458a19155 100644 --- a/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts +++ b/gitnexus/test/integration/impact-epistemic-lower-bound.test.ts @@ -124,12 +124,15 @@ withTestLbugDB( it('marks impact as a lower bound when scope extraction omitted files', async () => { vi.mocked(loadMeta).mockResolvedValueOnce({ + repoPath: '/test/repo', + lastCommit: 'abc123', + indexedAt: new Date().toISOString(), scopeExtractionReceipt: 1, scopeExtractionFailures: { total: 2, paths: ['src/broken-a.ts', 'src/broken-b.ts'], }, - } as Awaited>); + }); const result = await backend.callTool('impact', { target: 'formatDate', @@ -143,12 +146,15 @@ withTestLbugDB( it('never renders repository-controlled failure paths in boundary prose', async () => { vi.mocked(loadMeta).mockResolvedValueOnce({ + repoPath: '/test/repo', + lastCommit: 'abc123', + indexedAt: new Date().toISOString(), scopeExtractionReceipt: 1, scopeExtractionFailures: { total: 1, paths: ['src/`break`\n\u001b[31m\u202e\u200binject.ts'], }, - } as Awaited>); + }); const result = await backend.callTool('impact', { target: 'formatDate', @@ -239,12 +245,15 @@ withTestLbugDB( it('context() reports persisted scope extraction omissions as a lower bound', async () => { vi.mocked(loadMeta).mockResolvedValueOnce({ + repoPath: '/test/repo', + lastCommit: 'abc123', + indexedAt: new Date().toISOString(), scopeExtractionReceipt: 1, scopeExtractionFailures: { total: 2, paths: ['src/broken-a.ts', 'src/broken-b.ts'], }, - } as Awaited>); + }); const result = await backend.callTool('context', { name: 'formatDate', diff --git a/gitnexus/test/integration/impact-pdg-shape.test.ts b/gitnexus/test/integration/impact-pdg-shape.test.ts index 0458ab03d..3d83a6fbf 100644 --- a/gitnexus/test/integration/impact-pdg-shape.test.ts +++ b/gitnexus/test/integration/impact-pdg-shape.test.ts @@ -279,6 +279,7 @@ withTestLbugDB( const result = await downstream(); const localRisk = String(result.risk); expect(localRisk).toBe('UNKNOWN'); + if (localRisk !== 'UNKNOWN') throw new Error(`Expected UNKNOWN risk, got ${localRisk}`); // No cross-repo hits → mergeRisk returns localRisk verbatim: 'UNKNOWN', // NEVER coerced to a confident 'LOW' (the false-safe this guards). expect(mergeRisk(localRisk, [])).toBe('UNKNOWN'); diff --git a/gitnexus/test/integration/kotlin-spring-route-pipeline.test.ts b/gitnexus/test/integration/kotlin-spring-route-pipeline.test.ts index c7f29f912..6a931cd4e 100644 --- a/gitnexus/test/integration/kotlin-spring-route-pipeline.test.ts +++ b/gitnexus/test/integration/kotlin-spring-route-pipeline.test.ts @@ -10,7 +10,7 @@ import os from 'node:os'; import path from 'node:path'; import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; import { loadParseCache, PARSE_CACHE_VERSION, diff --git a/gitnexus/test/integration/local-backend-calltool.test.ts b/gitnexus/test/integration/local-backend-calltool.test.ts index 35eabbdd4..6477a5fce 100644 --- a/gitnexus/test/integration/local-backend-calltool.test.ts +++ b/gitnexus/test/integration/local-backend-calltool.test.ts @@ -356,7 +356,9 @@ withTestLbugDB( const result = await backend.callTool('tool_map', {}); expect(result).not.toHaveProperty('error'); - const tools = new Map(result.tools.map((tool: any) => [tool.name, tool])); + const toolEntries: Array<{ name: string; description: string; flows: string[] }> = + result.tools; + const tools = new Map(toolEntries.map((tool) => [tool.name, tool])); expect(tools.get('alpha')?.description).toBe('Calls chain A.'); expect(tools.get('beta')?.description).toBe('Calls chain B.'); expect(tools.get('alpha')?.flows).toEqual(['AlphaFlow']); diff --git a/gitnexus/test/integration/mcp/update-notice.test.ts b/gitnexus/test/integration/mcp/update-notice.test.ts index a1380af39..654d97524 100644 --- a/gitnexus/test/integration/mcp/update-notice.test.ts +++ b/gitnexus/test/integration/mcp/update-notice.test.ts @@ -18,7 +18,7 @@ interface FakeChecker { } interface FakeLogger { - info: ReturnType; + info: ReturnType, message: string) => unknown>>; } function checker(initial: UpdateState | null): { diff --git a/gitnexus/test/integration/multi-verb-route-identity.test.ts b/gitnexus/test/integration/multi-verb-route-identity.test.ts index d488c2522..f0d270358 100644 --- a/gitnexus/test/integration/multi-verb-route-identity.test.ts +++ b/gitnexus/test/integration/multi-verb-route-identity.test.ts @@ -21,7 +21,7 @@ import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; import { generateId } from '../../src/lib/utils.js'; import { routeNodeKey } from '../../src/core/ingestion/route-extractors/route-path.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'multi-verb-route-app'); diff --git a/gitnexus/test/integration/pdg-query.test.ts b/gitnexus/test/integration/pdg-query.test.ts index de5477d8a..a138d6766 100644 --- a/gitnexus/test/integration/pdg-query.test.ts +++ b/gitnexus/test/integration/pdg-query.test.ts @@ -134,7 +134,7 @@ withTestLbugDB( // An MCP client may send {"name":"pdg_query"} with no `arguments` field; // the dispatch then hands `params: undefined` to the impl. It must // default to {} and surface the mode-validation error, not a TypeError. - const result = await backend.callTool('pdg_query'); + const result = await backend.callTool('pdg_query', undefined); expect(result).toHaveProperty('error'); expect(result.error).toMatch(/mode/i); }); diff --git a/gitnexus/test/integration/resolvers/callable-value-flow.test.ts b/gitnexus/test/integration/resolvers/callable-value-flow.test.ts index e1502ab0b..6e2942945 100644 --- a/gitnexus/test/integration/resolvers/callable-value-flow.test.ts +++ b/gitnexus/test/integration/resolvers/callable-value-flow.test.ts @@ -323,7 +323,12 @@ function callableTargetQualifiedNames( ): string[] { return getRelationships(result, 'CALLS') .filter((edge) => edge.source === source && edge.rel.reason === 'callable-value-flow') - .map((edge) => result.graph.getNode(edge.rel.targetId)?.properties.qualifiedName ?? edge.target) + .map((edge) => { + const qualifiedName = result.graph.getNode(edge.rel.targetId)?.properties.qualifiedName; + if (qualifiedName == null) return edge.target; + if (typeof qualifiedName !== 'string') throw new Error('Expected a string qualified name'); + return qualifiedName; + }) .sort(); } @@ -347,7 +352,10 @@ describe('callable value flow', () => { it('does not overstate Objective-C callable-value-flow coverage', () => { expect(CALLABLE_FLOW_PROVIDER_COVERAGE[SupportedLanguages.ObjectiveC]).toBe('not-applicable'); expect( - PROVIDER_FLOW_CASES.some(({ language }) => language === SupportedLanguages.ObjectiveC), + PROVIDER_FLOW_CASES.some( + ({ language }: { language: SupportedLanguages }) => + language === SupportedLanguages.ObjectiveC, + ), ).toBe(false); }); diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 4bcb4e0bd..47899b348 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -1757,7 +1757,11 @@ describe('C++ template specialization disambiguation across files', () => { expect(targetOwnerEdge).toBeDefined(); expect(sourceOwnerEdge!.rel.sourceId).toBe(targetOwnerEdge!.rel.sourceId); const ownerNode = result.graph.getNode(sourceOwnerEdge!.rel.sourceId); - const fp = ownerNode?.properties.templateArguments?.join(','); + const templateArguments = ownerNode?.properties.templateArguments; + if (templateArguments != null && !Array.isArray(templateArguments)) { + throw new Error('Expected template arguments to be an array'); + } + const fp = Array.isArray(templateArguments) ? templateArguments.join(',') : undefined; if (fp) ownerFingerprints.add(fp); } expect(ownerFingerprints).toEqual(new Set(['User', 'Order'])); @@ -2125,14 +2129,15 @@ describe('C++ ambiguous integer-width overloads', () => { }); it('records a structured suppression reason for normalization ambiguity', () => { - const outcomes = getResolutionOutcomes(result).filter( - (o) => - o.kind === 'suppressed' && - o.name === 'process' && - o.phase === 'receiver-bound-calls' && - o.filePath.endsWith('caller.cpp') && - o.reason === 'overload-ambiguous-normalization', - ); + const outcomes = getResolutionOutcomes(result) + .filter((o) => o.kind === 'suppressed') + .filter( + (o) => + o.name === 'process' && + o.phase === 'receiver-bound-calls' && + o.filePath.endsWith('caller.cpp') && + o.reason === 'overload-ambiguous-normalization', + ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(2); @@ -2214,13 +2219,12 @@ describe('C++ overload resolution — conversion-rank disambiguation (#1578)', ( }); it('records a structured suppression reason for conversion-rank ties', () => { - const outcomes = getResolutionOutcomes(result).filter( - (o) => - o.kind === 'suppressed' && - o.name === 'h' && - o.phase === 'free-call-fallback' && - o.reason === 'conversion-rank-tied', - ); + const outcomes = getResolutionOutcomes(result) + .filter((o) => o.kind === 'suppressed') + .filter( + (o) => + o.name === 'h' && o.phase === 'free-call-fallback' && o.reason === 'conversion-rank-tied', + ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(2); @@ -3256,13 +3260,14 @@ describe('C++ ADL — non-function ordinary lookup suppresses ADL', () => { }); it('records a structured suppression reason for ADL blocker lookup', () => { - const outcomes = getResolutionOutcomes(result).filter( - (o) => - o.kind === 'suppressed' && - o.name === 'record' && - o.phase === 'free-call-fallback' && - o.reason === 'adl-ordinary-lookup-blocked', - ); + const outcomes = getResolutionOutcomes(result) + .filter((o) => o.kind === 'suppressed') + .filter( + (o) => + o.name === 'record' && + o.phase === 'free-call-fallback' && + o.reason === 'adl-ordinary-lookup-blocked', + ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(0); @@ -3552,13 +3557,14 @@ describe('C++ inline namespace — ambiguous same-name across inline children (# }); it('records a structured suppression reason for inline namespace ambiguity', () => { - const outcomes = getResolutionOutcomes(result).filter( - (o) => - o.kind === 'suppressed' && - o.name === 'foo' && - o.phase === 'receiver-bound-calls' && - o.reason === 'inline-ns-ambiguous', - ); + const outcomes = getResolutionOutcomes(result) + .filter((o) => o.kind === 'suppressed') + .filter( + (o) => + o.name === 'foo' && + o.phase === 'receiver-bound-calls' && + o.reason === 'inline-ns-ambiguous', + ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(0); diff --git a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts index 159c0d694..6df379155 100644 --- a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts +++ b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts @@ -413,6 +413,7 @@ describe('JavaScript plain-object property access (A1/A5)', () => { }); interface PropNode { + readonly id: string; readonly label: string; readonly properties: Record; } diff --git a/gitnexus/test/integration/resolvers/python.test.ts b/gitnexus/test/integration/resolvers/python.test.ts index 646fef3b0..57dc088d0 100644 --- a/gitnexus/test/integration/resolvers/python.test.ts +++ b/gitnexus/test/integration/resolvers/python.test.ts @@ -1146,6 +1146,7 @@ describe('Python mixin self-dispatch', () => { expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.filePath === 'mixins.py' && outcome.name === 'class_only' && outcome.reason === 'receiver-unresolved', @@ -1202,11 +1203,9 @@ describe('Python mixin self-dispatch', () => { it('records only the expected mixin dispatch gaps and partial coverage', () => { const unresolvedSites = getResolutionOutcomes(result) + .filter((outcome) => outcome.kind === 'suppressed') .filter( - (outcome) => - outcome.kind === 'suppressed' && - outcome.reason === 'receiver-unresolved' && - outcome.filePath === 'mixins.py', + (outcome) => outcome.reason === 'receiver-unresolved' && outcome.filePath === 'mixins.py', ) .map((outcome) => `${outcome.range.startLine}:${outcome.name}`) .sort(); @@ -1325,6 +1324,7 @@ describe('Python mixin self-dispatch', () => { expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.filePath === 'mixins.py' && outcome.name === 'duplicate_hook' && outcome.reason === 'member-lookup-ambiguous', @@ -1378,12 +1378,9 @@ describe('Python unproven subtype methods', () => { expect(calls.map((call) => call.rel.targetId)).toEqual([ expect.stringContaining('Concrete.hook'), ]); - const unresolved = getResolutionOutcomes(result).filter( - (outcome) => - outcome.kind === 'suppressed' && - outcome.name === 'hook' && - outcome.reason === 'receiver-unresolved', - ); + const unresolved = getResolutionOutcomes(result) + .filter((outcome) => outcome.kind === 'suppressed') + .filter((outcome) => outcome.name === 'hook' && outcome.reason === 'receiver-unresolved'); expect(unresolved.flatMap((outcome) => outcome.candidateIds).sort()).toEqual([ // AbstractWorker.hook (line 10) and Receiverless.hook (line 13). 'def:worker.py#10:4:Method:hook', @@ -1419,6 +1416,7 @@ describe('Python unproven subtype methods', () => { ]); expect( getResolutionOutcomes(result) + .filter((outcome) => outcome.kind === 'suppressed') .filter((outcome) => outcome.name === 'hook' && outcome.reason === 'receiver-unresolved') .flatMap((outcome) => outcome.candidateIds), ).toEqual([expect.stringMatching(/:Class:Base$/)]); @@ -1459,6 +1457,7 @@ class MissingHook(Mixin): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.name === 'hook' && outcome.reason === 'receiver-unresolved' && outcome.candidateIds.some((id) => id.endsWith(':Class:MissingHook')), @@ -1547,7 +1546,10 @@ class Mixin: ); expect( getResolutionOutcomes(result).some( - (outcome) => outcome.name === target && outcome.reason === 'receiver-unresolved', + (outcome) => + outcome.kind === 'suppressed' && + outcome.name === target && + outcome.reason === 'receiver-unresolved', ), ).toBe(true); } @@ -1647,7 +1649,10 @@ class ReboundProperty: ); expect( getResolutionOutcomes(result).some( - (outcome) => outcome.name === target && outcome.reason === 'receiver-unresolved', + (outcome) => + outcome.kind === 'suppressed' && + outcome.name === target && + outcome.reason === 'receiver-unresolved', ), ).toBe(true); } @@ -1660,13 +1665,17 @@ class ReboundProperty: expect( getResolutionOutcomes(result).some( (outcome) => - outcome.name === 'wrapped_helper' && outcome.reason === 'receiver-unresolved', + outcome.kind === 'suppressed' && + outcome.name === 'wrapped_helper' && + outcome.reason === 'receiver-unresolved', ), ).toBe(true); expect( getResolutionOutcomes(result).some( (outcome) => - outcome.name === 'rebound_helper' && outcome.reason === 'receiver-unresolved', + outcome.kind === 'suppressed' && + outcome.name === 'rebound_helper' && + outcome.reason === 'receiver-unresolved', ), ).toBe(true); } finally { @@ -1752,6 +1761,7 @@ class Worker(Mixin): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.filePath === 'case.py' && outcome.name === 'hook' && outcome.reason === 'receiver-unresolved', @@ -1760,6 +1770,7 @@ class Worker(Mixin): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.filePath === 'case.py' && outcome.name === 'own_hook' && outcome.reason === 'receiver-unresolved', @@ -1768,6 +1779,7 @@ class Worker(Mixin): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.filePath === 'case.py' && outcome.name === 'unicode_helper' && outcome.reason === 'receiver-unresolved', @@ -1776,6 +1788,7 @@ class Worker(Mixin): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.filePath === 'case.py' && outcome.name === 'nested_hook' && outcome.reason === 'receiver-unresolved', @@ -1855,6 +1868,7 @@ class DirectWorker(HookMixin, external.Parent, First, Second): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.name === 'hook' && outcome.reason === 'receiver-unresolved' && outcome.candidateIds.some((id) => id.endsWith(':Class:Worker')), @@ -1896,6 +1910,7 @@ class DirectWorker(HookMixin): expect( getResolutionOutcomes(result).some( (outcome) => + outcome.kind === 'suppressed' && outcome.name === 'hook' && outcome.reason === 'receiver-unresolved' && outcome.candidateIds.some((id) => id.endsWith(':Class:Worker')), diff --git a/gitnexus/test/integration/resolvers/value-ref-locality.test.ts b/gitnexus/test/integration/resolvers/value-ref-locality.test.ts index 9a1d6ee45..cfa1c931a 100644 --- a/gitnexus/test/integration/resolvers/value-ref-locality.test.ts +++ b/gitnexus/test/integration/resolvers/value-ref-locality.test.ts @@ -47,7 +47,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'node:path'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../../types/pipeline.js'; +import type { PipelineResult } from '../../../src/types/pipeline.js'; const LANG_FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution'); diff --git a/gitnexus/test/integration/shared-store-analyze.test.ts b/gitnexus/test/integration/shared-store-analyze.test.ts index 2c08b47a3..55062266a 100644 --- a/gitnexus/test/integration/shared-store-analyze.test.ts +++ b/gitnexus/test/integration/shared-store-analyze.test.ts @@ -186,9 +186,9 @@ describe('shared sibling store analyze (#3352)', () => { const conn = new lbug.Connection(db); let rows: { p: string }[]; try { - rows = (await ( - await conn.query('MATCH (f:File) RETURN f.filePath AS p ORDER BY p') - ).getAll()) as { p: string }[]; + const result = await conn.query('MATCH (f:File) RETURN f.filePath AS p ORDER BY p'); + if (Array.isArray(result)) throw new Error('Expected a single query result'); + rows = (await result.getAll()) as { p: string }[]; } finally { await conn.close(); await db.close(); diff --git a/gitnexus/test/integration/shared-store-seed.test.ts b/gitnexus/test/integration/shared-store-seed.test.ts index 2b50704b1..951a9f9c8 100644 --- a/gitnexus/test/integration/shared-store-seed.test.ts +++ b/gitnexus/test/integration/shared-store-seed.test.ts @@ -41,9 +41,9 @@ const queryNames = async (graph: string): Promise => { const db = new lbug.Database(graph, 0, true, true); const conn = new lbug.Connection(db); try { - const rows = (await ( - await conn.query('MATCH (f:Function) RETURN f.name AS n ORDER BY n') - ).getAll()) as { n: string }[]; + const result = await conn.query('MATCH (f:Function) RETURN f.name AS n ORDER BY n'); + if (Array.isArray(result)) throw new Error('Expected a single query result'); + const rows = (await result.getAll()) as { n: string }[]; return rows.map((r) => r.n); } finally { await conn.close(); diff --git a/gitnexus/test/integration/skip-fts.test.ts b/gitnexus/test/integration/skip-fts.test.ts index 896287ee9..4192316bb 100644 --- a/gitnexus/test/integration/skip-fts.test.ts +++ b/gitnexus/test/integration/skip-fts.test.ts @@ -367,8 +367,14 @@ describe('FTS opt-out analysis lifecycle (#3091)', () => { }); expect(response.status).toBe(200); const body = await response.json(); - expect(body.warning).toContain('FTS disabled for this index'); - expect(body.warning).not.toMatch(/failed to load|indexes missing|repair-fts/); + expect(body).toHaveProperty( + 'warning', + expect.stringContaining('FTS disabled for this index'), + ); + expect(body).toHaveProperty( + 'warning', + expect.not.stringMatching(/failed to load|indexes missing|repair-fts/), + ); } } finally { if (server.exitCode === null) server.kill('SIGTERM'); diff --git a/gitnexus/test/integration/spring-bean-pipeline.test.ts b/gitnexus/test/integration/spring-bean-pipeline.test.ts index c01cd9915..d010663bb 100644 --- a/gitnexus/test/integration/spring-bean-pipeline.test.ts +++ b/gitnexus/test/integration/spring-bean-pipeline.test.ts @@ -1,7 +1,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'spring-bean-app'); diff --git a/gitnexus/test/integration/spring-config-pipeline.test.ts b/gitnexus/test/integration/spring-config-pipeline.test.ts index 65adf802a..111f5f0e0 100644 --- a/gitnexus/test/integration/spring-config-pipeline.test.ts +++ b/gitnexus/test/integration/spring-config-pipeline.test.ts @@ -4,7 +4,7 @@ import { beforeAll, describe, expect, it, vi } from 'vitest'; import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; import { SPRING_CONFIG_DESCRIPTION } from '../../src/core/ingestion/frameworks/spring/config-bindings.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; import { createTempDir } from '../helpers/test-db.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'spring-config-app'); diff --git a/gitnexus/test/integration/spring-interface-inheritance-pipeline.test.ts b/gitnexus/test/integration/spring-interface-inheritance-pipeline.test.ts index d759e8348..dd0368ebf 100644 --- a/gitnexus/test/integration/spring-interface-inheritance-pipeline.test.ts +++ b/gitnexus/test/integration/spring-interface-inheritance-pipeline.test.ts @@ -10,7 +10,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const ORDER_API = `package com.example; import org.springframework.web.bind.annotation.*; diff --git a/gitnexus/test/integration/spring-route-pipeline.test.ts b/gitnexus/test/integration/spring-route-pipeline.test.ts index e6fd739af..1683cd027 100644 --- a/gitnexus/test/integration/spring-route-pipeline.test.ts +++ b/gitnexus/test/integration/spring-route-pipeline.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'node:path'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; -import type { PipelineResult } from '../../types/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const FIXTURE = path.resolve(__dirname, '..', 'fixtures', 'spring-route-app'); diff --git a/gitnexus/test/integration/taint-explain.test.ts b/gitnexus/test/integration/taint-explain.test.ts index 28478051e..09867bb6d 100644 --- a/gitnexus/test/integration/taint-explain.test.ts +++ b/gitnexus/test/integration/taint-explain.test.ts @@ -76,7 +76,7 @@ export function handle(req: any, db: any, map: Map, task: any, r .map((rel) => { const sink = result.graph.getNode(rel.targetId); const decoded = decodeTaintPath(rel.reason); - if (!decoded.ok) { + if (decoded.ok === false) { throw new Error(`invalid TAINTED reason for ${rel.id}: ${decoded.error}`); } return { diff --git a/gitnexus/test/integration/watch-filesystem.test.ts b/gitnexus/test/integration/watch-filesystem.test.ts index f1e9aae8b..646d95e2a 100644 --- a/gitnexus/test/integration/watch-filesystem.test.ts +++ b/gitnexus/test/integration/watch-filesystem.test.ts @@ -81,7 +81,9 @@ describe('watch filesystem integration', () => { const loop = await startWatchFileLoop( repo, 30, - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, (error) => { throw error; }, @@ -166,7 +168,9 @@ describe('watch filesystem integration', () => { const loop = await startWatchFileLoop( repo, 25, - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, (error) => { throw error; }, @@ -224,7 +228,9 @@ describe('watch filesystem integration', () => { const loop = await startWatchFileLoop( repo, 25, - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, (_error, paths) => errors.push([...paths]), ); loops.push(loop); @@ -252,7 +258,9 @@ describe('watch filesystem integration', () => { const loop = await startWatchFileLoop( repo, 25, - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, (error) => { throw error; }, diff --git a/gitnexus/test/integration/worker-pool.test.ts b/gitnexus/test/integration/worker-pool.test.ts index ac6fe0076..b995873a5 100644 --- a/gitnexus/test/integration/worker-pool.test.ts +++ b/gitnexus/test/integration/worker-pool.test.ts @@ -1367,7 +1367,7 @@ describe('worker pool integration', () => { ]); // Deterministically: a.ts crashes round 1, b.ts crashes round 2. - const quarantine = (pool.getQuarantinedPaths?.() ?? []).sort(); + const quarantine = [...(pool.getQuarantinedPaths?.() ?? [])].sort(); expect(quarantine).toEqual(['a.ts', 'b.ts']); // All non-quarantined files eventually parsed by the survivor slot. const allPaths = results.flatMap((r) => r.paths).sort(); diff --git a/gitnexus/test/unit/ai-context.test.ts b/gitnexus/test/unit/ai-context.test.ts index a7174e674..c411d7ec5 100644 --- a/gitnexus/test/unit/ai-context.test.ts +++ b/gitnexus/test/unit/ai-context.test.ts @@ -1667,7 +1667,7 @@ describe('count-only reindex does not churn the committed block (#2907)', () => describe('--no-stats drops the per-cluster symbol counts too (#2907)', () => { const stats = { nodes: 10, edges: 20, processes: 3 }; - const skills = [{ label: 'ingestion', name: 'p-ingestion', symbolCount: 120 }]; + const skills = [{ label: 'ingestion', name: 'p-ingestion', symbolCount: 120, fileCount: 10 }]; it('omits the count under --no-stats and keeps it otherwise', () => { const lean = generateGitNexusContent('P', stats, { generatedSkills: skills, noStats: true }); diff --git a/gitnexus/test/unit/analyze-delete-api.test.ts b/gitnexus/test/unit/analyze-delete-api.test.ts index e40ff53cc..74b01e733 100644 --- a/gitnexus/test/unit/analyze-delete-api.test.ts +++ b/gitnexus/test/unit/analyze-delete-api.test.ts @@ -152,7 +152,11 @@ const invokeDelete = (route: string, jobId: string): { statusCode: number; body: return this; }, }; - handler({ params: { jobId } }, res); + handler( + { params: { jobId } } as unknown as express.Request, + res as unknown as express.Response, + vi.fn(), + ); return res; }; diff --git a/gitnexus/test/unit/analyze-gitnexusrc.test.ts b/gitnexus/test/unit/analyze-gitnexusrc.test.ts index 081b3664c..e7fe6224c 100644 --- a/gitnexus/test/unit/analyze-gitnexusrc.test.ts +++ b/gitnexus/test/unit/analyze-gitnexusrc.test.ts @@ -22,8 +22,12 @@ const { getDefaultBranchMock, } = vi.hoisted(() => ({ runFullAnalysisMock: vi.fn(), - generateAIContextFilesMock: vi.fn(async () => ({ files: [] as string[] })), - refreshBaseRefLineMock: vi.fn(async () => ({ files: [] as string[] })), + generateAIContextFilesMock: vi.fn< + typeof import('../../src/cli/ai-context.js').generateAIContextFiles + >(async () => ({ files: [] as string[] })), + refreshBaseRefLineMock: vi.fn( + async () => ({ files: [] as string[] }), + ), generateSkillFilesMock: vi.fn(async () => ({ skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }], outputPath: '/repo/.claude/skills', diff --git a/gitnexus/test/unit/analyze-http-endpoint-error.test.ts b/gitnexus/test/unit/analyze-http-endpoint-error.test.ts index ffb7d2b91..718a9fa16 100644 --- a/gitnexus/test/unit/analyze-http-endpoint-error.test.ts +++ b/gitnexus/test/unit/analyze-http-endpoint-error.test.ts @@ -30,12 +30,15 @@ const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>(() => source: 'package', })); const isPrefixRuntimeLoadableMock = vi.fn(() => true); -const installEmbeddingRuntimeMock = vi.fn(async () => undefined); +const installEmbeddingRuntimeMock = vi.fn< + typeof import('../../src/core/embeddings/runtime-install.js').installEmbeddingRuntime +>(async () => undefined); vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({ ...(await importOriginal()), resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(), isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(), - installEmbeddingRuntime: (...args: unknown[]) => installEmbeddingRuntimeMock(...args), + installEmbeddingRuntime: (...args: Parameters) => + installEmbeddingRuntimeMock(...args), getEmbeddingRuntimeDir: () => '/fake/embedding-runtime', })); diff --git a/gitnexus/test/unit/analyze-local-embedding-error.test.ts b/gitnexus/test/unit/analyze-local-embedding-error.test.ts index e67f43fe5..161f3b77b 100644 --- a/gitnexus/test/unit/analyze-local-embedding-error.test.ts +++ b/gitnexus/test/unit/analyze-local-embedding-error.test.ts @@ -38,12 +38,15 @@ const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>(() => source: 'package', })); const isPrefixRuntimeLoadableMock = vi.fn(() => true); -const installEmbeddingRuntimeMock = vi.fn(async () => undefined); +const installEmbeddingRuntimeMock = vi.fn< + typeof import('../../src/core/embeddings/runtime-install.js').installEmbeddingRuntime +>(async () => undefined); vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({ ...(await importOriginal()), resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(), isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(), - installEmbeddingRuntime: (...args: unknown[]) => installEmbeddingRuntimeMock(...args), + installEmbeddingRuntime: (...args: Parameters) => + installEmbeddingRuntimeMock(...args), getEmbeddingRuntimeDir: () => '/fake/embedding-runtime', })); diff --git a/gitnexus/test/unit/analyze-no-stats-bridge.test.ts b/gitnexus/test/unit/analyze-no-stats-bridge.test.ts index a63a6fa5a..63a417b2e 100644 --- a/gitnexus/test/unit/analyze-no-stats-bridge.test.ts +++ b/gitnexus/test/unit/analyze-no-stats-bridge.test.ts @@ -3,7 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const { runFullAnalysisMock, generateAIContextFilesMock, generateSkillFilesMock, cliErrorMock } = vi.hoisted(() => { const runFullAnalysisMock = vi.fn(); - const generateAIContextFilesMock = vi.fn(async () => ({ files: [] as string[] })); + const generateAIContextFilesMock = vi.fn< + typeof import('../../src/cli/ai-context.js').generateAIContextFiles + >(async () => ({ files: [] as string[] })); const generateSkillFilesMock = vi.fn(async () => ({ skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }], outputPath: '/repo/.claude/skills', diff --git a/gitnexus/test/unit/analyze-worker-core.test.ts b/gitnexus/test/unit/analyze-worker-core.test.ts index 2b09108b5..2c0323220 100644 --- a/gitnexus/test/unit/analyze-worker-core.test.ts +++ b/gitnexus/test/unit/analyze-worker-core.test.ts @@ -23,6 +23,7 @@ import type { AnalyzerRunnerIdentity } from '../../src/storage/repo-manager.js'; import { IndexLockTimeoutError, type LockRecord } from '../../src/storage/index-lock.js'; const baseResult: AnalyzeResult = { + rebuildReasons: [], repoName: 'repo', repoPath: '/repo', storagePath: '/repo/.gitnexus', diff --git a/gitnexus/test/unit/analyze-worker-ipc.test.ts b/gitnexus/test/unit/analyze-worker-ipc.test.ts index d6dde3fcc..e05d8c180 100644 --- a/gitnexus/test/unit/analyze-worker-ipc.test.ts +++ b/gitnexus/test/unit/analyze-worker-ipc.test.ts @@ -29,6 +29,7 @@ function hostileResult(): AnalyzeResult { }; graph.self = graph; // circular — JSON.stringify throws return { + rebuildReasons: [], repoName: 'demo', repoPath: '/repos/demo', storagePath: '/repos/demo/.gitnexus', @@ -83,8 +84,16 @@ describe('#2112: analyze-worker IPC projection', () => { properties: { name: `n${i}`, filePath: 'x.ts' }, }); } - graph.addRelationship({ id: 'n0->n1', source: 'n0', target: 'n1', type: 'CALLS' }); + graph.addRelationship({ + id: 'n0->n1', + sourceId: 'n0', + targetId: 'n1', + type: 'CALLS', + confidence: 1, + reason: '', + }); const result: AnalyzeResult = { + rebuildReasons: [], repoName: 'demo', repoPath: '/r', storagePath: '/r/.gitnexus', diff --git a/gitnexus/test/unit/api-analyze-upload.test.ts b/gitnexus/test/unit/api-analyze-upload.test.ts index 8843231ff..bc8d7370e 100644 --- a/gitnexus/test/unit/api-analyze-upload.test.ts +++ b/gitnexus/test/unit/api-analyze-upload.test.ts @@ -1,3 +1,4 @@ +import type { AnalyzeUploadDeps } from '../../src/server/analyze-upload.js'; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import path from 'node:path'; import fs from 'node:fs/promises'; @@ -88,8 +89,13 @@ function uniqueTop(): string { describe('createAnalyzeUploadHandler', () => { it('ingests, promotes the inner folder, and launches analysis (202)', async () => { const top = uniqueTop(); - const createJob = vi.fn(() => ({ id: 'job-1', status: 'queued' })); - const launch = vi.fn((_j, dir: string) => promoted.push(dir)); + const createJob = vi.fn(() => ({ + id: 'job-1', + status: 'queued', + })); + const launch = vi.fn((_j, dir) => { + promoted.push(dir); + }); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -118,10 +124,12 @@ describe('createAnalyzeUploadHandler', () => { it('maps a busy job (createJob throws "already in progress") to 409 and promotes nothing', async () => { const top = uniqueTop(); - const createJob = vi.fn(() => { + const createJob = vi.fn(() => { throw new Error('Analysis already in progress for another repository'); }); - const launch = vi.fn((_j, dir: string) => promoted.push(dir)); + const launch = vi.fn((_j, dir) => { + promoted.push(dir); + }); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -142,8 +150,8 @@ describe('createAnalyzeUploadHandler', () => { }); it('rejects a traversal path in the manifest (400) without launching', async () => { - const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); - const launch = vi.fn(); + const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); + const launch = vi.fn(); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -162,8 +170,8 @@ describe('createAnalyzeUploadHandler', () => { }); it('rejects an un-nameable top folder (Windows-reserved → 400)', async () => { - const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); - const launch = vi.fn(); + const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); + const launch = vi.fn(); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -182,8 +190,13 @@ describe('createAnalyzeUploadHandler', () => { it('strips a crafted .gitnexus index from the promoted upload', async () => { const top = uniqueTop(); - const createJob = vi.fn(() => ({ id: 'job-x', status: 'queued' })); - const launch = vi.fn((_j, dir: string) => promoted.push(dir)); + const createJob = vi.fn(() => ({ + id: 'job-x', + status: 'queued', + })); + const launch = vi.fn((_j, dir) => { + promoted.push(dir); + }); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -204,8 +217,8 @@ describe('createAnalyzeUploadHandler', () => { }); it('rejects a single-segment manifest before creating a job (no slot taken)', async () => { - const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); - const launch = vi.fn(); + const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); + const launch = vi.fn(); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -224,8 +237,8 @@ describe('createAnalyzeUploadHandler', () => { }); it('rejects a multi-top-folder manifest (would silently drop folders)', async () => { - const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); - const launch = vi.fn(); + const createJob = vi.fn(() => ({ id: 'j', status: 'queued' })); + const launch = vi.fn(); const failJob = vi.fn(); const handler = createAnalyzeUploadHandler({ createJob, launch, failJob }); @@ -245,9 +258,12 @@ describe('createAnalyzeUploadHandler', () => { it('releases the single slot (failJob) when a step fails after createJob', async () => { const top = uniqueTop(); - const createJob = vi.fn(() => ({ id: 'job-fail', status: 'queued' })); + const createJob = vi.fn(() => ({ + id: 'job-fail', + status: 'queued', + })); // launch throws AFTER createJob + promote — the slot must be released. - const launch = vi.fn((_j, dir: string) => { + const launch = vi.fn((_j, dir: string) => { promoted.push(dir); throw new Error('worker fork blew up'); }); diff --git a/gitnexus/test/unit/api-fts-mode.test.ts b/gitnexus/test/unit/api-fts-mode.test.ts index ab8eaf401..a78a2525a 100644 --- a/gitnexus/test/unit/api-fts-mode.test.ts +++ b/gitnexus/test/unit/api-fts-mode.test.ts @@ -328,7 +328,7 @@ async function invoke(route: string, query: Record = {}) { this.emit('finish'); }, }); - await handler(req, res); + await handler(req as unknown as express.Request, res as unknown as express.Response, vi.fn()); expect(res.statusCode, JSON.stringify(res.body)).toBe(route === '/api/embed' ? 202 : 200); return res; } diff --git a/gitnexus/test/unit/api-graph-streaming.test.ts b/gitnexus/test/unit/api-graph-streaming.test.ts index f79a1ea5a..4aa6ccdb8 100644 --- a/gitnexus/test/unit/api-graph-streaming.test.ts +++ b/gitnexus/test/unit/api-graph-streaming.test.ts @@ -10,7 +10,7 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/auto-sync-runner.test.ts b/gitnexus/test/unit/auto-sync-runner.test.ts index a16f0fdac..e7c8761e3 100644 --- a/gitnexus/test/unit/auto-sync-runner.test.ts +++ b/gitnexus/test/unit/auto-sync-runner.test.ts @@ -98,7 +98,7 @@ async function runWithOnDiskPdgState(options: { getCurrentCommit: vi.fn(async () => 'commit-1'), runAnalysis, registerRepo: vi.fn(async () => 'repo'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -145,11 +145,11 @@ describe('auto-sync runner', () => { it('runs clone, analyzes changed commits, registers the repo, and syncs changed groups', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -229,11 +229,11 @@ describe('auto-sync runner', () => { await fs.writeFile(path.join(targetDir, '.gitnexusrc'), '{"embeddings": true}'); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => targetDir), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -268,12 +268,12 @@ describe('auto-sync runner', () => { const stateKey = '/tmp/repos/gitee.com/qts_server/qts_account|master'; const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), getExistingPdgMode: vi.fn(async () => false), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ [stateKey]: { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -318,12 +318,12 @@ describe('auto-sync runner', () => { }; const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), getExistingPdgMode: vi.fn(async () => true), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -411,12 +411,12 @@ describe('auto-sync runner', () => { const warn = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), getExistingPdgMode: vi.fn(async () => true), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -452,7 +452,7 @@ describe('auto-sync runner', () => { runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), resolveBranchPlacement: vi.fn(async () => ({ branch: 'master' })), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => true), @@ -480,11 +480,11 @@ describe('auto-sync runner', () => { it('syncs a group when a repo is newly added to the group', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => true), @@ -508,11 +508,11 @@ describe('auto-sync runner', () => { it('syncs a group after successful re-analysis even when membership already exists', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-3'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-3'), runAnalysis: vi.fn(async () => ({ stats: { files: 2 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-2', analyzedCommitId: 'commit-2', @@ -547,12 +547,14 @@ describe('auto-sync runner', () => { projects: [ { localPath: '/tmp/repos-a', + overwriteLocalChanges: false, groupName: 'back_end', branches: ['main'], remoteUrls: ['git@github.com:team-a/service.git'], }, { localPath: '/tmp/repos-b', + overwriteLocalChanges: false, groupName: 'back_end', branches: ['main'], remoteUrls: ['git@gitlab.com:team-b/service.git'], @@ -565,11 +567,11 @@ describe('auto-sync runner', () => { root: localPath, })), cloneOrPull: vi.fn(async (_url, targetDir) => targetDir), - getCurrentBranch: vi.fn(() => 'main'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'main'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'service'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => true), @@ -618,11 +620,11 @@ describe('auto-sync runner', () => { it('skips analysis when commit id has not changed', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), runAnalysis: vi.fn(), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -664,11 +666,11 @@ describe('auto-sync runner', () => { .mockResolvedValueOnce(undefined); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), runAnalysis: vi.fn(), registerRepo: vi.fn(), - loadState: vi.fn(async () => structuredClone(persistedState)), + loadState: vi.fn(async () => structuredClone(persistedState)), saveState: vi.fn(async (state) => { persistedState = structuredClone(state); }), @@ -705,11 +707,11 @@ describe('auto-sync runner', () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -763,11 +765,11 @@ describe('auto-sync runner', () => { const cloneOrPull = vi.fn(async () => '/tmp/repos/gitlab.mycompany.com/group/repo'); const deps: Partial = withCloneRoot({ cloneOrPull, - getCurrentBranch: vi.fn(() => 'main'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'main'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'repo'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -826,11 +828,11 @@ describe('auto-sync runner', () => { const runAnalysis = vi.fn(async () => ({ stats: { files: 1 } }) as any); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis, registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -873,11 +875,11 @@ describe('auto-sync runner', () => { if (options?.branch === 'missing') throw new Error('remote branch not found'); return '/tmp/repos/gitee.com/qts_server/qts_account'; }), - getCurrentBranch: vi.fn(() => 'develop'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'develop'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -940,7 +942,7 @@ describe('auto-sync runner', () => { getCurrentCommit: vi.fn(), runAnalysis: vi.fn(), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -980,11 +982,11 @@ describe('auto-sync runner', () => { const warnLogger = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'develop'), + getCurrentBranch: vi.fn(async () => 'develop'), getCurrentCommit: vi.fn(), runAnalysis: vi.fn(), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => true), @@ -1010,11 +1012,11 @@ describe('auto-sync runner', () => { const warnLogger = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => undefined), + getCurrentBranch: vi.fn(async () => undefined), getCurrentCommit: vi.fn(), runAnalysis: vi.fn(), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => true), @@ -1055,13 +1057,13 @@ describe('auto-sync runner', () => { if (remoteUrl.includes('failing_sync')) throw new Error('sync failed'); return '/tmp/repos/gitee.com/qts_server/qts_account'; }), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => { throw new Error('analysis failed'); }), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -1113,11 +1115,11 @@ describe('auto-sync runner', () => { it('records the resolved target directory when a post-sync operation fails', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async (_url, targetDir) => targetDir), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => { + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => { throw new Error('git log failed'); }), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), getAvailableMemoryGB: vi.fn(() => 8), @@ -1153,11 +1155,11 @@ describe('auto-sync runner', () => { return cloneRoot; }), cloneOrPull: vi.fn(async (_url, targetDir) => targetDir), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'repo'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1180,13 +1182,13 @@ describe('auto-sync runner', () => { const errorLogger = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async (_url, targetDir) => targetDir), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => { throw new Error('registry busy'); }), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1220,11 +1222,11 @@ describe('auto-sync runner', () => { const errorLogger = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1279,13 +1281,13 @@ describe('auto-sync runner', () => { events.push(`clone-end:${remoteUrl}`); return remoteUrl.includes('/one.git') ? '/tmp/repos/one' : '/tmp/repos/two'; }), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn((repoPath) => + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async (repoPath: string) => repoPath.endsWith('/one') ? 'one-commit' : 'two-commit', ), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'repo'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1323,11 +1325,11 @@ describe('auto-sync runner', () => { }; const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async (_url, targetDir) => targetDir), - getCurrentBranch: vi.fn(() => 'main'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'main'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async (_path, _meta, options) => options?.name ?? 'repo'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1367,14 +1369,14 @@ describe('auto-sync runner', () => { }; const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/github.com/owner/repo'), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), syncGroupByName: vi.fn(async () => {}), getAvailableMemoryGB: vi.fn(() => 8), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'abc'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'abc'), runAnalysis: vi.fn(async () => ({ stats: {} })), registerRepo: vi.fn(async () => 'repo'), }); @@ -1399,13 +1401,13 @@ describe('auto-sync runner', () => { const errorLogger = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => { throw new Error('parser crashed\nwith stack'); }), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -1460,13 +1462,13 @@ describe('auto-sync runner', () => { ); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), runAnalysis: vi.fn(async () => { throw leftover; }), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -1513,8 +1515,8 @@ describe('auto-sync runner', () => { it('does not count a retryable live-holder index-lock timeout toward the analyze failure threshold', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), runAnalysis: vi.fn(async () => { throw new AutoSyncAnalysisError('waited for the index lock', { code: 'index-lock-timeout', @@ -1522,7 +1524,7 @@ describe('auto-sync runner', () => { }); }), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1550,8 +1552,8 @@ describe('auto-sync runner', () => { it('still counts a non-retryable index-lock guard timeout toward the analyze failure threshold', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-1'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-1'), runAnalysis: vi.fn(async () => { throw new AutoSyncAnalysisError('index lock guard timeout', { code: 'index-lock-timeout', @@ -1559,7 +1561,7 @@ describe('auto-sync runner', () => { }); }), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1586,13 +1588,13 @@ describe('auto-sync runner', () => { it('records a null analysis failure without masking it with a TypeError', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => { throw null; }), registerRepo: vi.fn(), - loadState: vi.fn(async () => ({})), + loadState: vi.fn(async () => ({})), saveState: vi.fn(async () => {}), writeCommitInfo: vi.fn(async () => {}), addRepoToGroup: vi.fn(async () => false), @@ -1621,11 +1623,11 @@ describe('auto-sync runner', () => { const errorLogger = vi.fn(); const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', @@ -1673,11 +1675,11 @@ describe('auto-sync runner', () => { it('clears prior analyze failure count after a successful analyze', async () => { const deps: Partial = withCloneRoot({ cloneOrPull: vi.fn(async () => '/tmp/repos/gitee.com/qts_server/qts_account'), - getCurrentBranch: vi.fn(() => 'master'), - getCurrentCommit: vi.fn(() => 'commit-2'), + getCurrentBranch: vi.fn(async () => 'master'), + getCurrentCommit: vi.fn(async () => 'commit-2'), runAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any), registerRepo: vi.fn(async () => 'qts_account'), - loadState: vi.fn(async () => ({ + loadState: vi.fn(async () => ({ '/tmp/repos/gitee.com/qts_server/qts_account|master': { codeCommitId: 'commit-1', analyzedCommitId: 'commit-1', diff --git a/gitnexus/test/unit/auto-sync.test.ts b/gitnexus/test/unit/auto-sync.test.ts index ac974817a..ebf3ba866 100644 --- a/gitnexus/test/unit/auto-sync.test.ts +++ b/gitnexus/test/unit/auto-sync.test.ts @@ -287,7 +287,7 @@ describe('auto-sync', () => { const loaded = await loadAutoSyncConfig(); expect(loaded.ok).toBe(false); - if (loaded.ok) throw new Error('expected invalid config'); + if (loaded.ok !== false) throw new Error('expected invalid config'); expect(loaded.message).toContain('analyze_failure_threshold must be an integer >= 2'); }); @@ -307,7 +307,7 @@ describe('auto-sync', () => { const loaded = await loadAutoSyncConfig(); expect(loaded.ok).toBe(false); - if (loaded.ok) throw new Error('expected invalid config'); + if (loaded.ok !== false) throw new Error('expected invalid config'); expect(loaded.reason).toBe('invalid'); expect(loaded.message).toContain('[auto-sync] Invalid watch_config.yml:'); expect(loaded.message).toContain('sync_interval_minutes must be a positive integer'); @@ -330,7 +330,7 @@ describe('auto-sync', () => { const loaded = await loadAutoSyncConfig(); expect(loaded.ok).toBe(false); - if (loaded.ok) throw new Error('expected invalid config'); + if (loaded.ok !== false) throw new Error('expected invalid config'); expect(loaded.message).toContain('local_path must be an absolute path'); }); @@ -740,7 +740,7 @@ describe('auto-sync', () => { const loaded = await loadAutoSyncConfig(); expect(loaded.ok).toBe(false); - if (loaded.ok) throw new Error('expected invalid config'); + if (loaded.ok !== false) throw new Error('expected invalid config'); expect(loaded.message).toContain('must not set both branch and branches'); }); diff --git a/gitnexus/test/unit/call-routing/ruby.test.ts b/gitnexus/test/unit/call-routing/ruby.test.ts index 69c6405c3..a5ea5a6f7 100644 --- a/gitnexus/test/unit/call-routing/ruby.test.ts +++ b/gitnexus/test/unit/call-routing/ruby.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import type { SyntaxNode } from 'tree-sitter'; import { routeRubyCall } from '../../../src/core/ingestion/call-routing.js'; // ── Mock AST node helpers ──────────────────────────────────────────────────── @@ -164,67 +165,73 @@ function makeNamedSibling(type = 'expression_statement'): MockNode { return { type, text: '', isNamed: true }; } +// These routing fixtures intentionally provide only the traversed AST fields. +// Keep the native SyntaxNode assertion at the test boundary. +function syntaxNode(node: MockNode): SyntaxNode { + return node as unknown as SyntaxNode; +} + // ── require / require_relative ─────────────────────────────────────────────── describe('routeRubyCall — require / require_relative', () => { it('require with a valid string path returns import with isRelative=false', () => { const node = makeRequireCallNode('net/http'); - const result = routeRubyCall('require', node); + const result = routeRubyCall('require', syntaxNode(node)); expect(result).toEqual({ kind: 'import', importPath: 'net/http', isRelative: false }); }); it('require_relative without leading dot prepends "./"', () => { const node = makeRequireCallNode('models/user'); - const result = routeRubyCall('require_relative', node); + const result = routeRubyCall('require_relative', syntaxNode(node)); expect(result).toEqual({ kind: 'import', importPath: './models/user', isRelative: true }); }); it('require_relative with path already starting with "." does not double-prepend', () => { const node = makeRequireCallNode('./helpers/formatter'); - const result = routeRubyCall('require_relative', node); + const result = routeRubyCall('require_relative', syntaxNode(node)); expect(result).toEqual({ kind: 'import', importPath: './helpers/formatter', isRelative: true }); }); it('require_relative with "../" prefix is left unchanged', () => { const node = makeRequireCallNode('../shared/utils'); - const result = routeRubyCall('require_relative', node); + const result = routeRubyCall('require_relative', syntaxNode(node)); expect(result).toEqual({ kind: 'import', importPath: '../shared/utils', isRelative: true }); }); it('returns skip when there is no string_content node (non-literal argument)', () => { const node = makeRequireCallNodeNoContent(); - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('returns skip when import path is an empty string', () => { const node = makeRequireCallNode(''); - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('returns skip when import path contains a control character (\\x00)', () => { const node = makeRequireCallNode('some\x00path'); - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('returns skip when import path contains a newline control character (\\n)', () => { const node = makeRequireCallNode('path\ninjection'); - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('returns skip when import path exceeds 1024 characters', () => { const longPath = 'a'.repeat(1025); const node = makeRequireCallNode(longPath); - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('accepts import path of exactly 1024 characters', () => { const maxPath = 'a'.repeat(1024); const node = makeRequireCallNode(maxPath); - const result = routeRubyCall('require', node); + const result = routeRubyCall('require', syntaxNode(node)); expect(result).toEqual({ kind: 'import', importPath: maxPath, isRelative: false }); }); @@ -240,13 +247,13 @@ describe('routeRubyCall — require / require_relative', () => { text: '', childForFieldName: (name: string) => (name === 'arguments' ? argList : undefined), }; - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('returns skip when childForFieldName is absent (undefined callNode fields)', () => { // callNode has no childForFieldName method at all const node: MockNode = { type: 'call', text: '' }; - expect(routeRubyCall('require', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('require', syntaxNode(node))).toEqual({ kind: 'skip' }); }); }); @@ -259,7 +266,7 @@ describe('routeRubyCall — require / require_relative', () => { describe('routeRubyCall — include / extend / prepend (heritage owned by scope-resolution)', () => { it('include returns skip (heritage emitted by scope-resolution, not a call edge)', () => { const node = makeHeritageCallNode([makeConstantArg('Serializable')], 'class', 'User'); - expect(routeRubyCall('include', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('include', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('extend returns skip (heritage emitted by scope-resolution, not a call edge)', () => { @@ -268,12 +275,12 @@ describe('routeRubyCall — include / extend / prepend (heritage owned by scope- 'class', 'Post', ); - expect(routeRubyCall('extend', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('extend', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('prepend returns skip (heritage emitted by scope-resolution, not a call edge)', () => { const node = makeHeritageCallNode([makeConstantArg('Instrumented')], 'class', 'Service'); - expect(routeRubyCall('prepend', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('prepend', syntaxNode(node))).toEqual({ kind: 'skip' }); }); }); @@ -282,7 +289,7 @@ describe('routeRubyCall — include / extend / prepend (heritage owned by scope- describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('attr_accessor with a single symbol returns a property item', () => { const node = makeAccessorCallNode([makeSimpleSymbol('name', 5)]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toEqual({ kind: 'properties', @@ -292,7 +299,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('attr_reader sets accessorType to "attr_reader"', () => { const node = makeAccessorCallNode([makeSimpleSymbol('age', 3)]); - const result = routeRubyCall('attr_reader', node); + const result = routeRubyCall('attr_reader', syntaxNode(node)); expect(result).toEqual({ kind: 'properties', @@ -302,7 +309,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('attr_writer sets accessorType to "attr_writer"', () => { const node = makeAccessorCallNode([makeSimpleSymbol('email', 7)]); - const result = routeRubyCall('attr_writer', node); + const result = routeRubyCall('attr_writer', syntaxNode(node)); expect(result).toEqual({ kind: 'properties', @@ -319,7 +326,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { endPosition: { row: 2 }, }; const node = makeAccessorCallNode([symNode]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', items: [{ propName: 'title' }] }); }); @@ -333,7 +340,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { endPosition: { row: 1 }, }; const node = makeAccessorCallNode([symNode]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', items: [{ propName: 'status' }] }); }); @@ -345,7 +352,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { makeSimpleSymbol('dob', 10), ]; const node = makeAccessorCallNode(args); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toEqual({ kind: 'properties', @@ -360,7 +367,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('extracts simple YARD @return [Type] from preceding comment', () => { const comment = makeCommentNode('# @return [Address]'); const node = makeAccessorCallNode([makeSimpleSymbol('address', 20)], [comment]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', @@ -372,7 +379,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { // The regex captures "Array"; the simple match grabs the first uppercase word "Array" const comment = makeCommentNode('# @return [Array]'); const node = makeAccessorCallNode([makeSimpleSymbol('users', 15)], [comment]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', @@ -383,7 +390,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('extracts type from YARD comment with extra whitespace inside brackets', () => { const comment = makeCommentNode('# @return [ Integer ]'); const node = makeAccessorCallNode([makeSimpleSymbol('count', 8)], [comment]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', @@ -393,7 +400,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('does not set declaredType when no YARD comment precedes the call', () => { const node = makeAccessorCallNode([makeSimpleSymbol('score', 12)]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', items: [{ propName: 'score' }] }); const item = (result as any).items[0]; @@ -403,7 +410,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { it('does not set declaredType when comment has no @return annotation', () => { const comment = makeCommentNode('# This accessor stores the user name'); const node = makeAccessorCallNode([makeSimpleSymbol('user_name', 9)], [comment]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); const item = (result as any).items[0]; expect(item.declaredType).toBeUndefined(); @@ -413,7 +420,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { // e.g. "@return [string]" — lowercase first char fails the simple = raw.match(/^([A-Z]\w*)/) const comment = makeCommentNode('# @return [string]'); const node = makeAccessorCallNode([makeSimpleSymbol('label', 4)], [comment]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); const item = (result as any).items[0]; expect(item.declaredType).toBeUndefined(); @@ -426,7 +433,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { const named = makeNamedSibling(); const node = makeAccessorCallNode([makeSimpleSymbol('owner', 6)], [yardComment, named]); // named is last in the array → becomes direct previousSibling - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); const item = (result as any).items[0]; expect(item.declaredType).toBeUndefined(); @@ -438,7 +445,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { const comment = makeCommentNode('# @return [Order]'); // siblings in order oldest→newest; the last becomes the direct previousSibling const node = makeAccessorCallNode([makeSimpleSymbol('order', 30)], [comment, unnamedNode]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', @@ -450,12 +457,12 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { // Only an identifier node — not a symbol const identArg: MockNode = { type: 'identifier', text: 'some_var' }; const node = makeAccessorCallNode([identArg]); - expect(routeRubyCall('attr_accessor', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('attr_accessor', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('returns skip when arg list is empty', () => { const node = makeAccessorCallNode([]); - expect(routeRubyCall('attr_accessor', node)).toEqual({ kind: 'skip' }); + expect(routeRubyCall('attr_accessor', syntaxNode(node))).toEqual({ kind: 'skip' }); }); it('records correct startLine and endLine from symbol node positions', () => { @@ -466,7 +473,7 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { endPosition: { row: 42 }, }; const node = makeAccessorCallNode([sym]); - const result = routeRubyCall('attr_accessor', node); + const result = routeRubyCall('attr_accessor', syntaxNode(node)); expect(result).toMatchObject({ kind: 'properties', @@ -480,19 +487,19 @@ describe('routeRubyCall — attr_accessor / attr_reader / attr_writer', () => { describe('routeRubyCall — default (unknown method name)', () => { it('returns {kind: "call"} for an arbitrary method name', () => { const node: MockNode = { type: 'call', text: '' }; - expect(routeRubyCall('some_method', node)).toEqual({ kind: 'call' }); + expect(routeRubyCall('some_method', syntaxNode(node))).toEqual({ kind: 'call' }); }); it('returns {kind: "call"} for an empty method name string', () => { const node: MockNode = { type: 'call', text: '' }; - expect(routeRubyCall('', node)).toEqual({ kind: 'call' }); + expect(routeRubyCall('', syntaxNode(node))).toEqual({ kind: 'call' }); }); it('returns {kind: "call"} for a realistic method name (save, render, etc.)', () => { const node: MockNode = { type: 'call', text: '' }; - expect(routeRubyCall('render', node)).toEqual({ kind: 'call' }); - expect(routeRubyCall('save', node)).toEqual({ kind: 'call' }); - expect(routeRubyCall('destroy', node)).toEqual({ kind: 'call' }); + expect(routeRubyCall('render', syntaxNode(node))).toEqual({ kind: 'call' }); + expect(routeRubyCall('save', syntaxNode(node))).toEqual({ kind: 'call' }); + expect(routeRubyCall('destroy', syntaxNode(node))).toEqual({ kind: 'call' }); }); }); @@ -501,13 +508,13 @@ describe('routeRubyCall — default (unknown method name)', () => { describe('routeRubyCall passthrough', () => { it('routeRubyCall delegates correctly for require', () => { const node = makeRequireCallNode('json'); - const result = routeRubyCall('require', node); + const result = routeRubyCall('require', syntaxNode(node)); expect(result).toEqual({ kind: 'import', importPath: 'json', isRelative: false }); }); it('routeRubyCall returns {kind: "call"} for an unknown method name', () => { const node: MockNode = { type: 'call', text: '' }; - const result = routeRubyCall('render', node); + const result = routeRubyCall('render', syntaxNode(node)); expect(result).toEqual({ kind: 'call' }); }); }); diff --git a/gitnexus/test/unit/calltool-dispatch-id-bridge.test.ts b/gitnexus/test/unit/calltool-dispatch-id-bridge.test.ts index be6d32215..ac832db95 100644 --- a/gitnexus/test/unit/calltool-dispatch-id-bridge.test.ts +++ b/gitnexus/test/unit/calltool-dispatch-id-bridge.test.ts @@ -29,12 +29,12 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/calltool-dispatch.test.ts b/gitnexus/test/unit/calltool-dispatch.test.ts index 7eaf2e1ff..df5895120 100644 --- a/gitnexus/test/unit/calltool-dispatch.test.ts +++ b/gitnexus/test/unit/calltool-dispatch.test.ts @@ -30,13 +30,13 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); // Re-export shim must resolve to the same mocks vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); @@ -2560,9 +2560,7 @@ describe('LocalBackend.callTool', () => { // `oldName` must sit on the file's 0-based line 1 for the definition edit to // fire. (#2380: the mock previously put it on line 0, which stopped matching // once context() went 1-based.) - const readSpy = vi - .spyOn(fsPromises, 'readFile') - .mockResolvedValue('\nfunction oldName() {}\n' as unknown as Buffer); + const readSpy = vi.spyOn(fsPromises, 'readFile').mockResolvedValue('\nfunction oldName() {}\n'); const writeSpy = vi .spyOn(fsPromises, 'writeFile') .mockRejectedValue(new Error('EACCES: permission denied')); @@ -3034,7 +3032,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { it('mode absent → callgraph result (target populated, no mode-error, BFS runs)', async () => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream' }); // A clean callgraph result carries no mode error and runs the BFS. expect(result.error ?? '').not.toMatch(/Invalid "mode"/); @@ -3062,7 +3063,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { it("mode:'pdg' routes to the PDG traversal and attaches interprocedural symbol reach", async () => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream', @@ -3079,7 +3083,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { it("mode:'pdg' labels interprocedural symbols as a callgraph bridge", async () => { resolveSingleTarget(); - vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({ + vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ).mockResolvedValueOnce({ target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' }, direction: 'downstream', impactedCount: 1, @@ -3118,7 +3125,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { it("mode:'pdg' preserves unproven bridge evidence when call-site proof is unavailable", async () => { resolveSingleTarget(); - vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({ + vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ).mockResolvedValueOnce({ target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' }, direction: 'downstream', impactedCount: 1, @@ -3159,7 +3169,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { 'invalid mode %j → structured {error}, never a callgraph result (KTD5 anti-silent-fallback)', async (bad) => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream', @@ -3176,7 +3189,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { 'line param with mode:%j → structured {error} (line is PDG-only), never a callgraph result', async (mode) => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream', @@ -3199,7 +3215,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { 'mode:%j + adapter-materialized line:0 is treated as omitted and runs the BFS (#2279)', async (mode) => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream', @@ -3296,7 +3315,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { affectedStatementCount: 1, criterionLine: 8, }); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'downstream', @@ -3349,7 +3371,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { affectedStatementCount: 0, criterionLine: 8, }); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); await backend.callTool('impact', { target: 'main', direction: 'downstream', @@ -3426,7 +3451,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { affectedStatementCount: 1, criterionLine: 8, }); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const cap = _captureLogger(); try { const result = await backend.callTool('impact', { @@ -3482,7 +3510,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { affectedStatementCount: 1, criterionLine: 8, }); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const cap = _captureLogger('debug'); try { const result = await backend.callTool('impact', { @@ -3529,7 +3560,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { affectedStatementCount: 1, criterionLine: 8, }); - vi.spyOn(backend as any, '_runImpactBFS'); + vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const cap = _captureLogger(); try { await backend.callTool('impact', { @@ -3548,7 +3582,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { it("mode:'pdg' + crossDepth → hard {error} (single-repo PDG impact)", async () => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream', @@ -3565,17 +3602,22 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { ['minConfidence', { minConfidence: 0.5 }, (opts: any) => opts.minConfidence], ])("mode:'pdg' + %s feeds the interprocedural symbol reach", async (_label, extra, readOpt) => { resolveSingleTarget(); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({ - target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' }, - direction: 'upstream', - impactedCount: 0, - risk: 'LOW', - summary: { direct: 0, processes_affected: 0, modules_affected: 0 }, - byDepthCounts: {}, - affected_processes: [], - affected_modules: [], - byDepth: {}, - }); + const bfsSpy = vi + .spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ) + .mockResolvedValueOnce({ + target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' }, + direction: 'upstream', + impactedCount: 0, + risk: 'LOW', + summary: { direct: 0, processes_affected: 0, modules_affected: 0 }, + byDepthCounts: {}, + affected_processes: [], + affected_modules: [], + byDepth: {}, + }); const result = await backend.callTool('impact', { target: 'main', direction: 'upstream', @@ -3606,7 +3648,10 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => { startLine: 8, }, ]); - const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS'); + const bfsSpy = vi.spyOn( + backend as unknown as { _runImpactBFS: LocalBackend['_runImpactBFS'] }, + '_runImpactBFS', + ); const result = await backend.callTool('impact', { target: 'login', direction: 'upstream', diff --git a/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts b/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts index a9263e37f..99f8a0043 100644 --- a/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts +++ b/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts @@ -1,3 +1,4 @@ +import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; import { describe, it, expect, vi } from 'vitest'; import { createRequire } from 'node:module'; import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js'; diff --git a/gitnexus/test/unit/cfg/cfg-builder.test.ts b/gitnexus/test/unit/cfg/cfg-builder.test.ts index 1bef8cc41..6effd0dc4 100644 --- a/gitnexus/test/unit/cfg/cfg-builder.test.ts +++ b/gitnexus/test/unit/cfg/cfg-builder.test.ts @@ -116,7 +116,7 @@ describe('ControlFlowContext', () => { it('labeled break/continue resolve to the labeled loop, not the nearest', () => { const ctx = new ControlFlowContext(); - ctx.pushLoop(/*outer*/ 100, 200, 'outer'); + ctx.pushLoop(/*outer*/ 100, 200, ['outer']); ctx.pushLoop(/*inner*/ 110, 210); expect(ctx.breakTarget('outer')).toBe(200); expect(ctx.continueTarget('outer')).toBe(100); diff --git a/gitnexus/test/unit/cfg/control-dependence.test.ts b/gitnexus/test/unit/cfg/control-dependence.test.ts index e131a946e..04f96fc18 100644 --- a/gitnexus/test/unit/cfg/control-dependence.test.ts +++ b/gitnexus/test/unit/cfg/control-dependence.test.ts @@ -49,6 +49,7 @@ function mkCfg( return { filePath: 't.ts', functionStartLine: 1, + functionEndLine: blockCount, functionStartColumn: 0, entryIndex: entry, exitIndex: exit, diff --git a/gitnexus/test/unit/cfg/post-dominators.test.ts b/gitnexus/test/unit/cfg/post-dominators.test.ts index abc257b41..4f665b7b4 100644 --- a/gitnexus/test/unit/cfg/post-dominators.test.ts +++ b/gitnexus/test/unit/cfg/post-dominators.test.ts @@ -36,6 +36,7 @@ function mkCfg( return { filePath: 't.ts', functionStartLine: 1, + functionEndLine: blockCount, functionStartColumn: 0, entryIndex: entry, exitIndex: exit, diff --git a/gitnexus/test/unit/cfg/synthetic-escape.test.ts b/gitnexus/test/unit/cfg/synthetic-escape.test.ts index ee02e01c0..5f62a7227 100644 --- a/gitnexus/test/unit/cfg/synthetic-escape.test.ts +++ b/gitnexus/test/unit/cfg/synthetic-escape.test.ts @@ -51,6 +51,7 @@ function mkCfg( return { filePath: 't.ts', functionStartLine: 1, + functionEndLine: blockCount, functionStartColumn: 0, entryIndex: entry, exitIndex: exit, diff --git a/gitnexus/test/unit/cli-commands.test.ts b/gitnexus/test/unit/cli-commands.test.ts index ade958fe2..ed8031aef 100644 --- a/gitnexus/test/unit/cli-commands.test.ts +++ b/gitnexus/test/unit/cli-commands.test.ts @@ -96,7 +96,9 @@ describe('CLI commands', () => { describe('optional parser dependencies', () => { it('loads vendored grammars from vendor/ — never file: optionalDependencies (#1728) nor a node_modules copy (#2111)', async () => { const pkg = await import('../../package.json', { with: { type: 'json' } }); - const optional = pkg.default.optionalDependencies ?? {}; + const optional = + (pkg.default as { optionalDependencies?: Record }).optionalDependencies ?? + {}; expect(optional['tree-sitter-dart']).toBeUndefined(); expect(optional['tree-sitter-proto']).toBeUndefined(); expect(optional['tree-sitter-swift']).toBeUndefined(); @@ -142,8 +144,12 @@ describe('CLI commands', () => { expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs'); expect(swiftPkg.default.version).toBe('0.7.1'); // No scripts.install / dependencies inside vendor/ (#836 / #1728 hygiene). - expect(swiftPkg.default.scripts?.install).toBeUndefined(); - expect(swiftPkg.default.dependencies).toBeUndefined(); + expect( + (swiftPkg.default as { scripts?: Record }).scripts?.install, + ).toBeUndefined(); + expect( + (swiftPkg.default as { dependencies?: Record }).dependencies, + ).toBeUndefined(); expect(swiftPkg.default.peerDependencies['tree-sitter']).toContain('^0.21.1'); // Swift is now unified with Dart/Proto/Kotlin/C: the grammar SOURCE is // vendored so build-tree-sitter-grammars.cjs can source-build the binding @@ -164,7 +170,9 @@ describe('CLI commands', () => { const kotlinPkg = await import('../../vendor/tree-sitter-kotlin/package.json', { with: { type: 'json' }, }); - const optional = pkg.default.optionalDependencies ?? {}; + const optional = + (pkg.default as { optionalDependencies?: Record }).optionalDependencies ?? + {}; // Kotlin is now VENDORED (like Swift/Dart/Proto), not a third-party npm // optionalDependency. Its prebuilds are GitNexus-cross-built (upstream // ships source only) and loaded from vendor/ by absolute path (#2111). @@ -172,8 +180,12 @@ describe('CLI commands', () => { expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs'); expect(kotlinPkg.default.version).toBe('0.4.0'); // No scripts.install / dependencies inside vendor/ (#836 / #1728 hygiene). - expect(kotlinPkg.default.scripts?.install).toBeUndefined(); - expect(kotlinPkg.default.dependencies).toBeUndefined(); + expect( + (kotlinPkg.default as { scripts?: Record }).scripts?.install, + ).toBeUndefined(); + expect( + (kotlinPkg.default as { dependencies?: Record }).dependencies, + ).toBeUndefined(); expect(kotlinPkg.default.peerDependencies['tree-sitter']).toContain('^0.21'); }); @@ -182,15 +194,21 @@ describe('CLI commands', () => { const zigPkg = await import('../../vendor/tree-sitter-zig/package.json', { with: { type: 'json' }, }); - const optional = pkg.default.optionalDependencies ?? {}; + const optional = + (pkg.default as { optionalDependencies?: Record }).optionalDependencies ?? + {}; expect(optional['@tree-sitter-grammars/tree-sitter-zig']).toBeUndefined(); expect(Object.keys(pkg.default.overrides ?? {})).not.toContain( '@tree-sitter-grammars/tree-sitter-zig', ); expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs'); expect(zigPkg.default.version).toBe('1.1.2'); - expect(zigPkg.default.scripts?.install).toBeUndefined(); - expect(zigPkg.default.dependencies).toBeUndefined(); + expect( + (zigPkg.default as { scripts?: Record }).scripts?.install, + ).toBeUndefined(); + expect( + (zigPkg.default as { dependencies?: Record }).dependencies, + ).toBeUndefined(); expect(zigPkg.default.peerDependencies['tree-sitter']).toContain('^0.21'); }); @@ -205,8 +223,12 @@ describe('CLI commands', () => { expect(pkg.default.dependencies['tree-sitter-c']).toBeUndefined(); expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-grammars.cjs'); expect(cPkg.default.version).toBe('0.21.4'); - expect(cPkg.default.scripts?.install).toBeUndefined(); - expect(cPkg.default.dependencies).toBeUndefined(); + expect( + (cPkg.default as { scripts?: Record }).scripts?.install, + ).toBeUndefined(); + expect( + (cPkg.default as { dependencies?: Record }).dependencies, + ).toBeUndefined(); }); }); diff --git a/gitnexus/test/unit/cohesion-consistency.test.ts b/gitnexus/test/unit/cohesion-consistency.test.ts index 327f0cd65..0301264e7 100644 --- a/gitnexus/test/unit/cohesion-consistency.test.ts +++ b/gitnexus/test/unit/cohesion-consistency.test.ts @@ -10,7 +10,7 @@ */ import { describe, it, expect } from 'vitest'; import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; -import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js'; +import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; import { processCommunities } from '../../src/core/ingestion/community-processor.js'; // ============================================================================ diff --git a/gitnexus/test/unit/community-processor.test.ts b/gitnexus/test/unit/community-processor.test.ts index e9b3d07aa..a2bb8ce6d 100644 --- a/gitnexus/test/unit/community-processor.test.ts +++ b/gitnexus/test/unit/community-processor.test.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { Worker } from 'node:worker_threads'; import { describe, it, expect, vi } from 'vitest'; import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; -import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js'; +import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; import { getCommunityColor, COMMUNITY_COLORS, diff --git a/gitnexus/test/unit/convex-metadata.test.ts b/gitnexus/test/unit/convex-metadata.test.ts index e486f6bc2..45bb603b3 100644 --- a/gitnexus/test/unit/convex-metadata.test.ts +++ b/gitnexus/test/unit/convex-metadata.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import Parser from 'tree-sitter'; import TypeScript from 'tree-sitter-typescript'; -import type { ParsedImport } from 'gitnexus-shared'; +import type { NodeLabel, ParsedImport } from 'gitnexus-shared'; import { extractConvexEndpointProperties } from '../../src/core/ingestion/languages/typescript/convex-endpoint-metadata.js'; import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js'; @@ -27,18 +27,19 @@ const namedImport = ( importedName: string, localName = importedName, ): ParsedImport => ({ - kind: localName === importedName ? 'named' : 'alias', + ...(localName === importedName + ? { kind: 'named' as const } + : { kind: 'alias' as const, alias: localName }), targetRaw, importedName, localName, - ...(localName === importedName ? {} : { alias: localName }), }); function extract( source: string, imports: readonly ParsedImport[], isExported = true, - nodeLabel = 'Const', + nodeLabel: NodeLabel = 'Const', definitionType = 'export_statement', ) { return extractConvexEndpointProperties({ diff --git a/gitnexus/test/unit/cpp-ue-preprocessor.test.ts b/gitnexus/test/unit/cpp-ue-preprocessor.test.ts index 087c5ba2a..dbbc0dcfb 100644 --- a/gitnexus/test/unit/cpp-ue-preprocessor.test.ts +++ b/gitnexus/test/unit/cpp-ue-preprocessor.test.ts @@ -211,7 +211,7 @@ describe('stripUeMacros — tree-sitter extraction (end-to-end)', () => { */ function extractClassNames(source: string): string[] { const parser = new Parser(); - parser.setLanguage(CPP as unknown as Parser.Language); + parser.setLanguage(CPP); const tree = parser.parse(source); const names: string[] = []; const stack: Parser.SyntaxNode[] = [tree.rootNode]; @@ -249,7 +249,7 @@ describe('stripUeMacros — tree-sitter extraction (end-to-end)', () => { const src = `UCLASS()\nclass BRAWLUI_API UMyClass : public UObject\n{\n GENERATED_BODY()\n public:\n void Run();\n};`; const out = stripUeMacros(src); const parser = new Parser(); - parser.setLanguage(CPP as unknown as Parser.Language); + parser.setLanguage(CPP); const tree = parser.parse(out); const stack: Parser.SyntaxNode[] = [tree.rootNode]; let runLine: number | undefined; diff --git a/gitnexus/test/unit/embedding-restore-spill.test.ts b/gitnexus/test/unit/embedding-restore-spill.test.ts index b11695110..7f0fd8a8b 100644 --- a/gitnexus/test/unit/embedding-restore-spill.test.ts +++ b/gitnexus/test/unit/embedding-restore-spill.test.ts @@ -39,7 +39,7 @@ function row(id: string, fill: number, hash = `hash-${id}`) { } describe('embedding-restore-spill (#3306)', () => { - const spills: Array<{ path: string }> = []; + const spills: Array[0]> = []; afterEach(() => { for (const spill of spills) disposeEmbeddingSpill(spill); spills.length = 0; @@ -170,7 +170,7 @@ describe('embedding-restore-spill (#3306)', () => { const shortPath = path.join(os.tmpdir(), `gitnexus-embed-restore-short-${process.pid}.bin`); writeFileSync(shortPath, Buffer.from('NOPE')); - spills.push({ path: shortPath }); + spills.push({ path: shortPath, dims: DIMS, rowCount: 1 }); expect(() => readSpillVectors({ path: shortPath, dims: DIMS, rowCount: 1 }, [0])).toThrow( /invalid embedding spill header/, ); @@ -181,7 +181,7 @@ describe('embedding-restore-spill (#3306)', () => { badMagic.writeUInt32LE(DIMS, 5); const badPath = path.join(os.tmpdir(), `gitnexus-embed-restore-bad-${process.pid}.bin`); writeFileSync(badPath, badMagic); - spills.push({ path: badPath }); + spills.push({ path: badPath, dims: DIMS, rowCount: 1 }); expect(() => readSpillVectors({ path: badPath, dims: DIMS, rowCount: 1 }, [0])).toThrow( /invalid embedding spill header/, ); diff --git a/gitnexus/test/unit/embedding-sidecar-client.test.ts b/gitnexus/test/unit/embedding-sidecar-client.test.ts index 0bdf5745f..7e6943419 100644 --- a/gitnexus/test/unit/embedding-sidecar-client.test.ts +++ b/gitnexus/test/unit/embedding-sidecar-client.test.ts @@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; +import type { ForkImpl } from '../../src/core/embeddings/embedding-sidecar-client.js'; import type { ChildProcess } from 'node:child_process'; import type { SidecarRequest, @@ -57,7 +59,7 @@ describe('embedding sidecar client', () => { const hostPlatform = process.platform; const hostArch = process.arch; - let forkMock: ReturnType; + let forkMock: Mock; let children: FakeChild[]; beforeEach(async () => { @@ -66,7 +68,7 @@ describe('embedding sidecar client', () => { Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }); Object.defineProperty(process, 'arch', { value: 'x64', configurable: true }); children = []; - forkMock = vi.fn((_script: string, _args: string[], _opts: unknown) => { + forkMock = vi.fn((_script, _args, _opts) => { const child = new FakeChild(); children.push(child); return child as unknown as ChildProcess; diff --git a/gitnexus/test/unit/embeddings-install-command.test.ts b/gitnexus/test/unit/embeddings-install-command.test.ts index b60348c6c..00961e090 100644 --- a/gitnexus/test/unit/embeddings-install-command.test.ts +++ b/gitnexus/test/unit/embeddings-install-command.test.ts @@ -13,13 +13,15 @@ import type { LoggerCapture } from '../../src/core/logger.js'; const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>(); const isPrefixRuntimeLoadableMock = vi.fn(() => true); -const installEmbeddingRuntimeMock = vi.fn(async () => undefined); +const installEmbeddingRuntimeMock = vi.fn< + typeof import('../../src/core/embeddings/runtime-install.js').installEmbeddingRuntime +>(async () => undefined); vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({ ...(await importOriginal()), resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(), isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(), - installEmbeddingRuntime: (opts?: unknown) => installEmbeddingRuntimeMock(opts), + installEmbeddingRuntime: installEmbeddingRuntimeMock, getEmbeddingRuntimeDir: () => '/fake/embedding-runtime', getEmbeddingStackSpecs: () => ({ '@huggingface/transformers': '^4.1.0' }), })); diff --git a/gitnexus/test/unit/ensure-initialized-reinit-watermark.test.ts b/gitnexus/test/unit/ensure-initialized-reinit-watermark.test.ts index 5dc089e56..6ae5835c6 100644 --- a/gitnexus/test/unit/ensure-initialized-reinit-watermark.test.ts +++ b/gitnexus/test/unit/ensure-initialized-reinit-watermark.test.ts @@ -29,7 +29,7 @@ vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => { }; }); -import { LocalBackend } from '../../src/mcp/local/local-backend'; +import { LocalBackend } from '../../src/mcp/local/local-backend.js'; describe('ensureInitialized reinit watermark (tri-review NEW-7)', () => { const poolKey = '/tmp/nonexistent-repo/.gitnexus/lbug'; diff --git a/gitnexus/test/unit/entry-point-scoring.test.ts b/gitnexus/test/unit/entry-point-scoring.test.ts index 5d0992831..e468a94dd 100644 --- a/gitnexus/test/unit/entry-point-scoring.test.ts +++ b/gitnexus/test/unit/entry-point-scoring.test.ts @@ -1,3 +1,4 @@ +import { SupportedLanguages } from 'gitnexus-shared'; import { describe, it, expect } from 'vitest'; import { calculateEntryPointScore, @@ -8,34 +9,58 @@ import { describe('calculateEntryPointScore', () => { describe('base scoring', () => { it('returns 0 for functions with no outgoing calls', () => { - const result = calculateEntryPointScore('handler', 'typescript', true, 0, 0); + const result = calculateEntryPointScore('handler', SupportedLanguages.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); + const result = calculateEntryPointScore( + 'doStuff', + SupportedLanguages.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); + const few = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 1, 5); + const many = calculateEntryPointScore('doStuff', SupportedLanguages.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); + const exported = calculateEntryPointScore( + 'doStuff', + SupportedLanguages.TypeScript, + true, + 0, + 4, + ); + const notExported = calculateEntryPointScore( + 'doStuff', + SupportedLanguages.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); + const result = calculateEntryPointScore( + 'doStuff', + SupportedLanguages.TypeScript, + false, + 0, + 4, + ); expect(result.reasons).not.toContain('exported'); }); }); @@ -44,7 +69,7 @@ describe('calculateEntryPointScore', () => { it.each(['main', 'init', 'bootstrap', 'start', 'run', 'setup', 'configure'])( 'recognizes "%s" as entry point pattern', (name) => { - const result = calculateEntryPointScore(name, 'typescript', false, 0, 3); + const result = calculateEntryPointScore(name, SupportedLanguages.TypeScript, false, 0, 3); expect(result.reasons).toContain('entry-pattern'); }, ); @@ -64,13 +89,19 @@ describe('calculateEntryPointScore', () => { 'fireEvent', 'emitEvent', ])('recognizes "%s" as entry point pattern', (name) => { - const result = calculateEntryPointScore(name, 'typescript', false, 0, 3); + const result = calculateEntryPointScore(name, SupportedLanguages.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); + const matching = calculateEntryPointScore( + 'handleLogin', + SupportedLanguages.TypeScript, + false, + 0, + 4, + ); + const plain = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 0, 4); // matching gets 1.5x, plain gets 1.0x expect(matching.score).toBe(plain.score * 1.5); }); @@ -78,63 +109,81 @@ describe('calculateEntryPointScore', () => { describe('language-specific patterns', () => { it('recognizes React hooks for TypeScript', () => { - const result = calculateEntryPointScore('useEffect', 'typescript', false, 0, 2); + const result = calculateEntryPointScore( + 'useEffect', + SupportedLanguages.TypeScript, + false, + 0, + 2, + ); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes React hooks for JavaScript', () => { - const result = calculateEntryPointScore('useState', 'javascript', false, 0, 2); + const result = calculateEntryPointScore( + 'useState', + SupportedLanguages.JavaScript, + false, + 0, + 2, + ); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes Python REST patterns', () => { - const result = calculateEntryPointScore('get_users', 'python', false, 0, 2); + const result = calculateEntryPointScore('get_users', SupportedLanguages.Python, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes Java servlet patterns', () => { - const result = calculateEntryPointScore('doGet', 'java', false, 0, 2); + const result = calculateEntryPointScore('doGet', SupportedLanguages.Java, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes Go handler patterns', () => { - const result = calculateEntryPointScore('NewServer', 'go', false, 0, 2); + const result = calculateEntryPointScore('NewServer', SupportedLanguages.Go, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes Rust entry patterns', () => { - const result = calculateEntryPointScore('handle_request', 'rust', false, 0, 2); + const result = calculateEntryPointScore( + 'handle_request', + SupportedLanguages.Rust, + false, + 0, + 2, + ); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes Swift UIKit lifecycle', () => { - const result = calculateEntryPointScore('viewDidLoad', 'swift', false, 0, 2); + const result = calculateEntryPointScore('viewDidLoad', SupportedLanguages.Swift, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes Swift SwiftUI body', () => { - const result = calculateEntryPointScore('body', 'swift', false, 0, 2); + const result = calculateEntryPointScore('body', SupportedLanguages.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); + const result = calculateEntryPointScore('handle', SupportedLanguages.PHP, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes PHP RESTful resource methods', () => { - const result = calculateEntryPointScore('index', 'php', false, 0, 2); + const result = calculateEntryPointScore('index', SupportedLanguages.PHP, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes C# ASP.NET patterns', () => { - const result = calculateEntryPointScore('GetUsers', 'csharp', false, 0, 2); + const result = calculateEntryPointScore('GetUsers', SupportedLanguages.CSharp, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); it('recognizes C main entry point', () => { - const result = calculateEntryPointScore('main', 'c', false, 0, 2); + const result = calculateEntryPointScore('main', SupportedLanguages.C, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); @@ -152,7 +201,7 @@ describe('calculateEntryPointScore', () => { 'session_create', 'window_resize', ])('recognizes C pattern "%s"', (name) => { - const result = calculateEntryPointScore(name, 'c', false, 0, 2); + const result = calculateEntryPointScore(name, SupportedLanguages.C, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); @@ -167,7 +216,7 @@ describe('calculateEntryPointScore', () => { 'OnEventReceived', 'on_click', ])('recognizes C++ pattern "%s"', (name) => { - const result = calculateEntryPointScore(name, 'cpp', false, 0, 2); + const result = calculateEntryPointScore(name, SupportedLanguages.CPlusPlus, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); @@ -186,7 +235,7 @@ describe('calculateEntryPointScore', () => { 'module', 'AuthService', ])('recognizes Kotlin pattern "%s"', (name) => { - const result = calculateEntryPointScore(name, 'kotlin', false, 0, 2); + const result = calculateEntryPointScore(name, SupportedLanguages.Kotlin, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); @@ -197,14 +246,14 @@ describe('calculateEntryPointScore', () => { it.each(['main', 'build', 'createState', 'initState', 'dispose', 'runApp', 'onEvent'])( 'recognizes Dart pattern "%s"', (name) => { - const result = calculateEntryPointScore(name, 'dart', false, 0, 2); + const result = calculateEntryPointScore(name, SupportedLanguages.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); + const result = calculateEntryPointScore(name, SupportedLanguages.Ruby, false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); }); @@ -226,15 +275,21 @@ describe('calculateEntryPointScore', () => { 'cloneDeep', 'mergeObjects', ])('penalizes utility function "%s"', (name) => { - const result = calculateEntryPointScore(name, 'typescript', false, 0, 3); + const result = calculateEntryPointScore(name, SupportedLanguages.TypeScript, false, 0, 3); expect(result.reasons).toContain('utility-pattern'); // 0.3 multiplier - const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 3); + const plain = calculateEntryPointScore('doStuff', SupportedLanguages.TypeScript, false, 0, 3); expect(result.score).toBeLessThan(plain.score); }); it('penalizes private-by-convention functions', () => { - const result = calculateEntryPointScore('_internal', 'typescript', false, 0, 3); + const result = calculateEntryPointScore( + '_internal', + SupportedLanguages.TypeScript, + false, + 0, + 3, + ); expect(result.reasons).toContain('utility-pattern'); }); }); @@ -243,7 +298,7 @@ describe('calculateEntryPointScore', () => { it('boosts Next.js page entry points', () => { const result = calculateEntryPointScore( 'render', - 'typescript', + SupportedLanguages.TypeScript, true, 0, 3, @@ -256,7 +311,7 @@ describe('calculateEntryPointScore', () => { it('does not apply framework bonus for non-framework paths', () => { const result = calculateEntryPointScore( 'render', - 'typescript', + SupportedLanguages.TypeScript, true, 0, 3, @@ -271,7 +326,7 @@ describe('calculateEntryPointScore', () => { // handleLogin: entry pattern (1.5x) + exported (2.0x) + base const result = calculateEntryPointScore( 'handleLogin', - 'typescript', + SupportedLanguages.TypeScript, true, 0, 4, diff --git a/gitnexus/test/unit/fetch-reason-parsing.test.ts b/gitnexus/test/unit/fetch-reason-parsing.test.ts index 4d81cc5db..ffb0fc802 100644 --- a/gitnexus/test/unit/fetch-reason-parsing.test.ts +++ b/gitnexus/test/unit/fetch-reason-parsing.test.ts @@ -84,7 +84,8 @@ describe('fetch reason field parsing', () => { describe('confidence derivation from fetchCount', () => { it('high confidence when fetchCount is undefined (single fetch)', () => { - const isMultiFetch = (undefined ?? 1) > 1; + const fetchCount: number | undefined = undefined; + const isMultiFetch = (fetchCount ?? 1) > 1; expect(isMultiFetch).toBe(false); }); diff --git a/gitnexus/test/unit/field-extraction.test.ts b/gitnexus/test/unit/field-extraction.test.ts index fd86c3aaa..81062ced2 100644 --- a/gitnexus/test/unit/field-extraction.test.ts +++ b/gitnexus/test/unit/field-extraction.test.ts @@ -62,7 +62,8 @@ const createMockContext = (): FieldExtractorContext => ({ fileScope: () => new Map(), allScopes: () => new Map(), constructorTypeMap: new Map(), - } as TypeEnvironment, + flush: () => {}, + } satisfies TypeEnvironment, symbolTable: createSemanticModel().symbols, filePath: 'test.ts', language: SupportedLanguages.TypeScript, @@ -1327,7 +1328,7 @@ describeKotlin('GenericFieldExtractor — Kotlin (F52 companion)', () => { /** The first companion_object node in `src`. */ function companion(src: string): Parser.SyntaxNode { - parser.setLanguage(Kotlin as Parser.Language); + parser.setLanguage(Kotlin); const tree = parser.parse(src); let found: Parser.SyntaxNode | undefined; const walk = (n: Parser.SyntaxNode) => { @@ -1401,7 +1402,7 @@ describeKotlin('GenericFieldExtractor — Kotlin (F52 companion)', () => { /** Parse `src` and return the first node of the given type (depth-first). */ function firstNodeOfType(src: string, type: string): Parser.SyntaxNode { - parser.setLanguage(Kotlin as Parser.Language); + parser.setLanguage(Kotlin); const tree = parser.parse(src); let found: Parser.SyntaxNode | undefined; const walk = (n: Parser.SyntaxNode) => { @@ -1436,7 +1437,7 @@ describeSwift('GenericFieldExtractor — Swift (F75 protocol property requiremen /** Parse `src` and return the first class/protocol declaration node. */ function declNode(src: string): Parser.SyntaxNode { - parser.setLanguage(Swift as Parser.Language); + parser.setLanguage(Swift); const tree = parser.parse(src); const node = tree.rootNode.child(0); if (!node) throw new Error('no declaration node'); diff --git a/gitnexus/test/unit/framework-detection.test.ts b/gitnexus/test/unit/framework-detection.test.ts index 7757f7e18..58a25dea7 100644 --- a/gitnexus/test/unit/framework-detection.test.ts +++ b/gitnexus/test/unit/framework-detection.test.ts @@ -227,14 +227,14 @@ describe('detectFrameworkFromPath', () => { it('detects main.rs', () => { const result = detectFrameworkFromPath('src/main.rs'); expect(result).not.toBeNull(); - expect(result!.framework).toBe('rust'); + expect(result!.framework).toBe(SupportedLanguages.Rust); expect(result!.entryPointMultiplier).toBe(3.0); }); it('detects bin folder', () => { const result = detectFrameworkFromPath('src/bin/cli.rs'); expect(result).not.toBeNull(); - expect(result!.framework).toBe('rust'); + expect(result!.framework).toBe(SupportedLanguages.Rust); }); }); @@ -325,52 +325,53 @@ describe('detectFrameworkFromPath', () => { }); describe('detectFrameworkFromAST', () => { + // Deliberately invalid enum inputs exercise runtime tolerance at the public boundary. it('returns null for empty inputs', () => { - expect(detectFrameworkFromAST('', '')).toBeNull(); - expect(detectFrameworkFromAST('typescript', '')).toBeNull(); - expect(detectFrameworkFromAST('', 'some code')).toBeNull(); + expect(detectFrameworkFromAST('' as SupportedLanguages, '')).toBeNull(); + expect(detectFrameworkFromAST(SupportedLanguages.TypeScript, '')).toBeNull(); + expect(detectFrameworkFromAST('' as SupportedLanguages, 'some code')).toBeNull(); }); it('detects NestJS decorators in TypeScript', () => { - const result = detectFrameworkFromAST('typescript', '@Controller("/users")'); + const result = detectFrameworkFromAST(SupportedLanguages.TypeScript, '@Controller("/users")'); expect(result).not.toBeNull(); expect(result!.framework).toBe('nestjs'); expect(result!.entryPointMultiplier).toBe(3.2); }); it('detects NestJS decorators in JavaScript', () => { - const result = detectFrameworkFromAST('javascript', '@Get("/")'); + const result = detectFrameworkFromAST(SupportedLanguages.JavaScript, '@Get("/")'); expect(result).not.toBeNull(); expect(result!.framework).toBe('nestjs'); }); it('detects FastAPI decorators in Python', () => { - const result = detectFrameworkFromAST('python', '@app.get("/users")'); + const result = detectFrameworkFromAST(SupportedLanguages.Python, '@app.get("/users")'); expect(result).not.toBeNull(); expect(result!.framework).toBe('fastapi'); }); it('detects Flask decorators in Python', () => { - const result = detectFrameworkFromAST('python', '@app.route("/users")'); + const result = detectFrameworkFromAST(SupportedLanguages.Python, '@app.route("/users")'); expect(result).not.toBeNull(); expect(result!.framework).toBe('flask'); }); it('detects Spring annotations in Java', () => { - const result = detectFrameworkFromAST('java', '@RestController'); + const result = detectFrameworkFromAST(SupportedLanguages.Java, '@RestController'); expect(result).not.toBeNull(); expect(result!.framework).toBe('spring'); }); it('detects ASP.NET attributes in C#', () => { - const result = detectFrameworkFromAST('csharp', '[ApiController]'); + const result = detectFrameworkFromAST(SupportedLanguages.CSharp, '[ApiController]'); expect(result).not.toBeNull(); expect(result!.framework).toBe('aspnet'); }); it('detects Laravel route definitions in PHP', () => { const result = detectFrameworkFromAST( - 'php', + SupportedLanguages.PHP, "Route::get('/users', [UserController::class, 'index'])", ); expect(result).not.toBeNull(); @@ -378,27 +379,33 @@ describe('detectFrameworkFromAST', () => { }); it('detects Actix-web route attributes in Rust', () => { - const result = detectFrameworkFromAST('rust', '#[get("/")]'); + const result = detectFrameworkFromAST(SupportedLanguages.Rust, '#[get("/")]'); expect(result).not.toBeNull(); expect(result!.framework).toBe('actix-web'); }); it('returns null for language with no matching pattern', () => { - expect(detectFrameworkFromAST('c', 'int main() { return 0; }')).toBeNull(); + expect(detectFrameworkFromAST(SupportedLanguages.C, 'int main() { return 0; }')).toBeNull(); }); it('is case-insensitive', () => { - const result = detectFrameworkFromAST('TypeScript', '@controller("/")'); + const result = detectFrameworkFromAST('TypeScript' as SupportedLanguages, '@controller("/")'); expect(result).not.toBeNull(); }); it('detects Expo Router useRouter hook', () => { - const result = detectFrameworkFromAST('typescript', 'const router = useRouter()'); + const result = detectFrameworkFromAST( + SupportedLanguages.TypeScript, + 'const router = useRouter()', + ); expect(result).not.toBeNull(); expect(result!.framework).toBe('expo-router'); }); it('detects Expo Router router.push', () => { - const result = detectFrameworkFromAST('javascript', "router.push('/settings')"); + const result = detectFrameworkFromAST( + SupportedLanguages.JavaScript, + "router.push('/settings')", + ); expect(result).not.toBeNull(); expect(result!.framework).toBe('expo-router'); }); diff --git a/gitnexus/test/unit/git.test.ts b/gitnexus/test/unit/git.test.ts index 7ed94cfa8..b48fa9f89 100644 --- a/gitnexus/test/unit/git.test.ts +++ b/gitnexus/test/unit/git.test.ts @@ -196,6 +196,9 @@ describe('git utilities', () => { stdout: '', stderr: '', error: Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT' }), + pid: 0, + output: ['', '', ''], + signal: null, } as ReturnType); expect(listLocalHeads('/missing-git')).toBeNull(); }); diff --git a/gitnexus/test/unit/graph-collapse-wiring.test.ts b/gitnexus/test/unit/graph-collapse-wiring.test.ts index 13c09605a..1e09c1968 100644 --- a/gitnexus/test/unit/graph-collapse-wiring.test.ts +++ b/gitnexus/test/unit/graph-collapse-wiring.test.ts @@ -34,7 +34,7 @@ const graphWith = ( ): Pick => ({ forEachRelationshipFields(fn) { for (const [type, count] of Object.entries(byType)) { - for (let i = 0; i < (count ?? 0); i++) fn('src', 'dst', type as RelationshipType, 1); + for (let i = 0; i < (count ?? 0); i++) fn('src', 'dst', type as RelationshipType, 1, ''); } }, }); diff --git a/gitnexus/test/unit/graph.test.ts b/gitnexus/test/unit/graph.test.ts index 47be5125d..1106b3e12 100644 --- a/gitnexus/test/unit/graph.test.ts +++ b/gitnexus/test/unit/graph.test.ts @@ -6,7 +6,7 @@ */ import { describe, it, expect } from 'vitest'; import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; -import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js'; +import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; function makeNode(id: string, name: string, filePath: string = 'src/test.ts'): GraphNode { return { @@ -410,8 +410,9 @@ describe('createKnowledgeGraph', () => { const node: Parameters[0] = { id: 'cluster:x', label: 'Community', - properties: { name: 'x' }, + properties: { name: 'x', filePath: '' }, }; + Reflect.deleteProperty(node.properties, 'filePath'); g.addNode(node); g.addNode(makeNode('fn:a', 'a', 'src/a.ts')); diff --git a/gitnexus/test/unit/group/cross-impact.test.ts b/gitnexus/test/unit/group/cross-impact.test.ts index bea545017..66b329a3a 100644 --- a/gitnexus/test/unit/group/cross-impact.test.ts +++ b/gitnexus/test/unit/group/cross-impact.test.ts @@ -60,7 +60,7 @@ describe('cross-impact', () => { direction: 'sideways', }); expect(r.ok).toBe(false); - if (!r.ok) expect(r.error).toContain('direction'); + if (r.ok === false) expect(r.error).toContain('direction'); }); it('test_validateGroupImpactParams_clamps_crossDepth_and_warns', () => { @@ -110,7 +110,7 @@ describe('cross-impact', () => { direction: 'upstream', }); expect(r.ok).toBe(false); - if (!r.ok) expect(r.error).toBe('target or target_uid is required'); + if (r.ok === false) expect(r.error).toBe('target or target_uid is required'); }); it('test_collectImpactSymbolUids_respects_service_prefix', () => { diff --git a/gitnexus/test/unit/group/fastapi-composed-provider.test.ts b/gitnexus/test/unit/group/fastapi-composed-provider.test.ts index b0d342ebd..01dcb37fb 100644 --- a/gitnexus/test/unit/group/fastapi-composed-provider.test.ts +++ b/gitnexus/test/unit/group/fastapi-composed-provider.test.ts @@ -34,6 +34,7 @@ function run(files: Record): RunResult { }; const readFile = (rel: string): string | null => files[rel] ?? null; const ctx = PYTHON_HTTP_PLUGIN.prepareRepo?.({ + repoPath: '/repo', files: Object.keys(files), parser, readFile, diff --git a/gitnexus/test/unit/group/grpc-extractor.test.ts b/gitnexus/test/unit/group/grpc-extractor.test.ts index bf1a98033..0c244b3f2 100644 --- a/gitnexus/test/unit/group/grpc-extractor.test.ts +++ b/gitnexus/test/unit/group/grpc-extractor.test.ts @@ -1391,6 +1391,7 @@ service Foo { rpc Bar (Req) returns (Res); }`; describe('resolveProtoConflict', () => { const makeInfo = (pkg: string, protoPath: string): ProtoServiceInfo => ({ package: pkg, + javaPackage: '', serviceName: 'Svc', methods: ['Do'], protoPath, diff --git a/gitnexus/test/unit/group/java-const-route-parity.test.ts b/gitnexus/test/unit/group/java-const-route-parity.test.ts index 62c028e8e..256959c3b 100644 --- a/gitnexus/test/unit/group/java-const-route-parity.test.ts +++ b/gitnexus/test/unit/group/java-const-route-parity.test.ts @@ -29,7 +29,6 @@ import { expandJavaWildcardStaticImports, extractJavaModuleConstants, foldJavaOperands, - type RepoConstants, } from '../../../src/core/ingestion/route-extractors/java-const-resolver.js'; const parser = new Parser(); @@ -42,6 +41,7 @@ const parse = (src: string): Parser.Tree => parseSource(parser, src); /** Group side: prepareRepo + a 3-argument scan over every .java file. */ function groupProviders(files: Record): string[] { const ctx = JAVA_HTTP_PLUGIN.prepareRepo?.({ + repoPath: '/repo', files: Object.keys(files), parser: new Parser(), readFile: (rel: string) => files[rel] ?? null, @@ -59,7 +59,7 @@ function groupProviders(files: Record): string[] { /** Ingestion side: extract routes, then fold operands against the same map. */ function ingestionRoutes(files: Record): string[] { - const repo: RepoConstants = new Map(); + const repo = new Map>(); for (const [rel, src] of Object.entries(files)) { repo.set(rel, extractJavaModuleConstants(parse(src))); } diff --git a/gitnexus/test/unit/group/kotlin-const-route-fold.test.ts b/gitnexus/test/unit/group/kotlin-const-route-fold.test.ts index e38e05079..c486a4ed3 100644 --- a/gitnexus/test/unit/group/kotlin-const-route-fold.test.ts +++ b/gitnexus/test/unit/group/kotlin-const-route-fold.test.ts @@ -61,7 +61,7 @@ const describeKotlin = Kotlin && KOTLIN_HTTP_PLUGIN ? describe : describe.skip; const plugin = KOTLIN_HTTP_PLUGIN as HttpLanguagePlugin; const parseSource = (p: Parser, src: string): Parser.Tree => { - p.setLanguage(Kotlin as Parser.Language); + p.setLanguage(Kotlin); return p.parse(src); }; diff --git a/gitnexus/test/unit/group/service.test.ts b/gitnexus/test/unit/group/service.test.ts index 770e6889f..fde1be468 100644 --- a/gitnexus/test/unit/group/service.test.ts +++ b/gitnexus/test/unit/group/service.test.ts @@ -303,7 +303,7 @@ describe('GroupService', () => { const { cleanup, tmpDir } = makeTmpGroup(); try { vi.stubEnv('GITNEXUS_HOME', tmpDir); - const query = vi.fn(async () => ({ processes: [] })); + const query = vi.fn(async () => ({ processes: [] })); const svc = new GroupService(makePort({ query })); await svc.groupQuery({ name: 'test-group', query: 'auth flow' }); expect(query).toHaveBeenCalled(); @@ -320,7 +320,7 @@ describe('GroupService', () => { const { cleanup, tmpDir } = makeTmpGroup(); try { vi.stubEnv('GITNEXUS_HOME', tmpDir); - const query = vi.fn(async () => ({ processes: [] })); + const query = vi.fn(async () => ({ processes: [] })); const svc = new GroupService(makePort({ query })); await svc.groupQuery({ name: 'test-group', @@ -342,7 +342,7 @@ describe('GroupService', () => { const { cleanup, tmpDir } = makeTmpGroup(); try { vi.stubEnv('GITNEXUS_HOME', tmpDir); - const query = vi.fn(async () => ({ processes: [] })); + const query = vi.fn(async () => ({ processes: [] })); const svc = new GroupService(makePort({ query })); const infiniteLimit = await svc.groupQuery({ @@ -403,7 +403,7 @@ describe('GroupService', () => { const { cleanup, tmpDir } = makeTmpGroup(); try { vi.stubEnv('GITNEXUS_HOME', tmpDir); - const query = vi.fn(async () => ({ processes: [] })); + const query = vi.fn(async () => ({ processes: [] })); const svc = new GroupService(makePort({ query })); await svc.groupQuery({ name: 'test-group', query: 'auth flow', chain_depth: 2 }); expect(query).toHaveBeenCalled(); @@ -575,7 +575,7 @@ repos: const { cleanup, tmpDir } = makeTmpGroup(); try { vi.stubEnv('GITNEXUS_HOME', tmpDir); - const context = vi.fn(async () => ({ status: 'found' })); + const context = vi.fn(async () => ({ status: 'found' })); const svc = new GroupService(makePort({ context })); const r = await svc.groupContext({ name: 'test-group', @@ -594,7 +594,7 @@ repos: const { cleanup, tmpDir } = makeTmpGroup(); try { vi.stubEnv('GITNEXUS_HOME', tmpDir); - const context = vi.fn(async () => ({ + const context = vi.fn(async () => ({ status: 'found', symbol: { filePath: 'services/auth/x.ts', uid: 'u1', name: 'X' }, })); diff --git a/gitnexus/test/unit/group/sync-partial-extraction.test.ts b/gitnexus/test/unit/group/sync-partial-extraction.test.ts index e1588be1c..d676cdcbe 100644 --- a/gitnexus/test/unit/group/sync-partial-extraction.test.ts +++ b/gitnexus/test/unit/group/sync-partial-extraction.test.ts @@ -54,10 +54,12 @@ const grpcExtract = vi.fn(); // Bound through an arrow so the test body can read its calls: which repos the // deferred manifest phase re-opens is the observable side of dropping a failed // repo's handle, and a `vi.fn()` created inside the factory is unreachable here. -const initLbugMock = vi.fn(async () => {}); +const initLbugMock = vi.fn( + async () => true, +); vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({ - initLbug: (...args: unknown[]) => initLbugMock(...args), + initLbug: initLbugMock, executeParameterized: vi.fn(async () => []), pinRepo: vi.fn(() => () => {}), getMaxResidentRepos: vi.fn(() => 5), diff --git a/gitnexus/test/unit/group/sync-registry-identity.test.ts b/gitnexus/test/unit/group/sync-registry-identity.test.ts index fb63a1386..f718ad515 100644 --- a/gitnexus/test/unit/group/sync-registry-identity.test.ts +++ b/gitnexus/test/unit/group/sync-registry-identity.test.ts @@ -9,10 +9,12 @@ import type { GroupConfig } from '../../../src/core/group/types.js'; import { GroupService } from '../../../src/core/group/service.js'; import type { GroupToolPort } from '../../../src/core/group/service.js'; -const initLbugMock = vi.fn(async () => {}); +const initLbugMock = vi.fn( + async () => true, +); vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({ - initLbug: (...args: unknown[]) => initLbugMock(...args), + initLbug: (...args: Parameters) => initLbugMock(...args), executeParameterized: vi.fn(async () => []), pinRepo: vi.fn(() => () => {}), getMaxResidentRepos: vi.fn(() => 5), diff --git a/gitnexus/test/unit/group/sync.test.ts b/gitnexus/test/unit/group/sync.test.ts index 9e844fced..878aa0581 100644 --- a/gitnexus/test/unit/group/sync.test.ts +++ b/gitnexus/test/unit/group/sync.test.ts @@ -1438,6 +1438,7 @@ describe('syncGroup windowed manifest resolution (issue #2189 / PR #2191 review) const n = (initCounts.get(id) ?? 0) + 1; initCounts.set(id, n); if (id === 'app-repo-2' && n === 2) throw new Error('window init boom'); + return true; }); const execSpy = vi.spyOn(poolAdapter, 'executeParameterized').mockResolvedValue([]); const pinSpy = vi.spyOn(poolAdapter, 'pinRepo').mockImplementation(() => { diff --git a/gitnexus/test/unit/has-method.test.ts b/gitnexus/test/unit/has-method.test.ts index 5c17cc730..7c3484b04 100644 --- a/gitnexus/test/unit/has-method.test.ts +++ b/gitnexus/test/unit/has-method.test.ts @@ -509,24 +509,17 @@ class Outer { }); it('returns null for a node without parent', () => { - // Simulate a node with null parent - const fakeNode = { parent: null }; - const result = findEnclosingClassId(fakeNode, filePath); + const root = parseCode(TypeScript.typescript, 'const value = 1;').rootNode; + expect(root.parent).toBeNull(); + const result = findEnclosingClassId(root, filePath); expect(result).toBeNull(); }); it('skips containers without a name node', () => { - // Simulate AST nodes where the class container has no name - const fakeClassNode = { - type: 'class_declaration', - childForFieldName: () => null, - children: [], - parent: null, - }; - const fakeChild = { - parent: fakeClassNode, - }; - const result = findEnclosingClassId(fakeChild, filePath); + const tree = parseCode(TypeScript.typescript, 'const Anonymous = class { method() {} };'); + const method = findNode(tree.rootNode, (node) => node.type === 'method_definition'); + expect(method).not.toBeNull(); + const result = findEnclosingClassId(method!, filePath); // The class has no name, so should return null expect(result).toBeNull(); }); diff --git a/gitnexus/test/unit/hook-db-lock-probe.test.ts b/gitnexus/test/unit/hook-db-lock-probe.test.ts index c31c214c1..91d035eb0 100644 --- a/gitnexus/test/unit/hook-db-lock-probe.test.ts +++ b/gitnexus/test/unit/hook-db-lock-probe.test.ts @@ -738,7 +738,7 @@ describe.skipIf(!isLinux)('Linux cmdline-first DB-owner scan (#2180)', () => { err.code = code; throw err; } - return (realReaddir as (...a: unknown[]) => unknown)(p, ...rest); + return realReaddir(p, ...rest); }); cleanups.push(() => spy.mockRestore()); // White-box: assert the exported tri-state verdict directly (the diff --git a/gitnexus/test/unit/impact-batching-grouping.test.ts b/gitnexus/test/unit/impact-batching-grouping.test.ts index 7f4d18bd8..d56aee1f5 100644 --- a/gitnexus/test/unit/impact-batching-grouping.test.ts +++ b/gitnexus/test/unit/impact-batching-grouping.test.ts @@ -9,7 +9,7 @@ const executeParameterizedMock = vi.fn(); // imports) and the re-export shim (mcp/core/lbug-adapter.js) so the mocks intercept // regardless of import path. vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, initLbug: vi.fn(), @@ -20,7 +20,7 @@ vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, initLbug: vi.fn(), @@ -31,7 +31,7 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { }; }); -import { LocalBackend } from '../../src/mcp/local/local-backend'; +import { LocalBackend } from '../../src/mcp/local/local-backend.js'; describe('impact: batching and grouping', () => { beforeEach(() => { diff --git a/gitnexus/test/unit/impact-pagination.test.ts b/gitnexus/test/unit/impact-pagination.test.ts index 0a710a71e..2aecb445a 100644 --- a/gitnexus/test/unit/impact-pagination.test.ts +++ b/gitnexus/test/unit/impact-pagination.test.ts @@ -4,7 +4,7 @@ const executeQueryMock = vi.fn(); const executeParameterizedMock = vi.fn(); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, initLbug: vi.fn(), @@ -15,7 +15,7 @@ vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, initLbug: vi.fn(), @@ -26,8 +26,8 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { }; }); -import { LocalBackend } from '../../src/mcp/local/local-backend'; -import { collectImpactSymbolUids } from '../../src/core/group/cross-impact'; +import { LocalBackend } from '../../src/mcp/local/local-backend.js'; +import { collectImpactSymbolUids } from '../../src/core/group/cross-impact.js'; function makeBackend() { const backend = new LocalBackend(); diff --git a/gitnexus/test/unit/impact-pdg-id-bridge-gate.test.ts b/gitnexus/test/unit/impact-pdg-id-bridge-gate.test.ts index 6c08ec43d..0a11e244b 100644 --- a/gitnexus/test/unit/impact-pdg-id-bridge-gate.test.ts +++ b/gitnexus/test/unit/impact-pdg-id-bridge-gate.test.ts @@ -11,7 +11,6 @@ // `npm test`. import { describe, expect, it } from 'vitest'; -// @ts-expect-error — .mjs pure-JS harness module, no types (intentional; build-free). import * as M from '../../bench/impact-pdg/measure.mjs'; interface ReachedItem { diff --git a/gitnexus/test/unit/impact-pdg-id-vs-name-metrics.test.ts b/gitnexus/test/unit/impact-pdg-id-vs-name-metrics.test.ts index 757ee8668..990ade365 100644 --- a/gitnexus/test/unit/impact-pdg-id-vs-name-metrics.test.ts +++ b/gitnexus/test/unit/impact-pdg-id-vs-name-metrics.test.ts @@ -9,7 +9,6 @@ // `node --import tsx bench/impact-pdg/name-collision.mjs`, never in `npm test`. import { describe, expect, it } from 'vitest'; -// @ts-expect-error — .mjs pure-JS module, no types; intentional (build-free harness). import * as M from '../../bench/impact-pdg/name-collision.mjs'; interface ReachedItem { diff --git a/gitnexus/test/unit/impact-pdg-metric-math.test.ts b/gitnexus/test/unit/impact-pdg-metric-math.test.ts index e52051254..43007787b 100644 --- a/gitnexus/test/unit/impact-pdg-metric-math.test.ts +++ b/gitnexus/test/unit/impact-pdg-metric-math.test.ts @@ -9,7 +9,6 @@ // `measure.mjs`, never in `npm test`. import { describe, it, expect } from 'vitest'; -// @ts-expect-error — .mjs pure-JS module, no types; intentional (build-free harness). import * as M from '../../bench/impact-pdg/metrics.mjs'; const k = (sym: string, file = 'src/a.ts') => M.symbolKey(sym, file); diff --git a/gitnexus/test/unit/impact-route-enrichment.test.ts b/gitnexus/test/unit/impact-route-enrichment.test.ts index d2b53031c..a2ffdb901 100644 --- a/gitnexus/test/unit/impact-route-enrichment.test.ts +++ b/gitnexus/test/unit/impact-route-enrichment.test.ts @@ -11,7 +11,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const executeParameterizedMock = vi.fn(); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...(actual as object), initLbug: vi.fn(), @@ -22,7 +22,7 @@ vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...(actual as object), initLbug: vi.fn(), @@ -33,7 +33,7 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { }; }); -import { LocalBackend } from '../../src/mcp/local/local-backend'; +import { LocalBackend } from '../../src/mcp/local/local-backend.js'; const REPO = { id: 'repo', diff --git a/gitnexus/test/unit/incremental-parse-cache.test.ts b/gitnexus/test/unit/incremental-parse-cache.test.ts index fb2916ab4..e37895fd9 100644 --- a/gitnexus/test/unit/incremental-parse-cache.test.ts +++ b/gitnexus/test/unit/incremental-parse-cache.test.ts @@ -26,10 +26,8 @@ const minimalResult = (overrides: Partial = {}): ParseWorkerR nodes: [], relationships: [], symbols: [], - imports: [], calls: [], assignments: [], - heritage: [], routes: [], fetchCalls: [], fetchWrapperDefs: [], @@ -897,15 +895,12 @@ describe('loadParseCache / saveParseCache (round-trip)', () => { ['k2', 'v2'], ]); const innerSet = new Set(['s1', 's2']); - const fake = minimalResult({ - fileCount: 9, - imports: [ - { - typeBindings: innerMap, - extras: innerSet, - } as unknown as ParseWorkerResult['imports'][number], - ], - }); + // Exercise opaque structured-clone data without relying on a removed + // worker field (ParsedFiles intentionally live in a separate store). + const fake = { + ...minimalResult({ fileCount: 9 }), + serializationProbe: { typeBindings: innerMap, extras: innerSet }, + }; const key = 'f'.repeat(64); const cache: ParseCache = { version: PARSE_CACHE_VERSION, @@ -920,17 +915,29 @@ describe('loadParseCache / saveParseCache (round-trip)', () => { expect(names.some((n) => n.endsWith('.json') && n !== 'index.json')).toBe(false); const loaded = await loadParseCacheChunk(cache, key); expect(loaded?.[0]?.fileCount).toBe(9); - const smuggled = loaded?.[0]?.imports[0] as unknown as { - typeBindings?: unknown; - extras?: unknown; - }; + const restored = loaded?.[0]; + if (!restored || !('serializationProbe' in restored)) { + throw new Error('Missing serialized probe'); + } + const smuggled = restored.serializationProbe; + if ( + typeof smuggled !== 'object' || + smuggled === null || + !('typeBindings' in smuggled) || + !('extras' in smuggled) + ) { + throw new Error('Invalid serialized probe'); + } expect(smuggled.typeBindings).toBeInstanceOf(Map); - expect([...(smuggled.typeBindings as Map)]).toEqual([ + expect(smuggled.extras).toBeInstanceOf(Set); + if (!(smuggled.typeBindings instanceof Map) || !(smuggled.extras instanceof Set)) { + throw new Error('Structured-clone collections were not preserved'); + } + expect([...smuggled.typeBindings]).toEqual([ ['k1', 'v1'], ['k2', 'v2'], ]); - expect(smuggled.extras).toBeInstanceOf(Set); - expect([...(smuggled.extras as Set)].sort()).toEqual(['s1', 's2']); + expect([...smuggled.extras].sort()).toEqual(['s1', 's2']); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/gitnexus/test/unit/incremental-write-integrity.test.ts b/gitnexus/test/unit/incremental-write-integrity.test.ts index 14ac9fca9..bd201851a 100644 --- a/gitnexus/test/unit/incremental-write-integrity.test.ts +++ b/gitnexus/test/unit/incremental-write-integrity.test.ts @@ -105,7 +105,11 @@ describe('incremental graph identity before publication', () => { label: 'Tool', properties: { name: '王', filePath: 'src/Å.ts' }, }); - graph.addNode({ id: 'Destination:topic', label: 'Destination', properties: { name: 'topic' } }); + graph.addNode({ + id: 'Destination:topic', + label: 'Destination', + properties: { name: 'topic', filePath: '' }, + }); graph.addNode({ id: 'Function:src/Å.ts:王\u0000\uD800', label: 'Function', @@ -244,7 +248,7 @@ describe('incremental graph identity before publication', () => { const recovered = await runFullAnalysis(repo.dbPath, options, callbacks); expect(recovered.incrementalStats).toBeUndefined(); expect((await loadMeta(storagePath))?.incrementalInProgress).toBeUndefined(); - await adapter.initLbug(lbugPath, { readOnly: true }); + await adapter.initLbug(lbugPath); await expect( reconcileGraphNodeIdentities( recovered.pipelineResult.graph, diff --git a/gitnexus/test/unit/index-repo-command.test.ts b/gitnexus/test/unit/index-repo-command.test.ts index 6fcb6a7ca..88f129927 100644 --- a/gitnexus/test/unit/index-repo-command.test.ts +++ b/gitnexus/test/unit/index-repo-command.test.ts @@ -71,7 +71,7 @@ describe('indexCommand', () => { const storageFailure = ( state: 'empty' | 'owned' | 'unowned', hasCodeIndexDB: boolean, - ): StorageRequirementError => + ): InstanceType => new StorageRequirementError( { repoPath: resolvedRepo, diff --git a/gitnexus/test/unit/ipynb-python-captures.test.ts b/gitnexus/test/unit/ipynb-python-captures.test.ts index e6074a73c..1b886c1a7 100644 --- a/gitnexus/test/unit/ipynb-python-captures.test.ts +++ b/gitnexus/test/unit/ipynb-python-captures.test.ts @@ -45,8 +45,6 @@ describe('Python notebook scope captures', () => { expect(fnCapture?.['@scope.function']?.range.startLine).toBe(expectedJson + 1); const parsed = extractParsedFile(pythonProvider, notebook, 'analysis.ipynb'); expect(parsed).toBeDefined(); - expect(parsed!.localDefs.some((d) => d.qualifiedName === 'train' || d.name === 'train')).toBe( - true, - ); + expect(parsed!.localDefs.some((d) => d.qualifiedName === 'train')).toBe(true); }); }); diff --git a/gitnexus/test/unit/java-runtime-symbol-strategy.test.ts b/gitnexus/test/unit/java-runtime-symbol-strategy.test.ts index bfdc6f7da..d0932decf 100644 --- a/gitnexus/test/unit/java-runtime-symbol-strategy.test.ts +++ b/gitnexus/test/unit/java-runtime-symbol-strategy.test.ts @@ -3,7 +3,7 @@ import type { GraphNode } from 'gitnexus-shared'; import { javaProvider } from '../../src/core/ingestion/languages/java.js'; import { javaRuntimeSymbolStrategy } from '../../src/core/ingestion/languages/java/spring-actuator.js'; -function method(name: string, properties: GraphNode['properties'] = {}): GraphNode { +function method(name: string, properties: Partial = {}): GraphNode { return { id: `method:${name}:${JSON.stringify(properties.parameterTypes)}`, label: 'Method', diff --git a/gitnexus/test/unit/jvm-package-siblings.test.ts b/gitnexus/test/unit/jvm-package-siblings.test.ts index efa912408..bdcede07b 100644 --- a/gitnexus/test/unit/jvm-package-siblings.test.ts +++ b/gitnexus/test/unit/jvm-package-siblings.test.ts @@ -1,4 +1,5 @@ -import type { ParsedFile, ScopeResolutionIndexes, SymbolDefinition } from 'gitnexus-shared'; +import type { ScopeResolutionIndexes } from '../../src/core/ingestion/model/scope-resolution-indexes.js'; +import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared'; import { describe, expect, it } from 'vitest'; import { createJvmPackageSiblingVisibility } from '../../src/core/ingestion/languages/jvm/package-siblings.js'; import { collectJavaCaptureSideChannel } from '../../src/core/ingestion/languages/java/capture-side-channel.js'; diff --git a/gitnexus/test/unit/kotlin-route-const-resolver.test.ts b/gitnexus/test/unit/kotlin-route-const-resolver.test.ts index 1c2cd8b48..c92bddde0 100644 --- a/gitnexus/test/unit/kotlin-route-const-resolver.test.ts +++ b/gitnexus/test/unit/kotlin-route-const-resolver.test.ts @@ -51,7 +51,7 @@ try { } const parser = new Parser(); -if (Kotlin) parser.setLanguage(Kotlin as Parser.Language); +if (Kotlin) parser.setLanguage(Kotlin); const parse = (src: string): Parser.Tree => parser.parse(src); diff --git a/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts b/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts index 51c2225dd..7c98c11d6 100644 --- a/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts +++ b/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts @@ -24,7 +24,7 @@ try { } const parser = new Parser(); -if (Kotlin) parser.setLanguage(Kotlin as Parser.Language); +if (Kotlin) parser.setLanguage(Kotlin); const parse = (source: string): Parser.Tree => parser.parse(source); const describeKotlin = Kotlin ? describe : describe.skip; diff --git a/gitnexus/test/unit/lazy-action.test.ts b/gitnexus/test/unit/lazy-action.test.ts index 973d728ca..fcc6ce3ae 100644 --- a/gitnexus/test/unit/lazy-action.test.ts +++ b/gitnexus/test/unit/lazy-action.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it, vi } from 'vitest'; import { createAnalyzerLbugLazyAction, createLazyAction } from '../../src/cli/lazy-action.js'; const { checkLbugNativeMock } = vi.hoisted(() => ({ - checkLbugNativeMock: vi.fn(() => ({ ok: true })), + checkLbugNativeMock: vi.fn( + () => ({ ok: true }), + ), })); vi.mock('../../src/core/lbug/native-check.js', () => ({ diff --git a/gitnexus/test/unit/lbug-pool-evict-reopen-race.test.ts b/gitnexus/test/unit/lbug-pool-evict-reopen-race.test.ts index 64cd767d5..346d6d742 100644 --- a/gitnexus/test/unit/lbug-pool-evict-reopen-race.test.ts +++ b/gitnexus/test/unit/lbug-pool-evict-reopen-race.test.ts @@ -122,6 +122,8 @@ describe('pool-adapter evict-then-reopen race (fire-and-forget close fix)', () = vi.mocked(createLbugDatabase).mockImplementationOnce(() => ({ init: vi.fn().mockResolvedValue(undefined), + initSync: vi.fn(), + closeSync: vi.fn(), close: vi.fn().mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); order.push('repo1-close-end'); @@ -155,6 +157,8 @@ describe('pool-adapter evict-then-reopen race (fire-and-forget close fix)', () = vi.mocked(createLbugDatabase).mockImplementationOnce(() => ({ init: vi.fn().mockResolvedValue(undefined), + initSync: vi.fn(), + closeSync: vi.fn(), close: vi.fn().mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); order.push('close-end'); diff --git a/gitnexus/test/unit/lbug-pool-pinning.test.ts b/gitnexus/test/unit/lbug-pool-pinning.test.ts index d6fb1dbf8..8276b136a 100644 --- a/gitnexus/test/unit/lbug-pool-pinning.test.ts +++ b/gitnexus/test/unit/lbug-pool-pinning.test.ts @@ -264,6 +264,8 @@ describe('pool-adapter repo pinning (issue #2189)', () => { it('a pin acquired while closeOne awaits db.close() does not survive teardown', async () => { vi.mocked(createLbugDatabase).mockImplementationOnce(() => ({ init: vi.fn().mockResolvedValue(undefined), + initSync: vi.fn(), + closeSync: vi.fn(), close: vi.fn().mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); }), @@ -284,6 +286,8 @@ describe('pool-adapter repo pinning (issue #2189)', () => { it('initLbugWithDb waits for an in-flight closeOne before registering', async () => { vi.mocked(createLbugDatabase).mockImplementationOnce(() => ({ init: vi.fn().mockResolvedValue(undefined), + initSync: vi.fn(), + closeSync: vi.fn(), close: vi.fn().mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); }), @@ -294,6 +298,8 @@ describe('pool-adapter repo pinning (issue #2189)', () => { await Promise.resolve(); const injected = { init: vi.fn().mockResolvedValue(undefined), + initSync: vi.fn(), + closeSync: vi.fn(), close: vi.fn().mockResolvedValue(undefined), }; const injecting = initLbugWithDb( diff --git a/gitnexus/test/unit/local-cli-subprocess.test.ts b/gitnexus/test/unit/local-cli-subprocess.test.ts index 9e46f75f4..2910334fe 100644 --- a/gitnexus/test/unit/local-cli-subprocess.test.ts +++ b/gitnexus/test/unit/local-cli-subprocess.test.ts @@ -56,7 +56,7 @@ describe('Claude CLI subprocess contract', () => { beforeEach(() => { vi.resetModules(); fakeChild = makeFakeChild({ stdout: 'Claude response text' }); - spawnSpy = vi.fn(() => fakeChild.child); + spawnSpy = vi.fn(() => fakeChild.child); }); afterEach(() => { @@ -147,7 +147,7 @@ describe('Claude CLI subprocess contract', () => { it('rejects with exit code and stderr on non-zero exit', async () => { fakeChild = makeFakeChild({ exitCode: 1, stderr: 'auth required' }); - spawnSpy = vi.fn(() => fakeChild.child); + spawnSpy = vi.fn(() => fakeChild.child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('claude 1.0.0'), @@ -162,7 +162,7 @@ describe('Claude CLI subprocess contract', () => { it('rejects with actionable error on empty stdout', async () => { fakeChild = makeFakeChild({ stdout: '' }); - spawnSpy = vi.fn(() => fakeChild.child); + spawnSpy = vi.fn(() => fakeChild.child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('claude 1.0.0'), @@ -183,7 +183,7 @@ describe('Codex CLI subprocess contract', () => { beforeEach(() => { vi.resetModules(); fakeChild = makeFakeChild({ stdout: 'codex response' }); - spawnSpy = vi.fn(() => fakeChild.child); + spawnSpy = vi.fn(() => fakeChild.child); }); afterEach(() => { @@ -435,7 +435,7 @@ describe('Codex output file fallback', () => { vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('codex 0.1.0'), - spawn: vi.fn(() => fakeChild.child), + spawn: vi.fn(() => fakeChild.child), })); const { callCodexLLM } = await import('../../src/core/wiki/local-cli-client.js'); @@ -448,7 +448,7 @@ describe('Codex output file fallback', () => { vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('codex 0.1.0'), - spawn: vi.fn(() => fakeChild.child), + spawn: vi.fn(() => fakeChild.child), })); const { callCodexLLM } = await import('../../src/core/wiki/local-cli-client.js'); @@ -563,7 +563,7 @@ describe('Codex CLI flag contract snapshot', () => { it('spawn args match the exact expected contract (flag rename = test failure)', async () => { const fakeChild = makeFakeChild({ stdout: 'codex output' }); - const spawnSpy = vi.fn(() => fakeChild.child); + const spawnSpy = vi.fn(() => fakeChild.child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('codex 0.1.0'), @@ -608,7 +608,7 @@ describe('Codex CLI flag contract snapshot', () => { it('--model appears before - (stdin marker) and after --output-last-message', async () => { const fakeChild = makeFakeChild({ stdout: 'codex output' }); - const spawnSpy = vi.fn(() => fakeChild.child); + const spawnSpy = vi.fn(() => fakeChild.child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('codex 0.1.0'), @@ -1070,13 +1070,14 @@ describe('Grok CLI timeout', () => { } async function loadGrokWithChild(child: any, execFileSyncImpl?: (...args: any[]) => unknown) { - const spawnSpy = vi.fn(() => child); - const execFileSync = + const spawnSpy = vi.fn(() => child); + const execFileSync = vi.fn( execFileSyncImpl ?? - vi.fn().mockImplementation((cmd: string) => { - if (cmd === 'taskkill') return ''; - return 'grok 1.0.5'; - }); + ((cmd: string) => { + if (cmd === 'taskkill') return ''; + return 'grok 1.0.5'; + }), + ); vi.doMock('../../src/core/logger.js', () => ({ logger: { info: vi.fn(), warn: vi.fn() }, })); diff --git a/gitnexus/test/unit/mcp-context-route-chain.test.ts b/gitnexus/test/unit/mcp-context-route-chain.test.ts index 7cd84ae4d..e2ff0440d 100644 --- a/gitnexus/test/unit/mcp-context-route-chain.test.ts +++ b/gitnexus/test/unit/mcp-context-route-chain.test.ts @@ -18,12 +18,12 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/mcp-read-file-grep.test.ts b/gitnexus/test/unit/mcp-read-file-grep.test.ts index 3f9186011..48f80c52a 100644 --- a/gitnexus/test/unit/mcp-read-file-grep.test.ts +++ b/gitnexus/test/unit/mcp-read-file-grep.test.ts @@ -23,12 +23,12 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/mcp-wal-feedback.test.ts b/gitnexus/test/unit/mcp-wal-feedback.test.ts index 65515a987..4de7c4f75 100644 --- a/gitnexus/test/unit/mcp-wal-feedback.test.ts +++ b/gitnexus/test/unit/mcp-wal-feedback.test.ts @@ -17,12 +17,12 @@ const { lbugMocks, repoMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/method-extraction.test.ts b/gitnexus/test/unit/method-extraction.test.ts index f792cfdce..9945727bd 100644 --- a/gitnexus/test/unit/method-extraction.test.ts +++ b/gitnexus/test/unit/method-extraction.test.ts @@ -49,7 +49,7 @@ try { Dart = requireVendoredGrammar('tree-sitter-dart'); // Verify the grammar actually works with the installed tree-sitter version const testParser = new Parser(); - testParser.setLanguage(Dart as Parser.Language); + testParser.setLanguage(Dart); } catch { Dart = null; } @@ -59,7 +59,7 @@ try { Swift = requireVendoredGrammar('tree-sitter-swift'); // Verify the grammar actually works with the installed tree-sitter version const testParser = new Parser(); - testParser.setLanguage(Swift as Parser.Language); + testParser.setLanguage(Swift); } catch { Swift = null; } @@ -73,7 +73,7 @@ const parseJava = (code: string) => { const parseKotlin = (code: string) => { if (!Kotlin) throw new Error('tree-sitter-kotlin not available'); - parser.setLanguage(Kotlin as Parser.Language); + parser.setLanguage(Kotlin); return parser.parse(code); }; @@ -3783,7 +3783,7 @@ end const parseDart = (code: string) => { if (!Dart) throw new Error('tree-sitter-dart not available'); - parser.setLanguage(Dart as Parser.Language); + parser.setLanguage(Dart); return parser.parse(code); }; @@ -4043,7 +4043,7 @@ extension StringExt on String { // --------------------------------------------------------------------------- const parsePHP = (code: string) => { - parser.setLanguage(PHP.php_only as Parser.Language); + parser.setLanguage(PHP.php_only); return parser.parse(code); }; @@ -4683,7 +4683,7 @@ describe('Rust MethodExtractor', () => { const parseSwift = (code: string) => { if (!Swift) throw new Error('tree-sitter-swift not available'); - parser.setLanguage(Swift as Parser.Language); + parser.setLanguage(Swift); return parser.parse(code); }; diff --git a/gitnexus/test/unit/model/owned-members-lookup.test.ts b/gitnexus/test/unit/model/owned-members-lookup.test.ts index 35ee5d56a..207df6f23 100644 --- a/gitnexus/test/unit/model/owned-members-lookup.test.ts +++ b/gitnexus/test/unit/model/owned-members-lookup.test.ts @@ -224,7 +224,7 @@ describe('Step 2 perf contract', () => { const callScope: Scope = { id: 'scope:method-body', parent: 'scope:module', - kind: 'Method', + kind: 'Function', range: { startLine: 2, startCol: 0, endLine: 99, endCol: 0 }, filePath: 'x.ts', bindings: new Map(), @@ -240,7 +240,9 @@ describe('Step 2 perf contract', () => { scopes: buildScopeTree([moduleScope, callScope]), defs, qualifiedNames: buildQualifiedNameIndex([userClass, saveMethod]), - moduleScopes: buildModuleScopeIndex([moduleScope]), + moduleScopes: buildModuleScopeIndex([ + { filePath: moduleScope.filePath, moduleScopeId: moduleScope.id }, + ]), methodDispatch: buildMethodDispatchIndex({ owners: ['def:User'], computeMro: () => [], diff --git a/gitnexus/test/unit/name-guessed-edges-excluded-from-flows.test.ts b/gitnexus/test/unit/name-guessed-edges-excluded-from-flows.test.ts index d879c0ec9..4bcff43bc 100644 --- a/gitnexus/test/unit/name-guessed-edges-excluded-from-flows.test.ts +++ b/gitnexus/test/unit/name-guessed-edges-excluded-from-flows.test.ts @@ -82,7 +82,9 @@ describe('process tracing excludes name-guessed CALLS edges', () => { const result = await processProcesses(graph, []); expect(result.processes.length).toBeGreaterThan(0); - const tracedNames = new Set(result.steps.map((step) => step.toName)); + const tracedNames = new Set( + result.steps.map((step) => graph.getNode(step.nodeId)?.properties.name), + ); expect(tracedNames.has('unrelatedHelper')).toBe(false); }); }); diff --git a/gitnexus/test/unit/objective-c-provider.test.ts b/gitnexus/test/unit/objective-c-provider.test.ts index c191be2e7..5d65dab27 100644 --- a/gitnexus/test/unit/objective-c-provider.test.ts +++ b/gitnexus/test/unit/objective-c-provider.test.ts @@ -1,3 +1,5 @@ +import type { ObjCFileFacts } from '../../src/core/ingestion/languages/objective-c/facts.js'; +import { finalizeScopeModel } from '../../src/core/ingestion/finalize-orchestrator.js'; import { describe, expect, it } from 'vitest'; import path from 'path'; import Parser from 'tree-sitter'; @@ -504,27 +506,41 @@ static int helper(void) { return 1; } graph.addNode({ id: container.nodeId, label: container.label, - properties: { filePath: facts.filePath, qualifiedName: container.qualifiedName }, + properties: { + name: container.name, + filePath: facts.filePath, + qualifiedName: container.qualifiedName, + }, }); } for (const method of facts.methods) { graph.addNode({ id: method.nodeId, label: 'Method', - properties: { filePath: facts.filePath, qualifiedName: method.qualifiedName }, + properties: { + name: method.name, + filePath: facts.filePath, + qualifiedName: method.qualifiedName, + }, }); } - objectiveCScopeResolver.emitPostResolutionEdges?.(graph, [ - { - filePath: facts.filePath, - moduleScope: 0, - scopes: [], - parsedImports: [], - localDefs: [], - referenceSites: [], - captureSideChannel: { kind: 'objective-c', facts }, - }, - ]); + objectiveCScopeResolver.emitPostResolutionEdges?.( + graph, + [ + { + filePath: facts.filePath, + moduleScope: `scope:${facts.filePath}#module`, + scopes: [], + parsedImports: [], + localDefs: [], + referenceSites: [], + captureSideChannel: { kind: 'objective-c', facts }, + }, + ], + new Map(), + finalizeScopeModel([]), + { fileContents: new Map() }, + ); const go = facts.methods.find( (method) => method.selector === 'go' && method.declarationRole === 'implementation', @@ -779,7 +795,7 @@ static int helper(void) { return 1; } protocols: [] as string[], }; const protocolRunQn = objcMethodQualifiedName(protocolQn, '-', 'run'); - const factsList = [ + const factsList: ObjCFileFacts[] = [ { providerVersion: '0', grammarPackage: 'tree-sitter-objc', @@ -893,7 +909,7 @@ static int helper(void) { return 1; } const graph = createKnowledgeGraph(); const parsedFiles = factsList.map((facts) => ({ filePath: facts.filePath, - moduleScope: 0, + moduleScope: `scope:${facts.filePath}#module`, scopes: [], parsedImports: [], localDefs: [], @@ -904,25 +920,43 @@ static int helper(void) { return 1; } graph.addNode({ id: `File:${facts.filePath}`, label: 'File', - properties: { filePath: facts.filePath, qualifiedName: facts.filePath }, + properties: { + name: facts.filePath, + filePath: facts.filePath, + qualifiedName: facts.filePath, + }, }); for (const container of facts.containers) { graph.addNode({ id: container.nodeId, label: container.label, - properties: { filePath: facts.filePath, qualifiedName: container.qualifiedName }, + properties: { + name: container.name, + filePath: facts.filePath, + qualifiedName: container.qualifiedName, + }, }); } for (const method of facts.methods) { graph.addNode({ id: method.nodeId, label: 'Method', - properties: { filePath: facts.filePath, qualifiedName: method.qualifiedName }, + properties: { + name: method.name, + filePath: facts.filePath, + qualifiedName: method.qualifiedName, + }, }); } } - objectiveCScopeResolver.emitPostResolutionEdges?.(graph, parsedFiles); + objectiveCScopeResolver.emitPostResolutionEdges?.( + graph, + parsedFiles, + new Map(), + finalizeScopeModel([]), + { fileContents: new Map() }, + ); let implementerUses = 0; let receiverUses = 0; diff --git a/gitnexus/test/unit/ops-snapshot.test.ts b/gitnexus/test/unit/ops-snapshot.test.ts index 29fa32a9f..298b823e1 100644 --- a/gitnexus/test/unit/ops-snapshot.test.ts +++ b/gitnexus/test/unit/ops-snapshot.test.ts @@ -27,8 +27,8 @@ describe('serializeOpsJob / summarizeOpsLane', () => { expect(view.durationMs).toBe(5_000); expect(view.lane).toBe('analyze'); expect(view.repoName).toBe('repo'); - expect(view.repoUrl).toBeUndefined(); - expect(view.repoPath).toBeUndefined(); + expect(view).not.toHaveProperty('repoUrl'); + expect(view).not.toHaveProperty('repoPath'); expect(view.branch).toBeUndefined(); }); @@ -72,7 +72,7 @@ describe('serializeOpsJob / summarizeOpsLane', () => { const view = serializeOpsJob(manager.getJob(job.id)!, 'analyze'); expect(view.repoName).toBe('repo'); expect(view.repoName).not.toContain('access_token'); - expect(view.repoUrl).toBeUndefined(); + expect(view).not.toHaveProperty('repoUrl'); }); it('redacts the full repository URL from job.error on the public ops view', () => { diff --git a/gitnexus/test/unit/parse-impl-worker-lazy-cache.test.ts b/gitnexus/test/unit/parse-impl-worker-lazy-cache.test.ts index 216170a2b..9e58cedfb 100644 --- a/gitnexus/test/unit/parse-impl-worker-lazy-cache.test.ts +++ b/gitnexus/test/unit/parse-impl-worker-lazy-cache.test.ts @@ -12,6 +12,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; +import { SupportedLanguages } from '../../src/config/supported-languages.js'; import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; import { runChunkedParseAndResolve } from '../../src/core/ingestion/pipeline-phases/parse-impl.js'; import { computeChunkHash, fileContentHash } from '../../src/storage/parse-cache.js'; @@ -27,16 +28,15 @@ const emptyWorkerResult = (filePath: string, name: string): ParseWorkerResult => filePath, startLine: 1, endLine: 1, - language: 'typescript', + language: SupportedLanguages.TypeScript, + isExported: true, }, }, ], relationships: [], symbols: [], - imports: [], calls: [], assignments: [], - heritage: [], routes: [], fetchCalls: [], fetchWrapperDefs: [], diff --git a/gitnexus/test/unit/parsedfile-store.test.ts b/gitnexus/test/unit/parsedfile-store.test.ts index 7834a9d3c..2f5c3536c 100644 --- a/gitnexus/test/unit/parsedfile-store.test.ts +++ b/gitnexus/test/unit/parsedfile-store.test.ts @@ -26,29 +26,40 @@ import { * Maps — the round-trip's fidelity hinges on those Maps surviving JSON * serialization (they would otherwise collapse to `{}`). */ -const makeParsedFile = (filePath: string): ParsedFile => - ({ - filePath, - moduleScope: `${filePath}:module`, - parsedImports: [], - localDefs: [ - { nodeId: `Function:${filePath}:fn`, filePath, type: 'Function', qualifiedName: 'fn' }, - ], - referenceSites: [], - scopes: [ - { - id: `${filePath}:module`, - parent: null, - kind: 'Module', - range: { startLine: 1, startCol: 0, endLine: 9, endCol: 0 }, - filePath, - bindings: new Map([['fn', [{ defId: `Function:${filePath}:fn`, origin: 'local' }]]]), - ownedDefs: [], - imports: [], - typeBindings: new Map([['x', { name: 'int' }]]), - }, - ], - }) as unknown as ParsedFile; +const makeParsedFile = (filePath: string): ParsedFile => ({ + filePath, + moduleScope: `${filePath}:module`, + parsedImports: [], + localDefs: [ + { nodeId: `Function:${filePath}:fn`, filePath, type: 'Function', qualifiedName: 'fn' }, + ], + referenceSites: [], + scopes: [ + { + id: `${filePath}:module`, + parent: null, + kind: 'Module', + range: { startLine: 1, startCol: 0, endLine: 9, endCol: 0 }, + filePath, + bindings: new Map([ + [ + 'fn', + [ + { + def: { nodeId: `Function:${filePath}:fn`, filePath, type: 'Function' }, + origin: 'local', + }, + ], + ], + ]), + ownedDefs: [], + imports: [], + typeBindings: new Map([ + ['x', { rawName: 'int', declaredAtScope: `${filePath}:module`, source: 'annotation' }], + ]), + }, + ], +}); /** * Store payload with arbitrary (possibly corrupt) field overrides. The one @@ -79,9 +90,9 @@ describe('parsedfile-store', () => { const a = loaded.get('a.c')!; const scope = a.scopes[0]; expect(scope.bindings).toBeInstanceOf(Map); - expect(scope.bindings.get('fn')?.[0]?.defId).toBe('Function:a.c:fn'); + expect(scope.bindings.get('fn')?.[0]?.def.nodeId).toBe('Function:a.c:fn'); expect(scope.typeBindings).toBeInstanceOf(Map); - expect((scope.typeBindings.get('x') as { name: string }).name).toBe('int'); + expect(scope.typeBindings.get('x')?.rawName).toBe('int'); } finally { await rm(dir, { recursive: true, force: true }); } @@ -374,7 +385,7 @@ describe('parsedfile-store', () => { const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c'])); const scope = loaded.get('a.c')!.scopes[0]; expect(scope.bindings).toBeInstanceOf(Map); - expect(scope.bindings.get('fn')?.[0]?.defId).toBe('Function:a.c:fn'); + expect(scope.bindings.get('fn')?.[0]?.def.nodeId).toBe('Function:a.c:fn'); expect(scope.typeBindings).toBeInstanceOf(Map); } finally { await rm(dir, { recursive: true, force: true }); @@ -916,9 +927,12 @@ describe('parsedfile-store receiverChain sanitation', () => { type: 'Function' as const, qualifiedName: 'fn', }; - const pf = makeParsedFile('a.c'); - (pf.localDefs as unknown as object[])[0] = def; - (pf.scopes[0] as { ownedDefs: object[] }).ownedDefs = [def]; + const base = makeParsedFile('a.c'); + const pf: ParsedFile = { + ...base, + localDefs: [def], + scopes: [{ ...base.scopes[0], ownedDefs: [def] }], + }; await persistParsedFileChunk(dir, 'ok', [pf]); const loadedFile = (await loadParsedFilesForPaths(dir, new Set(['a.c']))).get('a.c'); expect(loadedFile).toBeDefined(); diff --git a/gitnexus/test/unit/pdg-callee-id-capture.test.ts b/gitnexus/test/unit/pdg-callee-id-capture.test.ts index ef2804f72..efd1e2aaa 100644 --- a/gitnexus/test/unit/pdg-callee-id-capture.test.ts +++ b/gitnexus/test/unit/pdg-callee-id-capture.test.ts @@ -140,6 +140,7 @@ function makeIndexes( unresolvedEdges: 0, sccCount: 0, largestSccSize: 0, + ambiguousWildcardExports: [], }, }; } diff --git a/gitnexus/test/unit/pdg-impact-engine.test.ts b/gitnexus/test/unit/pdg-impact-engine.test.ts index 3ca021e0b..4d6f3f558 100644 --- a/gitnexus/test/unit/pdg-impact-engine.test.ts +++ b/gitnexus/test/unit/pdg-impact-engine.test.ts @@ -116,6 +116,8 @@ describe('runImpactPDG', () => { }); expect(bfsQueries).toBe(IMPACT_MAX_DEPTH); + expect('truncated' in result).toBe(true); + if (!('truncated' in result)) throw new Error('Expected a successful PDG result'); expect(result.truncated).toBe(true); expect(result.truncatedBy).toBe('depth'); }); diff --git a/gitnexus/test/unit/publish.test.ts b/gitnexus/test/unit/publish.test.ts index fdbccbb47..58230fbe7 100644 --- a/gitnexus/test/unit/publish.test.ts +++ b/gitnexus/test/unit/publish.test.ts @@ -113,7 +113,7 @@ describe('understand-quickly helpers (gitnexus-shared)', () => { describe('publishCommand (no-token no-op)', () => { let tempDir: string; let originalToken: string | undefined; - let exitCodeBefore: number | undefined; + let exitCodeBefore: typeof process.exitCode; beforeEach(async () => { vi.resetModules(); @@ -180,7 +180,7 @@ describe('publishCommand (no-token no-op)', () => { describe('publishCommand response branches (MEDIUM 5)', () => { let tempDir: string; let originalToken: string | undefined; - let exitCodeBefore: number | undefined; + let exitCodeBefore: typeof process.exitCode; let fetchSpy: ReturnType; beforeEach(async () => { diff --git a/gitnexus/test/unit/query-degraded-signal.test.ts b/gitnexus/test/unit/query-degraded-signal.test.ts index f4c9e763c..7e28c5f6a 100644 --- a/gitnexus/test/unit/query-degraded-signal.test.ts +++ b/gitnexus/test/unit/query-degraded-signal.test.ts @@ -41,13 +41,13 @@ vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => { }; }); -import { LocalBackend } from '../../src/mcp/local/local-backend'; +import { LocalBackend } from '../../src/mcp/local/local-backend.js'; import { resetExtensionState } from '../../src/core/lbug/extension-loader.js'; // A backend whose hybrid search yields exactly one matched symbol, so the // enrichment chunk loop runs and can be made to fail. `ftsUsed` is parameterized // so we can exercise the FTS-missing + enrichment-degraded composition. -function makeBackend(ftsUsed = true, nonBenignErrors?: string[]): LocalBackend { +function makeBackend(ftsUsed = true, nonBenignErrors?: string[]) { const backend = new LocalBackend(); const repoHandle = { id: 'repo1', @@ -73,7 +73,7 @@ function makeBackend(ftsUsed = true, nonBenignErrors?: string[]): LocalBackend { .fn() .mockResolvedValue({ results: [sym], ftsUsed, ...(nonBenignErrors && { nonBenignErrors }) }); (backend as any).semanticSearch = vi.fn().mockResolvedValue([]); - return { backend, repoHandle } as any; + return { backend, repoHandle }; } const runQuery = (b: any, params: any = { query: 'x' }) => diff --git a/gitnexus/test/unit/range-binding-parse-timeout.test.ts b/gitnexus/test/unit/range-binding-parse-timeout.test.ts index 7e07d0616..683d88759 100644 --- a/gitnexus/test/unit/range-binding-parse-timeout.test.ts +++ b/gitnexus/test/unit/range-binding-parse-timeout.test.ts @@ -1,5 +1,6 @@ +import type { ScopeResolutionIndexes } from '../../src/core/ingestion/model/scope-resolution-indexes.js'; import { describe, it, expect, afterEach } from 'vitest'; -import type { ParsedFile, ScopeResolutionIndexes } from 'gitnexus-shared'; +import type { ParsedFile } from 'gitnexus-shared'; import { extractParsedFile } from '../../src/core/ingestion/scope-extractor-bridge.js'; import { goScopeResolver } from '../../src/core/ingestion/languages/go/scope-resolver.js'; import { cppScopeResolver } from '../../src/core/ingestion/languages/cpp/scope-resolver.js'; diff --git a/gitnexus/test/unit/rate-limit.test.ts b/gitnexus/test/unit/rate-limit.test.ts index bfb2865df..90e428b25 100644 --- a/gitnexus/test/unit/rate-limit.test.ts +++ b/gitnexus/test/unit/rate-limit.test.ts @@ -119,7 +119,7 @@ describe('createRouteLimiter — integration with a real route', () => { const res = await fetch(`${baseUrl}/test/file`); expect(res.status).toBe(429); const body = await res.json(); - expect(body.error).toContain('Too many'); + expect(body).toHaveProperty('error', expect.stringContaining('Too many')); }); it('emits draft-7 RateLimit response header (combined form), not legacy X-RateLimit-*', async () => { diff --git a/gitnexus/test/unit/repo-manager-ensure-ignore-readonly.test.ts b/gitnexus/test/unit/repo-manager-ensure-ignore-readonly.test.ts index 8e49a01f3..abf620bf4 100644 --- a/gitnexus/test/unit/repo-manager-ensure-ignore-readonly.test.ts +++ b/gitnexus/test/unit/repo-manager-ensure-ignore-readonly.test.ts @@ -13,7 +13,7 @@ const fswCtx = vi.hoisted(() => ({ vi.mock('fs/promises', async (importOriginal) => { const actual = await importOriginal(); - const d = actual.default; + const d = actual; fswCtx.realWrite = d.writeFile.bind(d); fswCtx.writeFileMock.mockImplementation((...args) => fswCtx.realWrite!(...args)); return { diff --git a/gitnexus/test/unit/repo-manager-reconcile.test.ts b/gitnexus/test/unit/repo-manager-reconcile.test.ts index 4e6ab26d6..9d3b1b50c 100644 --- a/gitnexus/test/unit/repo-manager-reconcile.test.ts +++ b/gitnexus/test/unit/repo-manager-reconcile.test.ts @@ -91,9 +91,10 @@ describe('reconcileMetadataFiles', () => { // The dirty flag travels through this file; a reconciliation that // reconstructed a trimmed object instead of carrying fields verbatim // would silently drop it and skip the recovery full-rebuild. - const dirty = metaAt('2026-06-01T00:00:00.000Z', 'crashed-run', { + const dirty = { + ...metaAt('2026-06-01T00:00:00.000Z', 'crashed-run'), incrementalInProgress: true, - } as Partial); + }; await fs.writeFile(path.join(storagePath, 'meta.json'), JSON.stringify(dirty)); await reconcileMetadataFiles(tmpRepo.dbPath); diff --git a/gitnexus/test/unit/repo-manager-rm-failure.test.ts b/gitnexus/test/unit/repo-manager-rm-failure.test.ts index 427cc8367..ab8b8a076 100644 --- a/gitnexus/test/unit/repo-manager-rm-failure.test.ts +++ b/gitnexus/test/unit/repo-manager-rm-failure.test.ts @@ -14,7 +14,7 @@ const fsCtx = vi.hoisted(() => ({ vi.mock('fs/promises', async (importOriginal) => { const actual = await importOriginal(); - const d = actual.default; + const d = actual; fsCtx.realRm = d.rm.bind(d); fsCtx.rmMock.mockImplementation((...args) => fsCtx.realRm!(...args)); return { diff --git a/gitnexus/test/unit/repro-3291-staleness-indexed-ref.test.ts b/gitnexus/test/unit/repro-3291-staleness-indexed-ref.test.ts index fa79f6c85..a58963d31 100644 --- a/gitnexus/test/unit/repro-3291-staleness-indexed-ref.test.ts +++ b/gitnexus/test/unit/repro-3291-staleness-indexed-ref.test.ts @@ -162,9 +162,10 @@ describe('#3291 — tool staleness names the indexed ref', () => { // The graph answer itself is held constant so the only thing that can differ // between the two responses is the freshness signalling under test. - vi.spyOn(backend as unknown as { impact: unknown }, 'impact').mockResolvedValue( - IMPACT_RESULT as never, - ); + vi.spyOn( + backend as unknown as { impact: () => Promise }, + 'impact', + ).mockResolvedValue(IMPACT_RESULT as never); const fromMain = (await backend.callTool('impact', { target: 'doWork', @@ -218,9 +219,10 @@ describe('#3291 — tool staleness names the indexed ref', () => { vi.spyOn(backend, 'selectToolRepository').mockResolvedValue( handleFor(mainClone, twoBack, 'main') as never, ); - vi.spyOn(backend as unknown as { impact: unknown }, 'impact').mockResolvedValue( - IMPACT_RESULT as never, - ); + vi.spyOn( + backend as unknown as { impact: () => Promise }, + 'impact', + ).mockResolvedValue(IMPACT_RESULT as never); const result = (await backend.callTool('impact', { target: 'doWork', @@ -244,9 +246,10 @@ describe('#3291 — tool staleness names the indexed ref', () => { const { branch: _unlabelled, ...unlabelledHandle } = handleFor(mainClone, head, 'main'); vi.spyOn(backend, 'selectToolRepository').mockResolvedValue(unlabelledHandle as never); - vi.spyOn(backend as unknown as { impact: unknown }, 'impact').mockResolvedValue( - IMPACT_RESULT as never, - ); + vi.spyOn( + backend as unknown as { impact: () => Promise }, + 'impact', + ).mockResolvedValue(IMPACT_RESULT as never); const result = (await backend.callTool('impact', { target: 'doWork', diff --git a/gitnexus/test/unit/resolve-enclosing-owner.test.ts b/gitnexus/test/unit/resolve-enclosing-owner.test.ts index 6af25951b..4856d7291 100644 --- a/gitnexus/test/unit/resolve-enclosing-owner.test.ts +++ b/gitnexus/test/unit/resolve-enclosing-owner.test.ts @@ -30,7 +30,7 @@ const parseRuby = (code: string) => { }; const parseKotlin = (code: string) => { - parser.setLanguage(Kotlin as Parser.Language); + parser.setLanguage(Kotlin); return parser.parse(code); }; diff --git a/gitnexus/test/unit/resolve-route-handler-symbols.test.ts b/gitnexus/test/unit/resolve-route-handler-symbols.test.ts index 017a945f8..950a757bc 100644 --- a/gitnexus/test/unit/resolve-route-handler-symbols.test.ts +++ b/gitnexus/test/unit/resolve-route-handler-symbols.test.ts @@ -237,7 +237,7 @@ describe('resolveRouteHandlerSymbols — decorator routes', () => { const owner = model.symbols.add('src/handlers.js', 'auth', 'object:auth', 'Variable'); model.methods.register(owner.nodeId, 'getCurrentUser', { filePath: 'src/handlers.js', - name: 'getCurrentUser', + qualifiedName: 'getCurrentUser', nodeId: 'method:auth.getCurrentUser', type: 'Method', ownerId: owner.nodeId, @@ -281,7 +281,7 @@ describe('resolveRouteHandlerSymbols — decorator routes', () => { model.symbols.add('src/routes.js', 'auth', 'object:auth', 'Variable'); model.methods.register('object:auth', 'getCurrentUser', { filePath: 'src/routes.js', - name: 'getCurrentUser', + qualifiedName: 'getCurrentUser', nodeId: 'method:auth.getCurrentUser', type: 'Method', ownerId: 'object:auth', @@ -326,7 +326,7 @@ describe('resolveRouteHandlerSymbols — decorator routes', () => { model.symbols.add('src/routes.js', 'services', 'object:services', 'Variable'); model.methods.register('object:services', 'getCurrentUser', { filePath: 'src/routes.js', - name: 'getCurrentUser', + qualifiedName: 'getCurrentUser', nodeId: 'method:decoy', type: 'Method', ownerId: 'object:services', diff --git a/gitnexus/test/unit/route-constant-baseline-gate.test.ts b/gitnexus/test/unit/route-constant-baseline-gate.test.ts index eb4919cff..0f4d4db2a 100644 --- a/gitnexus/test/unit/route-constant-baseline-gate.test.ts +++ b/gitnexus/test/unit/route-constant-baseline-gate.test.ts @@ -10,7 +10,6 @@ // `node --import tsx bench/*/measure.mjs --check`, never in `npm test`. import { describe, expect, it } from 'vitest'; -// @ts-expect-error — .mjs pure-JS harness module, no types (intentional; build-free). import * as G from '../../bench/lib/route-constant-guard.mjs'; const collectBaselineErrors = G.collectBaselineErrors as ( diff --git a/gitnexus/test/unit/route-process-linking.test.ts b/gitnexus/test/unit/route-process-linking.test.ts index 46bb69815..57397ec2d 100644 --- a/gitnexus/test/unit/route-process-linking.test.ts +++ b/gitnexus/test/unit/route-process-linking.test.ts @@ -101,7 +101,7 @@ describe('Route → process linking (ENTRY_POINT_OF) re-keying', () => { await processesPhase.execute( makeCtx(graph), - new Map([ + new Map>([ ['structure', phaseResult('structure', { totalFiles: 1 })], ['communities', phaseResult('communities', { communityResult: { memberships: [] } })], ['routes', phaseResult('routes', { routeRegistry })], @@ -168,7 +168,7 @@ describe('Route → process linking (ENTRY_POINT_OF) re-keying', () => { await processesPhase.execute( makeCtx(graph), - new Map([ + new Map>([ ['structure', phaseResult('structure', { totalFiles: 1 })], ['communities', phaseResult('communities', { communityResult: { memberships: [] } })], ['routes', phaseResult('routes', { routeRegistry })], @@ -239,7 +239,7 @@ describe('Route → process linking (ENTRY_POINT_OF) re-keying', () => { await processesPhase.execute( makeCtx(graph), - new Map([ + new Map>([ ['structure', phaseResult('structure', { totalFiles: 1 })], ['communities', phaseResult('communities', { communityResult: { memberships: [] } })], ['routes', phaseResult('routes', { routeRegistry })], diff --git a/gitnexus/test/unit/run-analyze-adopt-failure.test.ts b/gitnexus/test/unit/run-analyze-adopt-failure.test.ts index 7ffb6e6a2..4661b6ce6 100644 --- a/gitnexus/test/unit/run-analyze-adopt-failure.test.ts +++ b/gitnexus/test/unit/run-analyze-adopt-failure.test.ts @@ -130,7 +130,11 @@ describe('fast-path restamp failure modes (#2364 F3)', () => { const logs: string[] = []; rmCtx.adoptMock.mockRejectedValueOnce(new Error('mock adopt failure')); - const first = await runFullAnalysis(tmpRepo.dbPath, {}, { onLog: (m) => logs.push(m) }); + const first = await runFullAnalysis( + tmpRepo.dbPath, + {}, + { onProgress: () => {}, onLog: (m) => logs.push(m) }, + ); expect(first.alreadyUpToDate).toBe(true); expect(logs.some((m) => m.includes('could not restamp the workspace branch label'))).toBe(true); @@ -140,7 +144,7 @@ describe('fast-path restamp failure modes (#2364 F3)', () => { await expect(fs.access(branchMetaDir)).resolves.toBeUndefined(); // …and the next same-commit run retries and completes the whole sync. - const second = await runFullAnalysis(tmpRepo.dbPath, {}, {}); + const second = await runFullAnalysis(tmpRepo.dbPath, {}, { onProgress: () => {} }); expect(second.alreadyUpToDate).toBe(true); const healed = await loadMeta(flatStorage); expect(healed?.branch).toBe('feature/x'); @@ -152,7 +156,7 @@ describe('fast-path restamp failure modes (#2364 F3)', () => { rmCtx.adoptMock.mockClear(); rmCtx.saveMetaMock.mockClear(); - const result = await runFullAnalysis(tmpRepo.dbPath, {}, {}); + const result = await runFullAnalysis(tmpRepo.dbPath, {}, { onProgress: () => {} }); expect(result.alreadyUpToDate).toBe(true); expect(rmCtx.adoptMock).toHaveBeenCalledTimes(1); @@ -171,7 +175,11 @@ describe('fast-path restamp failure modes (#2364 F3)', () => { const logs: string[] = []; rmCtx.saveMetaMock.mockRejectedValueOnce(Object.assign(new Error('mock ro'), { code })); - const result = await runFullAnalysis(tmpRepo.dbPath, {}, { onLog: (m) => logs.push(m) }); + const result = await runFullAnalysis( + tmpRepo.dbPath, + {}, + { onProgress: () => {}, onLog: (m) => logs.push(m) }, + ); expect(result.alreadyUpToDate).toBe(true); expect(logs.some((m) => m.includes('read-only') && m.includes('#1549'))).toBe(true); @@ -187,7 +195,7 @@ describe('fast-path restamp failure modes (#2364 F3)', () => { Object.assign(new Error('mock read-only storage'), { code: 'EROFS' }), ); - const result = await runFullAnalysis(tmpRepo.dbPath, {}, {}); + const result = await runFullAnalysis(tmpRepo.dbPath, {}, { onProgress: () => {} }); expect(result.alreadyUpToDate).toBe(true); expect(rmCtx.writableMock).not.toHaveBeenCalled(); @@ -199,7 +207,9 @@ describe('fast-path restamp failure modes (#2364 F3)', () => { const readOnly = Object.assign(new Error('mock read-only storage'), { code: 'EROFS' }); rmCtx.writableMock.mockRejectedValueOnce(readOnly); - await expect(runFullAnalysis(tmpRepo.dbPath, {}, {})).rejects.toBe(readOnly); + await expect(runFullAnalysis(tmpRepo.dbPath, {}, { onProgress: () => {} })).rejects.toBe( + readOnly, + ); expect(rmCtx.writableMock).toHaveBeenCalledWith(flatStorage); }); }); diff --git a/gitnexus/test/unit/run-analyze-fts-crash-marker.test.ts b/gitnexus/test/unit/run-analyze-fts-crash-marker.test.ts index b372fb8dd..a68f4a5ae 100644 --- a/gitnexus/test/unit/run-analyze-fts-crash-marker.test.ts +++ b/gitnexus/test/unit/run-analyze-fts-crash-marker.test.ts @@ -99,7 +99,7 @@ const fileGraph = () => { graph.addNode({ id: 'file:src/a.ts', label: 'File', - properties: { filePath: REL_FILE }, + properties: { name: REL_FILE, filePath: REL_FILE }, }); return graph; }; @@ -117,7 +117,7 @@ const mockLbugAdapter = async () => { // that identity instead of trusting the synthetic node count alone. executeQuery: vi.fn(async (query: string) => query.startsWith('MATCH (n:`File`) RETURN n.id AS id') - ? [{ id: 'file:src/a.ts', name: '', filePath: REL_FILE }] + ? [{ id: 'file:src/a.ts', name: REL_FILE, filePath: REL_FILE }] : [], ), executeWithReusedStatement: vi.fn(async () => []), diff --git a/gitnexus/test/unit/run-analyze-fts-repair.test.ts b/gitnexus/test/unit/run-analyze-fts-repair.test.ts index b60cd60f1..452166590 100644 --- a/gitnexus/test/unit/run-analyze-fts-repair.test.ts +++ b/gitnexus/test/unit/run-analyze-fts-repair.test.ts @@ -318,7 +318,17 @@ describe('runFullAnalysis FTS repair and verification failure paths', () => { indexedAt: seededIndexedAt, stats: { files: 7, nodes: 42, edges: 10 }, runnerIdentity: { - source: { kind: 'source' as const, digest: 'src-digest' }, + schemaVersion: 4, + runtime: { + executablePath: '/usr/bin/node', + version: 'v24.11.0', + platform: 'linux', + architecture: 'x64', + modulesAbi: '137', + libc: 'glibc', + }, + cliVersion: '1.0.0', + invokedArtifact: { path: '/x/cli/index.ts', digest: 'src-digest' }, build: { kind: 'source' as const, rootPath: '/x', @@ -1108,7 +1118,9 @@ describe('runFullAnalysis wipe-and-restore vector-index stamp (tri-review 466951 properties: { filePath: 'src/app.ts' }, }; const buildVectorIndex = vi.fn(async () => false); - const executeWithReusedStatement = vi.fn(async () => []); + const executeWithReusedStatement = vi.fn< + typeof import('../../src/core/lbug/lbug-adapter.js').executeWithReusedStatement + >(async () => {}); vi.doMock('../../src/core/lbug/lbug-adapter.js', () => ({ initLbug: vi.fn(async () => undefined), loadGraphToLbug: vi.fn(async () => undefined), @@ -1239,7 +1251,9 @@ describe('runFullAnalysis wipe-and-restore vector-index stamp (tri-review 466951 name: 'handler', properties: { filePath: 'src/app.ts' }, }; - const executeWithReusedStatement = vi.fn(async () => []); + const executeWithReusedStatement = vi.fn< + typeof import('../../src/core/lbug/lbug-adapter.js').executeWithReusedStatement + >(async () => {}); vi.doMock('../../src/core/lbug/lbug-adapter.js', () => ({ initLbug: vi.fn(async () => undefined), loadGraphToLbug: vi.fn(async () => undefined), @@ -3073,6 +3087,7 @@ describe('runFullAnalysis embedding-checkpoint resilience (#2790 review)', () => const adapter = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(adapter.initLbug).mockImplementation(async (dbPath) => { if (dbPath.includes('.staging.')) await fs.writeFile(dbPath, 'staged fixture'); + return { db: null, conn: null }; }); vi.mocked(adapter.wipeLbugDbFiles).mockImplementation(async (dbPath) => { await fs.rm(dbPath, { force: true }); @@ -3325,6 +3340,7 @@ describe('runFullAnalysis embedding-checkpoint resilience (#2790 review)', () => const adapter = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(adapter.loadGraphToLbug).mockImplementation(async () => { await fs.writeFile(lbugPath, 'in-place replacement'); + return { success: true, insertedRels: 0, skippedRels: 0, warnings: [] }; }); // Import on the host first, then choose the Windows default in-place // branch. The native adapter is mocked; source files and lock cleanup are real. @@ -3629,6 +3645,7 @@ describe('runFullAnalysis embedding-checkpoint resilience (#2790 review)', () => const adapter = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(adapter.initLbug).mockImplementation(async (dbPath) => { if (dbPath.includes('.staging.')) await fs.writeFile(dbPath, 'staged fixture'); + return { db: null, conn: null }; }); vi.mocked(adapter.wipeLbugDbFiles).mockImplementation(async (dbPath) => { await fs.rm(dbPath, { force: true }); @@ -3670,6 +3687,7 @@ describe('runFullAnalysis embedding-checkpoint resilience (#2790 review)', () => const adapter = await import('../../src/core/lbug/lbug-adapter.js'); vi.mocked(adapter.initLbug).mockImplementation(async (dbPath) => { if (dbPath.includes('.staging.')) await fs.writeFile(dbPath, 'staged fixture'); + return { db: null, conn: null }; }); vi.mocked(adapter.wipeLbugDbFiles).mockImplementation(async (dbPath) => { await fs.rm(dbPath, { force: true }); diff --git a/gitnexus/test/unit/scope-resolution/c/c-workspace.test.ts b/gitnexus/test/unit/scope-resolution/c/c-workspace.test.ts index 732f01b46..9134a0289 100644 --- a/gitnexus/test/unit/scope-resolution/c/c-workspace.test.ts +++ b/gitnexus/test/unit/scope-resolution/c/c-workspace.test.ts @@ -462,7 +462,7 @@ describe('C/C++ monorepo config', () => { targetRaw: string, fromFile: string, workspace: ReadonlySet, - ): string | null { + ): ReturnType { return cScopeResolver.resolveImportTarget( targetRaw, fromFile, diff --git a/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts b/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts index 9c0ffcea1..9de8ac5f3 100644 --- a/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts +++ b/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts @@ -98,6 +98,7 @@ function indexes(scopes: readonly Scope[]): ScopeResolutionIndexes { unresolvedEdges: 0, sccCount: 1, largestSccSize: 1, + ambiguousWildcardExports: [], }, }; } @@ -115,8 +116,8 @@ function operand(name: string, line: number): CallableFlowOperand { function runReverseChain(length: number) { const moduleBindings = new Map([ - ['target', [{ def: targetDef }]], - ['entry', [{ def: entryDef }]], + ['target', [{ def: targetDef, origin: 'local' }]], + ['entry', [{ def: entryDef, origin: 'local' }]], ]); const scopes = [ scope(MODULE, null, 'Module', [targetDef, entryDef], moduleBindings), diff --git a/gitnexus/test/unit/scope-resolution/construction-syntax-wiring.test.ts b/gitnexus/test/unit/scope-resolution/construction-syntax-wiring.test.ts index 3729f3937..962aea08e 100644 --- a/gitnexus/test/unit/scope-resolution/construction-syntax-wiring.test.ts +++ b/gitnexus/test/unit/scope-resolution/construction-syntax-wiring.test.ts @@ -12,6 +12,7 @@ */ import { describe, it, expect } from 'vitest'; +import { SupportedLanguages } from 'gitnexus-shared'; import { SCOPE_RESOLVERS } from '../../../src/core/ingestion/scope-resolution/pipeline/registry.js'; describe('constructionSyntax wiring inventory', () => { @@ -33,7 +34,13 @@ describe('constructionSyntax wiring inventory', () => { it('leaves the languages that already resolve the shape unwired', () => { // Java resolves it via the #2564 object_creation_expression capture // rewrite; php/swift/dart/kotlin via their own capture-side paths. - for (const language of ['java', 'php', 'swift', 'dart', 'kotlin'] as const) { + for (const language of [ + SupportedLanguages.Java, + SupportedLanguages.PHP, + SupportedLanguages.Swift, + SupportedLanguages.Dart, + SupportedLanguages.Kotlin, + ] as const) { const resolver = SCOPE_RESOLVERS.get(language); expect(resolver, `${language} resolver is registered`).toBeDefined(); expect(resolver!.constructionSyntax, `${language} stays unwired`).toBeUndefined(); diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts index 032b8b0eb..00797e0d2 100644 --- a/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts @@ -232,7 +232,7 @@ describe('Tier-A predicate registry', () => { type: 'Function', templateConstraints: payload, ...(opts.parameterTypeClasses !== undefined - ? { parameterTypeClasses: opts.parameterTypeClasses } + ? { parameterTypeClasses: [...opts.parameterTypeClasses] } : {}), }; return cppConstraintCompatibility({ arity: argumentTypes.length }, def, { diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-qualified-ns-index.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-qualified-ns-index.test.ts index de7fd9f06..098c832a4 100644 --- a/gitnexus/test/unit/scope-resolution/cpp/cpp-qualified-ns-index.test.ts +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-qualified-ns-index.test.ts @@ -1,3 +1,4 @@ +import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js'; /** * #2788 — `resolveCppQualifiedNamespaceMember` serves qualified `ns::member()` * lookups from a per-pipeline index instead of rescanning every parsed file per @@ -15,12 +16,7 @@ * This is the failure mode the index introduces; nothing else covers it. */ -import type { - ParsedFile, - ScopeId, - ScopeResolutionIndexes, - SymbolDefinition, -} from 'gitnexus-shared'; +import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared'; import { beforeEach, describe, expect, it } from 'vitest'; import { clearCppInlineNamespaces, diff --git a/gitnexus/test/unit/scope-resolution/emit-references.test.ts b/gitnexus/test/unit/scope-resolution/emit-references.test.ts index c88dc998e..097ac269a 100644 --- a/gitnexus/test/unit/scope-resolution/emit-references.test.ts +++ b/gitnexus/test/unit/scope-resolution/emit-references.test.ts @@ -99,6 +99,12 @@ function makeIndexes(scopes: Scope[], allDefs: SymbolDefinition[]): ScopeResolut }), imports: new Map(), bindings: new Map(), + bindingAugmentations: new Map(), + workspaceFqnBindings: new Map(), + workspaceTypeBindings: new Map(), + namespaceFqnBindings: new Map(), + namespaceTypeBindings: new Map(), + accessibleNamespacesByScope: new Map(), referenceSites: [], sccs: [], stats: { @@ -108,6 +114,7 @@ function makeIndexes(scopes: Scope[], allDefs: SymbolDefinition[]): ScopeResolut unresolvedEdges: 0, sccCount: 0, largestSccSize: 0, + ambiguousWildcardExports: [], }, }; } diff --git a/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts b/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts index 45eaaebd6..8972ba40e 100644 --- a/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts +++ b/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest'; import { emitTsScopeCaptures } from '../../../src/core/ingestion/languages/typescript/captures.js'; import { emitJsScopeCaptures } from '../../../src/core/ingestion/languages/javascript/captures.js'; import { extract } from '../../../src/core/ingestion/scope-extractor.js'; -import { typescriptScopeResolver } from '../../../src/core/ingestion/languages/typescript/scope-resolver.js'; +import { typescriptProvider } from '../../../src/core/ingestion/languages/typescript.js'; type Emit = typeof emitTsScopeCaptures; @@ -309,7 +309,7 @@ describe('SymbolDefinition.isExported through the extractor', () => { const esm = extract( emitTsScopeCaptures('export function a() {}\nfunction b() {}\n', 'x.ts'), 'x.ts', - typescriptScopeResolver, + typescriptProvider, ); const byName = new Map(esm.localDefs.map((d) => [d.qualifiedName, d.isExported])); expect(byName.get('a')).toBe(true); @@ -317,7 +317,7 @@ describe('SymbolDefinition.isExported through the extractor', () => { const cjs = extract( emitTsScopeCaptures('function a() {}\nmodule.exports = a;\n', 'y.ts'), 'y.ts', - typescriptScopeResolver, + typescriptProvider, ); expect(cjs.localDefs.find((d) => d.qualifiedName === 'a')?.isExported).toBeUndefined(); expect('isExported' in cjs.localDefs.find((d) => d.qualifiedName === 'a')!).toBe(false); diff --git a/gitnexus/test/unit/scope-resolution/go/go-hooks.test.ts b/gitnexus/test/unit/scope-resolution/go/go-hooks.test.ts index 5b044246c..9d06c5054 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-hooks.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-hooks.test.ts @@ -1,4 +1,11 @@ import { describe, expect, it } from 'vitest'; +import { + buildDefIndex, + buildQualifiedNameIndex, + buildScopeTree, + buildMethodDispatchIndex, + buildModuleScopeIndex, +} from 'gitnexus-shared'; import type { BindingRef, Callsite, @@ -30,10 +37,6 @@ describe('Go arity compatibility', () => { it('returns unknown when no param count info', () => { const def = makeDef(); const callsite: Callsite = { - name: 'F', - inScope: 's', - atRange: { startLine: 1, startCol: 1, endLine: 1, endCol: 5 }, - kind: 'call', arity: 1, }; expect(goArityCompatibility(def, callsite)).toBe('unknown'); @@ -42,10 +45,6 @@ describe('Go arity compatibility', () => { it('exact match is compatible', () => { const def = makeDef({ parameterCount: 2, requiredParameterCount: 2 }); const callsite: Callsite = { - name: 'F', - inScope: 's', - atRange: { startLine: 1, startCol: 1, endLine: 1, endCol: 5 }, - kind: 'call', arity: 2, }; expect(goArityCompatibility(def, callsite)).toBe('compatible'); @@ -54,10 +53,6 @@ describe('Go arity compatibility', () => { it('too few args is incompatible', () => { const def = makeDef({ parameterCount: 2, requiredParameterCount: 2 }); const callsite: Callsite = { - name: 'F', - inScope: 's', - atRange: { startLine: 1, startCol: 1, endLine: 1, endCol: 5 }, - kind: 'call', arity: 1, }; expect(goArityCompatibility(def, callsite)).toBe('incompatible'); @@ -70,10 +65,6 @@ describe('Go arity compatibility', () => { parameterTypes: ['string', '...string'], }); const callsite: Callsite = { - name: 'F', - inScope: 's', - atRange: { startLine: 1, startCol: 1, endLine: 1, endCol: 5 }, - kind: 'call', arity: 5, }; expect(goArityCompatibility(def, callsite)).toBe('compatible'); @@ -82,10 +73,6 @@ describe('Go arity compatibility', () => { it('non-variadic rejects extra args', () => { const def = makeDef({ parameterCount: 2, requiredParameterCount: 2 }); const callsite: Callsite = { - name: 'F', - inScope: 's', - atRange: { startLine: 1, startCol: 1, endLine: 1, endCol: 5 }, - kind: 'call', arity: 3, }; expect(goArityCompatibility(def, callsite)).toBe('incompatible'); @@ -222,32 +209,40 @@ function scopeIndexes( readonly imports?: ReadonlyMap; } = {}, ): ScopeResolutionIndexes { - const defsById = new Map(defs.map((def) => [def.nodeId, def])); - const qualifiedNames = new Map(); - for (const def of defs) { - const ids = qualifiedNames.get(def.qualifiedName) ?? []; - ids.push(def.nodeId); - qualifiedNames.set(def.qualifiedName, ids); - } - const scopesById = new Map(scopes.map((s) => [s.id, s])); return { - defs: { get: (id: string) => defsById.get(id) }, - qualifiedNames: { get: (name: string) => qualifiedNames.get(name) ?? [] }, - scopeTree: { getScope: (id: ScopeId) => scopesById.get(id) }, + defs: buildDefIndex(defs), + qualifiedNames: buildQualifiedNameIndex(defs), + scopeTree: buildScopeTree([...scopes]), bindings: new Map(), bindingAugmentations: options.bindingAugmentations ?? new Map(), imports: options.imports ?? new Map(), workspaceFqnBindings: new Map(), namespaceFqnBindings: new Map(), accessibleNamespacesByScope: new Map(), - methodDispatch: {} as any, - moduleScopes: {} as any, + methodDispatch: buildMethodDispatchIndex({ + owners: [], + computeMro: () => [], + implementsOf: () => [], + }), + moduleScopes: buildModuleScopeIndex( + scopes + .filter((scope) => scope.kind === 'Module') + .map((scope) => ({ filePath: scope.filePath, moduleScopeId: scope.id })), + ), workspaceTypeBindings: new Map(), namespaceTypeBindings: new Map(), referenceSites: [], sccs: [], - stats: {} as any, - } as ScopeResolutionIndexes; + stats: { + totalFiles: new Set(scopes.map((scope) => scope.filePath)).size, + totalEdges: 0, + linkedEdges: 0, + unresolvedEdges: 0, + sccCount: 0, + largestSccSize: 0, + ambiguousWildcardExports: [], + }, + }; } const emptyIndexes = scopeIndexes([]); @@ -257,13 +252,19 @@ function scope( kind: Scope['kind'], ownedDefs: readonly SymbolDefinition[], parent: ScopeId | null = null, + line = 1, ): Scope { return { id, parent, kind, filePath: 'repo.go', - range: { startLine: 1, startCol: 0, endLine: 1, endCol: 1 }, + range: { + startLine: kind === 'Module' ? 0 : line, + startCol: 0, + endLine: kind === 'Module' ? 100 : line, + endCol: 1, + }, bindings: new Map(), ownedDefs, imports: [], @@ -629,7 +630,11 @@ describe('Go structural interface detection', () => { returnType: 'error', }); const defs = [reader, base, file, readerRead, baseRead]; - const scopes = [scope('scope:Base', 'Class', [base]), scope('scope:File', 'Class', [file])]; + const scopes = [ + scope('scope:module', 'Module', []), + scope('scope:Base', 'Class', [base], 'scope:module', 1), + scope('scope:File', 'Class', [file], 'scope:module', 2), + ]; const result = detectGoInterfaceImplementations( parsedGoDefs(defs, { @@ -671,8 +676,9 @@ describe('Go structural interface detection', () => { ); const defs = [reader, base, shadowFile, readerRead, baseRead, shadowRead]; const scopes = [ - scope('scope:Base', 'Class', [base]), - scope('scope:ShadowFile', 'Class', [shadowFile]), + scope('scope:module', 'Module', []), + scope('scope:Base', 'Class', [base], 'scope:module', 1), + scope('scope:ShadowFile', 'Class', [shadowFile], 'scope:module', 2), ]; const result = detectGoInterfaceImplementations( @@ -709,9 +715,10 @@ describe('Go structural interface detection', () => { }); const defs = [reader, baseA, baseB, file, readerRead, baseARead, baseBRead]; const scopes = [ - scope('scope:BaseA', 'Class', [baseA]), - scope('scope:BaseB', 'Class', [baseB]), - scope('scope:File', 'Class', [file]), + scope('scope:module', 'Module', []), + scope('scope:BaseA', 'Class', [baseA], 'scope:module', 1), + scope('scope:BaseB', 'Class', [baseB], 'scope:module', 2), + scope('scope:File', 'Class', [file], 'scope:module', 3), ]; const result = detectGoInterfaceImplementations( @@ -749,10 +756,11 @@ describe('Go structural interface detection', () => { }); const defs = [reader, shallow, deepBase, deepWrapper, file, readerRead, shallowRead, deepRead]; const scopes = [ - scope('scope:Shallow', 'Class', [shallow]), - scope('scope:DeepBase', 'Class', [deepBase]), - scope('scope:DeepWrapper', 'Class', [deepWrapper]), - scope('scope:File', 'Class', [file]), + scope('scope:module', 'Module', []), + scope('scope:Shallow', 'Class', [shallow], 'scope:module', 1), + scope('scope:DeepBase', 'Class', [deepBase], 'scope:module', 2), + scope('scope:DeepWrapper', 'Class', [deepWrapper], 'scope:module', 3), + scope('scope:File', 'Class', [file], 'scope:module', 4), ]; const result = detectGoInterfaceImplementations( @@ -846,9 +854,9 @@ describe('Go structural interface detection', () => { ]; const scopes = [ scope('scope:module', 'Module', []), - scope('scope:ReaderA', 'Class', [readerA], 'scope:module'), - scope('scope:ReaderB', 'Class', [readerB], 'scope:module'), - scope('scope:ReadCloser', 'Class', [readCloser], 'scope:module'), + scope('scope:ReaderA', 'Class', [readerA], 'scope:module', 1), + scope('scope:ReaderB', 'Class', [readerB], 'scope:module', 2), + scope('scope:ReadCloser', 'Class', [readCloser], 'scope:module', 3), ]; const result = detectGoInterfaceImplementations( diff --git a/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts b/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts index 6c8c28561..6cbbea6d3 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts @@ -42,7 +42,9 @@ describe('Go import decomposition', () => { ])('uses the same package qualifier for %s as route extraction', (spec, expected) => { const matches = parseThenSplit(`import ${spec}`); expect(matches[0]['@import.name']?.text).toBe(expected); - expect(interpretGoImport(matches[0])?.localName).toBe(expected); + const parsed = interpretGoImport(matches[0]); + expect(parsed).not.toBeNull(); + expect(parsed && 'localName' in parsed ? parsed.localName : undefined).toBe(expected); }); it('decomposes grouped imports', () => { diff --git a/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts b/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts index edf48b33a..93f7d3f43 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts @@ -1,8 +1,10 @@ +import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js'; import { describe, it, expect } from 'vitest'; import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js'; import { goScopeResolver } from '../../../../src/core/ingestion/languages/go/scope-resolver.js'; import { populateGoRangeBindings } from '../../../../src/core/ingestion/languages/go/range-binding.js'; -import type { ParsedFile, ScopeResolutionIndexes } from 'gitnexus-shared'; +import type { ParsedFile } from 'gitnexus-shared'; +import { finalizeScopeModel } from '../../../../src/core/ingestion/finalize-orchestrator.js'; function parseGo(src: string, path = 'main.go'): ParsedFile { const p = extractParsedFile(goScopeResolver.languageProvider, src, path); @@ -12,13 +14,7 @@ function parseGo(src: string, path = 'main.go'): ParsedFile { } function makeEmptyIndexes(): ScopeResolutionIndexes { - return { - bindings: new Map(), - imports: [], - scopeTree: { roots: [] } as any, - methodDispatch: new Map(), - sccs: [], - } as ScopeResolutionIndexes; + return finalizeScopeModel([]); } describe('Go range binding — null guard (#1346, #1366)', () => { diff --git a/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts b/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts index bc8384257..e029c4260 100644 --- a/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts +++ b/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts @@ -848,6 +848,8 @@ describe('import-target index hoist — built once per file set, not once per im filePath, bindings: new Map([[name, [{ def, origin: 'local' }]]]), ownedDefs: [def], + imports: [], + typeBindings: new Map(), }, ], parsedImports: [], diff --git a/gitnexus/test/unit/scope-resolution/node-lookup-determinism.test.ts b/gitnexus/test/unit/scope-resolution/node-lookup-determinism.test.ts index 976247da4..62a6763f2 100644 --- a/gitnexus/test/unit/scope-resolution/node-lookup-determinism.test.ts +++ b/gitnexus/test/unit/scope-resolution/node-lookup-determinism.test.ts @@ -1,3 +1,4 @@ +import { SupportedLanguages } from 'gitnexus-shared'; import type { NodeLabel } from 'gitnexus-shared'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -25,20 +26,28 @@ interface Candidate { 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 nodes = candidates.map((candidate) => { + const node = { + id: candidate.id, + label: candidate.label ?? ('Method' as NodeLabel), + properties: { + name: candidate.name ?? 'save', + qualifiedName: candidate.qualifiedName ?? 'Service.save', + filePath: FILE, + startLine: candidate.startLine, + endLine: candidate.startLine, + language: SupportedLanguages.TypeScript, + isExported: false, + ...(candidate.startColumn !== undefined ? { startColumn: candidate.startColumn } : {}), + }, + } satisfies ParseWorkerResult['nodes'][number]; + // Missing positions deliberately exercise legacy worker-result tolerance. + if (candidate.startLine === undefined) { + Reflect.deleteProperty(node.properties, 'startLine'); + Reflect.deleteProperty(node.properties, 'endLine'); + } + return node; + }); const result: ParseWorkerResult = { nodes, relationships: [], diff --git a/gitnexus/test/unit/scope-resolution/php-import-target-parity.test.ts b/gitnexus/test/unit/scope-resolution/php-import-target-parity.test.ts index ef3238706..25118b542 100644 --- a/gitnexus/test/unit/scope-resolution/php-import-target-parity.test.ts +++ b/gitnexus/test/unit/scope-resolution/php-import-target-parity.test.ts @@ -323,10 +323,14 @@ function parsedFilesFor(testCase: Case): readonly ParsedFile[] { (filePath) => ({ filePath, + moduleScope: `scope:${filePath}#module`, + scopes: [], + parsedImports: [], + referenceSites: [], localDefs: (testCase.defs?.get(filePath) ?? []).map(([type, name]) => definition(filePath, type, name), ), - }) as ParsedFile, + }) satisfies ParsedFile, ); } diff --git a/gitnexus/test/unit/scope-resolution/property-name-index.test.ts b/gitnexus/test/unit/scope-resolution/property-name-index.test.ts index ec08650b9..a079bf494 100644 --- a/gitnexus/test/unit/scope-resolution/property-name-index.test.ts +++ b/gitnexus/test/unit/scope-resolution/property-name-index.test.ts @@ -56,11 +56,13 @@ describe('buildPropertyNameIndex', () => { label: 'Function', properties: { name: 'run', filePath: 'a.js', startLine: 1, endLine: 2 }, }); - graph.addNode({ + const noPathNode = { id: 'Property:a.js:noPath', - label: 'Property', - properties: { name: 'noPath', startLine: 1, endLine: 1 }, - }); + label: 'Property' as const, + properties: { name: 'noPath', filePath: '', startLine: 1, endLine: 1 }, + }; + Reflect.deleteProperty(noPathNode.properties, 'filePath'); + graph.addNode(noPathNode); const index = buildPropertyNameIndex(graph); expect(index.get('run')).toBeUndefined(); diff --git a/gitnexus/test/unit/scope-resolution/python/python-hooks.test.ts b/gitnexus/test/unit/scope-resolution/python/python-hooks.test.ts index 8ffadb3f2..9723253b5 100644 --- a/gitnexus/test/unit/scope-resolution/python/python-hooks.test.ts +++ b/gitnexus/test/unit/scope-resolution/python/python-hooks.test.ts @@ -216,10 +216,11 @@ describe('pythonMissingReceiverSubtypeDecision', () => { it('declines class receivers without suppressing their ordinary resolution path', () => { expect( - pythonMissingReceiverSubtypeDecision( - { ...selfType, source: 'cls' }, - { receiverBindingIsStatic: true, memberName: 'hook', callArity: 0 }, - ), + pythonMissingReceiverSubtypeDecision(selfType, { + receiverBindingIsStatic: true, + memberName: 'hook', + callArity: 0, + }), ).toBe(false); }); }); diff --git a/gitnexus/test/unit/scope-resolution/reconcile-ownership.test.ts b/gitnexus/test/unit/scope-resolution/reconcile-ownership.test.ts index 497b77db9..afeb935ce 100644 --- a/gitnexus/test/unit/scope-resolution/reconcile-ownership.test.ts +++ b/gitnexus/test/unit/scope-resolution/reconcile-ownership.test.ts @@ -47,7 +47,7 @@ const mkProperty = (opts: { filePath: string; name: string; ownerId: string; - type?: 'Property' | 'Variable'; + type?: 'Property' | 'Variable' | 'Const' | 'Static'; }): SymbolDefinition => ({ nodeId: opts.nodeId, filePath: opts.filePath, diff --git a/gitnexus/test/unit/scope-resolution/references-to-edges-call-form.test.ts b/gitnexus/test/unit/scope-resolution/references-to-edges-call-form.test.ts index 99cf0ad91..51d318553 100644 --- a/gitnexus/test/unit/scope-resolution/references-to-edges-call-form.test.ts +++ b/gitnexus/test/unit/scope-resolution/references-to-edges-call-form.test.ts @@ -97,6 +97,7 @@ function makeIndexes(scope: Scope, allDefs: readonly SymbolDefinition[]): ScopeR unresolvedEdges: 0, sccCount: 0, largestSccSize: 0, + ambiguousWildcardExports: [], }, }; } diff --git a/gitnexus/test/unit/scope-resolution/resolve-references.test.ts b/gitnexus/test/unit/scope-resolution/resolve-references.test.ts index e5524d9e6..670657cee 100644 --- a/gitnexus/test/unit/scope-resolution/resolve-references.test.ts +++ b/gitnexus/test/unit/scope-resolution/resolve-references.test.ts @@ -79,6 +79,11 @@ function makeIndexes( }), imports: new Map(), bindings: new Map(), + workspaceFqnBindings: new Map(), + workspaceTypeBindings: new Map(), + namespaceFqnBindings: new Map(), + namespaceTypeBindings: new Map(), + accessibleNamespacesByScope: new Map(), bindingAugmentations: new Map(), referenceSites, sccs: [], @@ -89,6 +94,7 @@ function makeIndexes( unresolvedEdges: 0, sccCount: 0, largestSccSize: 0, + ambiguousWildcardExports: [], }, }; } diff --git a/gitnexus/test/unit/scope-resolution/resolved-callee-names.test.ts b/gitnexus/test/unit/scope-resolution/resolved-callee-names.test.ts index 20a81a8e2..3ba7e5647 100644 --- a/gitnexus/test/unit/scope-resolution/resolved-callee-names.test.ts +++ b/gitnexus/test/unit/scope-resolution/resolved-callee-names.test.ts @@ -21,7 +21,13 @@ describe('collectResolvedCalleeNames', () => { fn('b', 'b', 'src/b.go'); fn('c', 'c', 'src/c.ts'); fn('d', 'd', 'src/d.go'); - g.addNode({ id: 'file', label: 'File' as NodeLabel, properties: { filePath: 'src/a.go' } }); + const namelessNode = { + id: 'file', + label: 'File' as const, + properties: { name: '', filePath: 'src/a.go' }, + }; + Reflect.deleteProperty(namelessNode.properties, 'name'); + g.addNode(namelessNode); g.addRelationship({ id: 'r1', sourceId: 'a', diff --git a/gitnexus/test/unit/scope-resolution/rust/rust-mod-scope-qualifier.test.ts b/gitnexus/test/unit/scope-resolution/rust/rust-mod-scope-qualifier.test.ts index 84610395a..a963570fd 100644 --- a/gitnexus/test/unit/scope-resolution/rust/rust-mod-scope-qualifier.test.ts +++ b/gitnexus/test/unit/scope-resolution/rust/rust-mod-scope-qualifier.test.ts @@ -16,7 +16,7 @@ import { import { couldNameAModule } from '../../../../src/core/ingestion/languages/rust/module-path.js'; const parser = new Parser(); -parser.setLanguage(Rust as unknown as Parser.Language); +parser.setLanguage(Rust as unknown); /** First node of `type` in a post-order walk — the innermost such node. */ function findNode(root: SyntaxNode, type: string): SyntaxNode { diff --git a/gitnexus/test/unit/scope-resolution/rust/rust-range-binding-order.test.ts b/gitnexus/test/unit/scope-resolution/rust/rust-range-binding-order.test.ts index 734f1d992..e2e6de192 100644 --- a/gitnexus/test/unit/scope-resolution/rust/rust-range-binding-order.test.ts +++ b/gitnexus/test/unit/scope-resolution/rust/rust-range-binding-order.test.ts @@ -1,5 +1,7 @@ +import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js'; import { describe, expect, it } from 'vitest'; -import type { ParsedFile, ScopeResolutionIndexes } from 'gitnexus-shared'; +import type { ParsedFile } from 'gitnexus-shared'; +import { finalizeScopeModel } from '../../../../src/core/ingestion/finalize-orchestrator.js'; import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js'; import { rustScopeResolver } from '../../../../src/core/ingestion/languages/rust/scope-resolver.js'; import { populateRustRangeBindings } from '../../../../src/core/ingestion/languages/rust/range-binding.js'; @@ -25,14 +27,7 @@ function parse(src: string, path: string): ParsedFile { } function makeEmptyIndexes(): ScopeResolutionIndexes { - return { - bindings: new Map(), - bindingAugmentations: new Map(), - imports: [], - scopeTree: { roots: [] }, - methodDispatch: new Map(), - sccs: [], - } as unknown as ScopeResolutionIndexes; + return finalizeScopeModel([]); } function boundTypeOf(parsed: ParsedFile, variableName: string): string | undefined { diff --git a/gitnexus/test/unit/scope-resolution/typescript/typescript-imports.test.ts b/gitnexus/test/unit/scope-resolution/typescript/typescript-imports.test.ts index 40ba4a332..bb41357b7 100644 --- a/gitnexus/test/unit/scope-resolution/typescript/typescript-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/typescript/typescript-imports.test.ts @@ -18,7 +18,6 @@ import * as importResolverUtils from '../../../../src/core/ingestion/import-reso import { typescriptScopeResolver } from '../../../../src/core/ingestion/languages/typescript/scope-resolver.js'; import type { SyntaxNode } from '../../../../src/core/ingestion/utils/ast-helpers.js'; import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared'; -import { SupportedLanguages } from 'gitnexus-shared'; function importsFor(src: string): ParsedImport[] { const matches = emitTsScopeCaptures(src, 'test.ts'); @@ -116,17 +115,17 @@ describe('interpretTsImport — static imports', () => { expect(imps).toHaveLength(3); expect(imps.map((i) => i.kind)).toEqual(['alias', 'named', 'alias']); - const def = imps.find((i) => i.localName === 'D'); + const def = imps.find((i) => 'localName' in i && i.localName === 'D'); expect(def).toMatchObject({ importedName: 'default', targetRaw: './m' }); - const named = imps.find((i) => i.localName === 'X'); + const named = imps.find((i) => 'localName' in i && i.localName === 'X'); expect(named).toMatchObject({ kind: 'named', importedName: 'X', targetRaw: './m', }); - const aliased = imps.find((i) => i.localName === 'Z'); + const aliased = imps.find((i) => 'localName' in i && i.localName === 'Z'); expect(aliased).toMatchObject({ kind: 'alias', importedName: 'Y', @@ -139,11 +138,11 @@ describe('interpretTsImport — static imports', () => { const imps = importsFor('import D, * as N from "./m";'); expect(imps).toHaveLength(2); - const def = imps.find((i) => i.localName === 'D'); + const def = imps.find((i) => 'localName' in i && i.localName === 'D'); expect(def?.kind).toBe('alias'); expect((def as { importedName: string }).importedName).toBe('default'); - const ns = imps.find((i) => i.localName === 'N'); + const ns = imps.find((i) => 'localName' in i && i.localName === 'N'); expect(ns?.kind).toBe('namespace'); expect((ns as { importedName: string }).importedName).toBe('./m'); }); @@ -523,19 +522,14 @@ describe('resolveTsImportTarget — standard suffix + alias resolution', () => { expect(result).toBe(null); }); - it('switches extensions when language=JavaScript', () => { + it('resolves JavaScript extensions from a JavaScript source file', () => { const parsed: ParsedImport = { kind: 'named', localName: 'X', importedName: 'X', targetRaw: './a', }; - const result = resolveTsImportTarget( - parsed, - ctx('src/main.js', ['src/main.js', 'src/a.js'], { - language: SupportedLanguages.JavaScript, - }), - ); + const result = resolveTsImportTarget(parsed, ctx('src/main.js', ['src/main.js', 'src/a.js'])); expect(result).toBe('src/a.js'); }); diff --git a/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts b/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts index 5661467fb..99301bec6 100644 --- a/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts +++ b/gitnexus/test/unit/scope-resolution/unresolved-receivers.test.ts @@ -465,25 +465,47 @@ describe('classifyReceiverShape', () => { }); it('reports no-chain for a chain with no steps', () => { - expect(classifyReceiverShape({ steps: [] })).toBe('no-chain'); + expect( + classifyReceiverShape({ baseReceiverName: 'receiver', truncated: false, steps: [] }), + ).toBe('no-chain'); }); it('reports chain-call when every step is a call', () => { - expect(classifyReceiverShape({ steps: [{ kind: 'call' }, { kind: 'call' }] })).toBe( - 'chain-call', - ); + expect( + classifyReceiverShape({ + baseReceiverName: 'receiver', + truncated: false, + steps: [ + { kind: 'call', name: 'method' }, + { kind: 'call', name: 'method' }, + ], + }), + ).toBe('chain-call'); }); it('reports chain-field when every step is a field', () => { - expect(classifyReceiverShape({ steps: [{ kind: 'field' }] })).toBe('chain-field'); + expect( + classifyReceiverShape({ + baseReceiverName: 'receiver', + truncated: false, + steps: [{ kind: 'field', name: 'property' }], + }), + ).toBe('chain-field'); }); // The distinction that makes the census actionable: a mixed chain fails for // different reasons than a pure one, so collapsing it into either bucket // would misattribute the population a fix has to target. it('reports chain-mixed when the chain interleaves calls and fields', () => { - expect(classifyReceiverShape({ steps: [{ kind: 'call' }, { kind: 'field' }] })).toBe( - 'chain-mixed', - ); + expect( + classifyReceiverShape({ + baseReceiverName: 'receiver', + truncated: false, + steps: [ + { kind: 'call', name: 'method' }, + { kind: 'field', name: 'property' }, + ], + }), + ).toBe('chain-mixed'); }); }); diff --git a/gitnexus/test/unit/scope-resolution/walkers-augmentations.test.ts b/gitnexus/test/unit/scope-resolution/walkers-augmentations.test.ts index a4dbb0e5b..60bbadb87 100644 --- a/gitnexus/test/unit/scope-resolution/walkers-augmentations.test.ts +++ b/gitnexus/test/unit/scope-resolution/walkers-augmentations.test.ts @@ -72,13 +72,14 @@ function scope(id: ScopeId, bindings = new Map()) function indexesForScopeLookup( moduleScope: Scope, augmented: Map, + bindings: ScopeResolutionIndexes['bindings'] = new Map(), ): ScopeResolutionIndexes { const scopeTree = { getScope: (id: ScopeId) => (id === moduleScope.id ? moduleScope : undefined), } as unknown as ScopeTree; return { scopeTree, - bindings: new Map(), + bindings, bindingAugmentations: new Map([[moduleScope.id, augmented]]), } as unknown as ScopeResolutionIndexes; } @@ -225,8 +226,11 @@ describe('walker helpers read bindingAugmentations', () => { origin: 'import', visibility: 'static-member-import', } as BindingRef; - const indexes = indexesForScopeLookup(moduleScope, new Map([['callMe', [staticImport]]])); - indexes.bindings.set(SCOPE, new Map([['callMe', [finalized]]])); + const indexes = indexesForScopeLookup( + moduleScope, + new Map([['callMe', [staticImport]]]), + new Map([[SCOPE, new Map([['callMe', [finalized]]])]]), + ); const candidates = findAllCallableBindingCandidatesInScope(SCOPE, 'callMe', indexes); diff --git a/gitnexus/test/unit/semantic-search-ready.test.ts b/gitnexus/test/unit/semantic-search-ready.test.ts index 0c2ad3f17..682f912d2 100644 --- a/gitnexus/test/unit/semantic-search-ready.test.ts +++ b/gitnexus/test/unit/semantic-search-ready.test.ts @@ -15,7 +15,7 @@ vi.mock('../../src/core/embeddings/embedder.js', () => ({ })); vi.mock('../../src/core/lbug/lbug-adapter.js', () => ({ - loadVectorExtension: (...args: unknown[]) => loadVectorExtensionMock(...args), + loadVectorExtension: loadVectorExtensionMock, createVectorIndex: vi.fn(), })); diff --git a/gitnexus/test/unit/server-api-repo-resolution.test.ts b/gitnexus/test/unit/server-api-repo-resolution.test.ts index 66d0c0b2c..aaea0ef8a 100644 --- a/gitnexus/test/unit/server-api-repo-resolution.test.ts +++ b/gitnexus/test/unit/server-api-repo-resolution.test.ts @@ -142,7 +142,7 @@ describe('resolveOmittedRepoSelection', () => { const second = entry({ name: 'beta', path: '/tmp/beta', storagePath: '/tmp/beta/.gitnexus' }); const result = resolveOmittedRepoSelection([first, second]); expect(result.ok).toBe(false); - if (result.ok) return; + if (result.ok !== false) throw new Error('expected ambiguous repository selection'); expect(result.status).toBe(400); expect(result.error).toMatch(/Multiple repositories indexed/); expect(result.error).toContain('alpha'); diff --git a/gitnexus/test/unit/server.test.ts b/gitnexus/test/unit/server.test.ts index da5c37d90..6d7e4f048 100644 --- a/gitnexus/test/unit/server.test.ts +++ b/gitnexus/test/unit/server.test.ts @@ -16,6 +16,7 @@ import { describe, it, expect, vi } from 'vitest'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; import { createMCPServer, installSignalShutdown, @@ -60,7 +61,7 @@ async function callToolThroughServer( try { await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); - const response = await client.callTool({ name, arguments: args }); + const response = CallToolResultSchema.parse(await client.callTool({ name, arguments: args })); const text = response.content.find((item) => item.type === 'text')?.text; if (typeof text !== 'string') throw new Error('Expected an MCP text response'); return { text, isError: response.isError === true }; diff --git a/gitnexus/test/unit/setup-codex.test.ts b/gitnexus/test/unit/setup-codex.test.ts index 10a19e38c..a30badf50 100644 --- a/gitnexus/test/unit/setup-codex.test.ts +++ b/gitnexus/test/unit/setup-codex.test.ts @@ -14,7 +14,7 @@ const execFileMock = vi.fn((...args: any[]) => { } }); -const execFileSyncMock = vi.fn(() => { +const execFileSyncMock = vi.fn((): string => { throw new Error('not found'); }); diff --git a/gitnexus/test/unit/setup-jsonc.test.ts b/gitnexus/test/unit/setup-jsonc.test.ts index d84b9a41b..a06c1f5bb 100644 --- a/gitnexus/test/unit/setup-jsonc.test.ts +++ b/gitnexus/test/unit/setup-jsonc.test.ts @@ -15,7 +15,7 @@ const execFileMock = vi.fn((...args: any[]) => { } }); -const execFileSyncMock = vi.fn(() => { +const execFileSyncMock = vi.fn((): string => { throw new Error('not found'); }); diff --git a/gitnexus/test/unit/setup.test.ts b/gitnexus/test/unit/setup.test.ts index 942211ae3..e9d0a5873 100644 --- a/gitnexus/test/unit/setup.test.ts +++ b/gitnexus/test/unit/setup.test.ts @@ -23,7 +23,7 @@ const execFileMock = vi.fn((...args: any[]) => { // By default, execFileSync throws (simulating `which gitnexus` not found) // so getMcpEntry() falls back to the npx path. -const execFileSyncMock = vi.fn(() => { +const execFileSyncMock = vi.fn((): string => { throw new Error('not found'); }); diff --git a/gitnexus/test/unit/shard-balance.test.ts b/gitnexus/test/unit/shard-balance.test.ts index ff2feb372..9b2f9f402 100644 --- a/gitnexus/test/unit/shard-balance.test.ts +++ b/gitnexus/test/unit/shard-balance.test.ts @@ -7,8 +7,8 @@ import { describe, it, expect } from 'vitest'; import { assignShards, specWeight } from '../helpers/shard-balance.js'; -const key = (s: { id: string }) => s.id; -const weight = (s: { w: number }) => s.w; +const key = (s: { id: string; w: number }) => s.id; +const weight = (s: { id: string; w: number }) => s.w; const make = (n: number) => Array.from({ length: n }, (_, i) => ({ id: `f${i}`, w: (i % 5) + 1 })); describe('assignShards', () => { diff --git a/gitnexus/test/unit/skill-gen.test.ts b/gitnexus/test/unit/skill-gen.test.ts index 57167ce20..d21225a55 100644 --- a/gitnexus/test/unit/skill-gen.test.ts +++ b/gitnexus/test/unit/skill-gen.test.ts @@ -11,7 +11,8 @@ import path from 'path'; import os from 'os'; import { generateSkillFiles } from '../../src/cli/skill-gen.js'; import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; -import type { GraphNode, GraphRelationship, KnowledgeGraph } from '../../src/core/graph/types.js'; +import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; +import type { KnowledgeGraph } from '../../src/core/graph/types.js'; import type { CommunityNode, CommunityMembership, @@ -125,6 +126,15 @@ function buildPipelineResult(opts: { ? opts.processes.reduce((s, p) => s + p.stepCount, 0) / opts.processes.length : 0, entryPointsFound: 0, + truncation: { + truncated: false, + entryPointCandidatesDropped: 0, + entryPointsUnexplored: 0, + walksCutByBudget: 0, + tracesDepthCapped: 0, + calleesDropped: 0, + processesDropped: 0, + }, }, } : undefined; @@ -135,6 +145,11 @@ function buildPipelineResult(opts: { totalFileCount: 0, communityResult, processResult, + resolutionOutcomes: [], + usedWorkerPool: false, + reparsedFileCount: 0, + scopeExtractionFailures: [], + unavailableScopeLanguageFiles: 0, }; } @@ -627,6 +642,11 @@ describe('generateSkillFiles — return values', () => { stats: { totalCommunities: 1, modularity: 0.5, nodesProcessed: 4 }, }, processResult: undefined, + resolutionOutcomes: [], + usedWorkerPool: false, + reparsedFileCount: 0, + scopeExtractionFailures: [], + unavailableScopeLanguageFiles: 0, }; const result = await generateSkillFiles(tmpDir, 'TestProject', pipeline); diff --git a/gitnexus/test/unit/spring-destinations-phase.test.ts b/gitnexus/test/unit/spring-destinations-phase.test.ts index 44fb534a6..a46d7ec4c 100644 --- a/gitnexus/test/unit/spring-destinations-phase.test.ts +++ b/gitnexus/test/unit/spring-destinations-phase.test.ts @@ -1,3 +1,4 @@ +import type { JavaAnnotationSyntaxFact } from '../../src/core/ingestion/languages/java/spring-di.js'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { mkdtemp, writeFile, symlink, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; @@ -31,6 +32,19 @@ import { generateId } from '../../src/lib/utils.js'; const OWNER_RANGE = { startLine: 4, startCol: 4, endLine: 6, endCol: 5 } as const; +function annotation( + fact: Omit, +): JavaAnnotationSyntaxFact { + const args = fact.args + ?.map((arg) => (arg.name ? `${arg.name} = ${arg.text}` : arg.text)) + .join(', '); + return { + ...fact, + line: OWNER_RANGE.startLine - 1, + text: `@${fact.name}${args === undefined ? '' : `(${args})`}`, + }; +} + function callableNode(graph: KnowledgeGraph, filePath: string, name: string): void { graph.addNode({ id: generateId('Method', `${filePath}:${name}`), @@ -87,10 +101,13 @@ describe('springDestinations phase', () => { ownerFilePath: filePath, ownerRange: OWNER_RANGE, annotations: [ - { name: 'KafkaListener', args: [{ name: 'topicPattern', text: '"orders.*"' }] }, - { name: 'KafkaListener', args: [{ name: 'topics', text: '{}' }] }, - { name: 'KafkaListener', args: [{ name: 'groupId', text: '"g"' }] }, - { name: 'KafkaListener' }, + annotation({ + name: 'KafkaListener', + args: [{ name: 'topicPattern', text: '"orders.*"' }], + }), + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '{}' }] }), + annotation({ name: 'KafkaListener', args: [{ name: 'groupId', text: '"g"' }] }), + annotation({ name: 'KafkaListener' }), ], }, ]); @@ -137,7 +154,10 @@ describe('springDestinations phase', () => { ownerFilePath: filePath, ownerRange: OWNER_RANGE, annotations: [ - { name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }, + annotation({ + name: 'KafkaListener', + args: [{ name: 'topics', text: '"${app.topic}"' }], + }), ], }, ]); @@ -258,14 +278,14 @@ describe('springDestinations phase', () => { ownerScopeId: `${filePath}#first` as never, ownerFilePath: filePath, annotations: [ - { name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }, + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }), ], }, { ownerScopeId: `${filePath}#second` as never, ownerFilePath: filePath, annotations: [ - { name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }, + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }), ], }, ]); @@ -296,7 +316,10 @@ describe('springDestinations phase', () => { ownerFilePath: filePath, ownerRange: OWNER_RANGE, annotations: [ - { name: 'KafkaListener', args: [{ name: 'topics', text: `"\${${key}:events}"` }] }, + annotation({ + name: 'KafkaListener', + args: [{ name: 'topics', text: `"\${${key}:events}"` }], + }), ], }, ]); @@ -329,10 +352,10 @@ describe('springDestinations phase', () => { ownerFilePath: filePath, ownerRange: OWNER_RANGE, annotations: [ - { + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"#{@kafkaProps.ordersTopic}"' }], - }, + }), ], }, ]); @@ -361,7 +384,9 @@ describe('springDestinations phase', () => { ownerScopeId: `${filePath}#consume` as never, ownerFilePath: filePath, ownerRange: OWNER_RANGE, - annotations: [{ name: 'KafkaListener', args: [{ name: 'topics', text: '"${}"' }] }], + annotations: [ + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"${}"' }] }), + ], }, ]); setJavaSpringMessageProducerFacts(filePath, []); @@ -406,7 +431,9 @@ describe('springDestinations phase', () => { ownerScopeId: `${listenerFile}#consume` as never, ownerFilePath: listenerFile, ownerRange: OWNER_RANGE, - annotations: [{ name: 'KafkaListener', args: [{ name: 'topics', text: '"orders"' }] }], + annotations: [ + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"orders"' }] }), + ], }, ]); setJavaSpringMessageProducerFacts(listenerFile, []); @@ -417,7 +444,9 @@ describe('springDestinations phase', () => { ownerScopeId: `${strangerFile}#consume` as never, ownerFilePath: strangerFile, ownerRange: OWNER_RANGE, - annotations: [{ name: 'RabbitListener', args: [{ name: 'queues', text: '"orders"' }] }], + annotations: [ + annotation({ name: 'RabbitListener', args: [{ name: 'queues', text: '"orders"' }] }), + ], }, ]); setJavaSpringMessageProducerFacts(strangerFile, []); @@ -483,7 +512,9 @@ describe('springDestinations phase', () => { ownerScopeId: `${filePath}#consume` as never, ownerFilePath: filePath, ownerRange: OWNER_RANGE, - annotations: [{ name: 'RabbitListener', args: [{ name: 'queues', text: '"orders"' }] }], + annotations: [ + annotation({ name: 'RabbitListener', args: [{ name: 'queues', text: '"orders"' }] }), + ], }, ]); setJavaSpringMessageProducerFacts(filePath, [ @@ -535,7 +566,9 @@ describe('springDestinations phase', () => { ownerScopeId: `${filePath}#consume` as never, ownerFilePath: filePath, ownerRange: OWNER_RANGE, - annotations: [{ name: 'KafkaListener', args: [{ name: 'topics', text: '"orders"' }] }], + annotations: [ + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"orders"' }] }), + ], }, ]); setJavaSpringMessageProducerFacts(filePath, [ @@ -571,8 +604,8 @@ describe('springDestinations phase', () => { ownerFilePath: filePath, ownerRange: OWNER_RANGE, annotations: [ - { name: 'RabbitListener', args: [{ name: 'queues', text: '"orders"' }] }, - { name: 'KafkaListener', args: [{ name: 'topics', text: '"shipments"' }] }, + annotation({ name: 'RabbitListener', args: [{ name: 'queues', text: '"orders"' }] }), + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"shipments"' }] }), ], }, ]); @@ -834,7 +867,7 @@ operations: ownerFilePath: filePath, ownerRange: OWNER_RANGE, annotations: [ - { name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }, + annotation({ name: 'KafkaListener', args: [{ name: 'topics', text: '"${app.topic}"' }] }), ], }, ]); diff --git a/gitnexus/test/unit/stream-graph-emit-config.test.ts b/gitnexus/test/unit/stream-graph-emit-config.test.ts index 878698b8c..96c901806 100644 --- a/gitnexus/test/unit/stream-graph-emit-config.test.ts +++ b/gitnexus/test/unit/stream-graph-emit-config.test.ts @@ -1,3 +1,4 @@ +import type { GraphNode } from 'gitnexus-shared'; /** * Streamed structural graph emit — config gate and pruner integration (#2680). * @@ -98,7 +99,7 @@ describe('buildPhaseList under streamGraphEmit', () => { // The sink answers a complete relationship read, so these phases work // unchanged. If this ever regresses to filtering them out, streaming can no // longer be the default. - const streamed = names({ streamGraphEmit: true, pdg: true, force: true }); + const streamed = names({ streamGraphEmit: true, pdg: true }); expect(streamed).toContain('communities'); expect(streamed).toContain('processes'); @@ -107,7 +108,7 @@ describe('buildPhaseList under streamGraphEmit', () => { }); it('keeps mro and di, whose reads are all in the retained set', () => { - const streamed = names({ streamGraphEmit: true, pdg: true, force: true }); + const streamed = names({ streamGraphEmit: true, pdg: true }); expect(streamed).toContain('mro'); expect(streamed).toContain('di'); @@ -119,7 +120,7 @@ describe('buildPhaseList under streamGraphEmit', () => { it('leaves the phase list untouched when the flag is off', () => { // Guards the default path: the gating predicates must not filter anything // for existing (flag-off) users. - const withPdg = names({ pdg: true, force: true }); + const withPdg = names({ pdg: true }); expect(withPdg).toContain('communities'); expect(withPdg).toContain('processes'); diff --git a/gitnexus/test/unit/symbol-identity-validation.test.ts b/gitnexus/test/unit/symbol-identity-validation.test.ts index 7fe380a20..230f78a9b 100644 --- a/gitnexus/test/unit/symbol-identity-validation.test.ts +++ b/gitnexus/test/unit/symbol-identity-validation.test.ts @@ -12,12 +12,12 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/taint/path-codec.test.ts b/gitnexus/test/unit/taint/path-codec.test.ts index a7b891844..70005df8e 100644 --- a/gitnexus/test/unit/taint/path-codec.test.ts +++ b/gitnexus/test/unit/taint/path-codec.test.ts @@ -29,7 +29,7 @@ function unescapeCSVField(cell: string): string { const roundTrip = (hops: readonly TaintPathHopInput[]) => { const { reason } = encodeTaintPath(hops); const decoded = decodeTaintPath(reason); - if (!decoded.ok) throw new Error(`decode failed: ${decoded.error}`); + if (decoded.ok === false) throw new Error(`decode failed: ${decoded.error}`); return { reason, decoded }; }; @@ -293,7 +293,7 @@ describe('typed parse failures (never a throw)', () => { it(`fails typed on ${label}`, () => { const decoded = decodeTaintPath(input); expect(decoded.ok).toBe(false); - if (!decoded.ok) expect(decoded.error.length).toBeGreaterThan(0); + if (decoded.ok === false) expect(decoded.error.length).toBeGreaterThan(0); }); } diff --git a/gitnexus/test/unit/taint/propagate.test.ts b/gitnexus/test/unit/taint/propagate.test.ts index a1bb61e3d..3ab469240 100644 --- a/gitnexus/test/unit/taint/propagate.test.ts +++ b/gitnexus/test/unit/taint/propagate.test.ts @@ -36,8 +36,15 @@ import { computeTaintFlows, type FunctionTaintResult, type TaintLimits, + type TaintSourceOccurrence, } from '../../../src/core/ingestion/taint/propagate.js'; +function sourceProperty(source: TaintSourceOccurrence): string { + expect(source.type).toBe('member-read'); + if (source.type !== 'member-read') throw new Error('Expected a member-read source'); + return source.property; +} + /** The single binding index for `name` (throws when shadowed/ambiguous). */ function bindingIdx(cfg: FunctionCfg, name: string): number { const idxs = (cfg.bindings ?? []).map((b, i) => (b.name === name ? i : -1)).filter((i) => i >= 0); @@ -153,7 +160,7 @@ describe('rule (b) — statement-local findings', () => { expect(r.findings).toHaveLength(1); const f = r.findings[0]; expect(f.sinkKind).toBe('command-injection'); - expect(f.source.property).toBe('body'); + expect(sourceProperty(f.source)).toBe('body'); expect(f.source.point.line).toBe(2); expect(f.sink.point.line).toBe(2); expect(f.sink.argIndex).toBe(0); @@ -168,14 +175,14 @@ describe('rule (b) — statement-local findings', () => { }; const r = analyze(`function f(req) { exec(req.body, req.query); }`, { spec }); expect(r.findings).toHaveLength(2); - const ids = r.findings.map((f) => `${f.source.property}@arg${f.sink.argIndex}`); + const ids = r.findings.map((f) => `${sourceProperty(f.source)}@arg${f.sink.argIndex}`); expect(ids).toEqual(['body@arg0', 'query@arg1']); }); it('exec(req.body.toString()) → finding via the statement-local rule', () => { const r = analyze(`function f(req) { exec(req.body.toString()); }`); expect(r.findings).toHaveLength(1); - expect(r.findings[0].source.property).toBe('body'); + expect(sourceProperty(r.findings[0].source)).toBe('body'); }); it('a source read at an UNREGISTERED sink position produces no finding', () => { @@ -194,7 +201,7 @@ describe('rule (a) — worklist over def→use facts', () => { }`); expect(r.findings).toHaveLength(1); expect(hopSummary(r)).toEqual(['b@2', 'b@3']); - expect(r.findings[0].source.property).toBe('body'); + expect(sourceProperty(r.findings[0].source)).toBe('body'); expect(r.findings[0].sink.point.line).toBe(3); }); @@ -491,7 +498,7 @@ describe('multi-source identity — distinct sources do not merge at one def', ( spec: MULTI, }); expect(r.findings).toHaveLength(2); - const props = r.findings.map((f) => f.source.property).sort(); + const props = r.findings.map((f) => sourceProperty(f.source)).sort(); expect(props).toEqual(['body', 'query']); }); @@ -691,7 +698,7 @@ describe('caps — deterministic truncation (R6 substrate)', () => { expect(r.findings).toHaveLength(2); expect(r.droppedFindings).toBe(2); // deterministic prefix of the sorted order: statement order - expect(r.findings.map((f) => f.source.property)).toEqual(['body', 'query']); + expect(r.findings.map((f) => sourceProperty(f.source))).toEqual(['body', 'query']); }); it('without a cap all findings emit and droppedFindings is 0', () => { diff --git a/gitnexus/test/unit/taint/taint-emit.test.ts b/gitnexus/test/unit/taint/taint-emit.test.ts index c6d8f734a..dda9e1cb9 100644 --- a/gitnexus/test/unit/taint/taint-emit.test.ts +++ b/gitnexus/test/unit/taint/taint-emit.test.ts @@ -189,7 +189,7 @@ function handler(req: { body: string; query: string }) { expect(new Set(tainted.map((t) => t.id)).size).toBe(2); const variables = tainted.map((t) => { const d = decodeTaintPath(t.reason); - if (!d.ok) throw new Error(d.error); + if (d.ok === false) throw new Error(d.error); return d.hops[d.hops.length - 1].variable; }); expect(variables.sort()).toEqual(['a', 'b']); diff --git a/gitnexus/test/unit/tool-process-linking.test.ts b/gitnexus/test/unit/tool-process-linking.test.ts index d28dd9329..0c7203db2 100644 --- a/gitnexus/test/unit/tool-process-linking.test.ts +++ b/gitnexus/test/unit/tool-process-linking.test.ts @@ -127,7 +127,7 @@ describe('Tool handler and process linking phases', () => { await processesPhase.execute( makeCtx(graph), - new Map([ + new Map>([ ['structure', phaseResult('structure', { totalFiles: 1 })], ['communities', phaseResult('communities', { communityResult: { memberships: [] } })], ['routes', phaseResult('routes', { routeRegistry: new Map() })], diff --git a/gitnexus/test/unit/trace-bfs.test.ts b/gitnexus/test/unit/trace-bfs.test.ts index c76ef9a01..bf287ec1c 100644 --- a/gitnexus/test/unit/trace-bfs.test.ts +++ b/gitnexus/test/unit/trace-bfs.test.ts @@ -17,12 +17,12 @@ const { lbugMocks } = vi.hoisted(() => ({ })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); diff --git a/gitnexus/test/unit/trpc-entry-point-scoring.test.ts b/gitnexus/test/unit/trpc-entry-point-scoring.test.ts index 03b1de78c..f6a81558e 100644 --- a/gitnexus/test/unit/trpc-entry-point-scoring.test.ts +++ b/gitnexus/test/unit/trpc-entry-point-scoring.test.ts @@ -1,3 +1,4 @@ +import { SupportedLanguages } from 'gitnexus-shared'; import { describe, it, expect } from 'vitest'; import { calculateEntryPointScore } from '../../src/core/ingestion/entry-point-scoring.js'; @@ -5,7 +6,7 @@ describe('tRPC entry-point scoring', () => { it('scores a T3 router procedure higher than the same name on a utility file', () => { const t3 = calculateEntryPointScore( 'setSettings', - 'typescript', + SupportedLanguages.TypeScript, true, 0, 3, @@ -13,7 +14,7 @@ describe('tRPC entry-point scoring', () => { ); const util = calculateEntryPointScore( 'setSettings', - 'typescript', + SupportedLanguages.TypeScript, true, 0, 3, @@ -28,7 +29,14 @@ describe('tRPC entry-point scoring', () => { it('still applies utility-pattern to non-accessor helpers in a T3 router file', () => { const routerPath = 'src/server/api/routers/settings.ts'; for (const name of ['formatDate', '_internal', 'parseInput'] as const) { - const result = calculateEntryPointScore(name, 'typescript', true, 0, 3, routerPath); + const result = calculateEntryPointScore( + name, + SupportedLanguages.TypeScript, + true, + 0, + 3, + routerPath, + ); expect(result.reasons).toContain('utility-pattern'); expect(result.reasons).toContain('framework:trpc-router'); } @@ -37,7 +45,7 @@ describe('tRPC entry-point scoring', () => { it('skips utility-pattern for accessor procedures on a T3 JavaScript router', () => { const result = calculateEntryPointScore( 'setSettings', - 'javascript', + SupportedLanguages.JavaScript, true, 0, 3, @@ -50,7 +58,7 @@ describe('tRPC entry-point scoring', () => { it('still applies utility-pattern to non-accessor helpers on a T3 JavaScript router', () => { const result = calculateEntryPointScore( 'formatDate', - 'javascript', + SupportedLanguages.JavaScript, true, 0, 3, @@ -63,7 +71,7 @@ describe('tRPC entry-point scoring', () => { it('does not crash on a .js router path whose framework detection returns null', () => { const jsResult = calculateEntryPointScore( 'settingsRouter', - 'javascript', + SupportedLanguages.JavaScript, true, 0, 3, diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index 785c1ec83..62830ccac 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { buildTypeEnv, type TypeEnvironment } from '../../src/core/ingestion/type-env.js'; import { BindingAccumulator } from '../../src/core/ingestion/binding-accumulator.js'; -import { type SymbolDefinition } from 'gitnexus-shared'; +import { SupportedLanguages, type SymbolDefinition } from 'gitnexus-shared'; import { createSemanticModel, type SemanticModel, @@ -29,7 +29,7 @@ let Dart: unknown; try { Dart = requireVendoredGrammar('tree-sitter-dart'); const testParser = new Parser(); - testParser.setLanguage(Dart as Parser.Language); + testParser.setLanguage(Dart); } catch { Dart = null; } @@ -38,27 +38,27 @@ let Swift: unknown; try { Swift = requireVendoredGrammar('tree-sitter-swift'); const testParser = new Parser(); - testParser.setLanguage(Swift as Parser.Language); + testParser.setLanguage(Swift); } catch { Swift = null; } const parser = new Parser(); -const parse = (code: string, lang: any) => { +const parse = (code: string, lang: unknown) => { parser.setLanguage(lang); return parser.parse(code); }; const parseDart = (code: string) => { if (!Dart) throw new Error('tree-sitter-dart not available'); - parser.setLanguage(Dart as Parser.Language); + parser.setLanguage(Dart); return parser.parse(code); }; const parseSwift = (code: string) => { if (!Swift) throw new Error('tree-sitter-swift not available'); - parser.setLanguage(Swift as Parser.Language); + parser.setLanguage(Swift); return parser.parse(code); }; @@ -95,56 +95,56 @@ describe('buildTypeEnv', () => { describe('TypeScript', () => { it('extracts type from const declaration', () => { const tree = parse('const user: User = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('extracts type from let declaration', () => { const tree = parse('let repo: Repository;', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); it('extracts type from function parameters', () => { const tree = parse('function save(user: User, repo: Repository) {}', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); it('extracts type from arrow function parameters', () => { const tree = parse('const fn = (user: User) => user.save();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('ignores variables without type annotations', () => { const tree = parse('const x = 5; let y = "hello";', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatSize(typeEnv)).toBe(0); }); it('extracts type from nullable union User | null', () => { const tree = parse('const user: User | null = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('extracts type from optional union User | undefined', () => { const tree = parse('let user: User | undefined;', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('extracts type from triple nullable union User | null | undefined', () => { const tree = parse('const user: User | null | undefined = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('ignores non-nullable unions like User | Repo', () => { const tree = parse('const entity: User | Repo = getEntity();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'entity')).toBeUndefined(); }); }); @@ -162,7 +162,7 @@ describe('buildTypeEnv', () => { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); @@ -176,7 +176,7 @@ describe('buildTypeEnv', () => { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); @@ -190,7 +190,7 @@ describe('buildTypeEnv', () => { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -207,7 +207,7 @@ describe('buildTypeEnv', () => { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -222,7 +222,7 @@ describe('buildTypeEnv', () => { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -235,7 +235,7 @@ describe('buildTypeEnv', () => { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); @@ -254,7 +254,7 @@ describe('buildTypeEnv', () => { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -270,7 +270,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -284,7 +284,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -298,7 +298,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -312,7 +312,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repo'); }); @@ -327,7 +327,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -341,7 +341,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); @@ -355,7 +355,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'sl')).toBe('User'); }); @@ -369,7 +369,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'm')).toBe('User'); }); @@ -384,7 +384,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -399,7 +399,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -411,7 +411,7 @@ describe('buildTypeEnv', () => { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); // Go parameter extraction depends on tree-sitter grammar structure // Parameters may or may not have 'name'/'type' fields }); @@ -427,7 +427,7 @@ describe('buildTypeEnv', () => { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -438,7 +438,7 @@ describe('buildTypeEnv', () => { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); @@ -452,7 +452,7 @@ describe('buildTypeEnv', () => { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -460,19 +460,19 @@ describe('buildTypeEnv', () => { describe('Python', () => { it('extracts type from annotated assignment (PEP 484)', () => { const tree = parse('user: User = get_user()', Python); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('extracts type from standalone annotation without value (file scope)', () => { const tree = parse('active_user: User', Python); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'active_user')).toBe('User'); }); it('extracts type from function parameters', () => { const tree = parse('def process(user: User, repo: Repository): pass', Python); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // Python uses typed_parameter nodes, check if they match }); @@ -484,7 +484,7 @@ describe('buildTypeEnv', () => { `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'name')).toBe('str'); expect(flatGet(typeEnv, 'age')).toBe('int'); }); @@ -496,7 +496,7 @@ describe('buildTypeEnv', () => { `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'repo')).toBe('UserRepo'); }); @@ -512,7 +512,7 @@ describe('buildTypeEnv', () => { `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'name')).toBe('str'); expect(flatGet(typeEnv, 'age')).toBe('int'); expect(flatGet(typeEnv, 'repo')).toBe('UserRepo'); @@ -533,7 +533,7 @@ def process(x): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'u')).toBe('User'); }); @@ -551,7 +551,7 @@ def process(x): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // u is already bound from the annotation, pattern binding should not overwrite expect(flatGet(typeEnv, 'u')).toBe('User'); }); @@ -574,7 +574,7 @@ def process(x): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'r')).toBe('Repo'); }); @@ -590,7 +590,7 @@ def process(x): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // No class_pattern child — should return undefined expect(flatGet(typeEnv, 'n')).toBeUndefined(); }); @@ -607,7 +607,7 @@ def process(x): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -620,7 +620,7 @@ def process(x): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -633,7 +633,7 @@ def process(x): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -644,7 +644,7 @@ def process(x): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repository'); }); @@ -661,7 +661,7 @@ def process(x): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -677,7 +677,7 @@ def process(x): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -690,7 +690,7 @@ def process(x): `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); // PHP parameter type extraction expect(flatGet(typeEnv, '$user')).toBe('User'); expect(flatGet(typeEnv, '$repo')).toBe('Repository'); @@ -704,7 +704,7 @@ class UserService { } }`; const tree = parse(code, PHP.php); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); // Find the call node ($this->save()) const calls: any[] = []; @@ -731,7 +731,7 @@ class User { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$repo')).toBe('UserRepo'); }); @@ -744,7 +744,7 @@ class UserService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$repo')).toBe('UserRepo'); }); @@ -757,7 +757,7 @@ class UserService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$name')).toBe('string'); }); @@ -774,7 +774,7 @@ function create($repo, $name) { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$repo')).toBe('UserRepo'); expect(flatGet(typeEnv, '$name')).toBe('string'); }); @@ -792,7 +792,7 @@ function process($repo, $name) { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$repo')).toBe('UserRepo'); expect(flatGet(typeEnv, '$name')).toBe('string'); }); @@ -812,7 +812,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'repo')).toBe('UserRepo'); expect(flatGet(typeEnv, 'name')).toBe('String'); }); @@ -826,7 +826,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -839,7 +839,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'name')).toBe('String'); }); @@ -852,7 +852,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'value')).toBeUndefined(); }); @@ -865,7 +865,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatSize(typeEnv)).toBe(0); }); @@ -881,7 +881,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'name')).toBe('String'); }); @@ -894,7 +894,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'users')).toBe('Array'); }); }); @@ -912,7 +912,7 @@ class User extends BaseModel { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const calls: any[] = []; function findCalls(node: any) { @@ -942,7 +942,7 @@ class User extends BaseModel { } }`; const tree = parse(code, Java); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); const calls: any[] = []; function findCalls(node: any) { @@ -968,7 +968,7 @@ class User(BaseModel): return True `; const tree = parse(code, Python); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); const calls: any[] = []; function findCalls(node: any) { @@ -991,7 +991,7 @@ class Standalone { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // No calls in this code — test the resolution function directly // by using the class body as the context node @@ -1009,7 +1009,7 @@ object AppConfig { } }`; const tree = parse(code, Kotlin); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); const calls: any[] = []; function findCalls(node: any) { @@ -1036,7 +1036,7 @@ object AppConfig { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Each function has its own scope for 'user' (keyed by funcName@startIndex) // Find the scope keys that start with handleUser/handleRepo @@ -1058,7 +1058,7 @@ function handleRepo(user: Repo) { user.save(); }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Find the call nodes inside each function const calls: any[] = []; @@ -1090,7 +1090,7 @@ class RepoService { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Find the call nodes inside each process method const calls: any[] = []; @@ -1120,7 +1120,7 @@ class RepoService { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // config is at file-level scope const fileScope = typeEnv.fileScope(); @@ -1150,7 +1150,7 @@ class RepoService { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // The typed variable is captured expect(flatGet(typeEnv, 'user')).toBe('User'); // Destructured bindings (name, email) would need type inference to resolve @@ -1168,53 +1168,32 @@ class RepoService { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Complex type annotation (object type) — extractSimpleTypeName returns undefined expect(flatSize(typeEnv)).toBe(0); }); }); describe('destructured call results', () => { - // Minimal mock SemanticModel for call-result return type lookup - // (SM-21 inversion — buildTypeEnv takes a SemanticModel via `model:`). - const makeSymbolTable = (callables: Array<{ name: string; returnType?: string }>) => ({ - types: { - lookupClassByName: () => [], - lookupClassByQualifiedName: () => [], - lookupImplByName: () => [], - }, - methods: { - lookupMethodByOwner: () => undefined, - lookupMethodByName: () => [], - }, - fields: { - lookupFieldByOwner: () => undefined, - }, - symbols: { - add: () => {}, - lookupExact: () => undefined, - lookupExactFull: () => undefined, - lookupExactAll: () => [], - lookupCallableByName: (name: string) => - callables - .filter((c) => c.name === name) - .map((c) => ({ - nodeId: 'n1', - filePath: 'src.ts', - type: 'Function' as const, - returnType: c.returnType, - })), - getFiles: () => [][Symbol.iterator](), - getStats: () => ({ fileCount: 0 }), - }, - }); + // Register only the callable return types needed by these fixtures. + const makeSymbolTable = ( + callables: Array<{ name: string; returnType?: string }>, + ): SemanticModel => { + const model = createSemanticModel(); + for (const [index, callable] of callables.entries()) { + model.symbols.add('src.ts', callable.name, `function:${index}`, 'Function', { + returnType: callable.returnType, + }); + } + return model; + }; it('emits callResult + fieldAccess items for const { x } = fn()', () => { const symbolTable = makeSymbolTable([{ name: 'getUser', returnType: 'User' }]); const tree = parse('const { name } = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { model: symbolTable }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable }); // callResult resolves __destr_getUser_N → User - // fieldAccess resolves name via User's properties (no Property nodes in mock → undefined) + // fieldAccess resolves name via User's properties (no Property nodes in model → undefined) // But the callResult itself IS emitted — verify constructorBindings is still empty expect(typeEnv.constructorBindings).toEqual([]); }); @@ -1225,14 +1204,14 @@ class RepoService { 'async function f() { const { data } = await fetchData(); }', TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript', { model: symbolTable }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable }); expect(typeEnv.constructorBindings).toEqual([]); }); it('gracefully handles no return type (composable without annotation)', () => { const symbolTable = makeSymbolTable([{ name: 'useUserRole' }]); // no returnType const tree = parse('const { isMaker } = useUserRole();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { model: symbolTable }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable }); // No return type → callResult unresolved → fieldAccess unresolved expect(flatGet(typeEnv, 'isMaker')).toBeUndefined(); }); @@ -1240,7 +1219,7 @@ class RepoService { it('resolves destructured properties when return type has declared fields', () => { // importedReturnTypes provides the return type since no real SymbolTable const tree = parse('const { name } = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedReturnTypes: new Map([['getUser', 'User']]), }); // callResult resolves __destr_getUser_N → User via importedReturnTypes @@ -1264,7 +1243,7 @@ class RepoService { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // repo is resolved to Repo via Tier 1 (constructor inference) expect(flatGet(typeEnv, 'repo')).toBe('Repo'); // Destructured method call emits methodCallResult + fieldAccess @@ -1274,7 +1253,7 @@ class RepoService { it('handles renamed destructuring: const { address: addr } = fn()', () => { const tree = parse('const { address: addr } = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedReturnTypes: new Map([['getUser', 'User']]), }); // 'addr' should be the binding, not 'address' @@ -1294,53 +1273,53 @@ class RepoService { describe('TypeScript', () => { it('infers type from new expression when no annotation', () => { const tree = parse('const user = new User();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); it('prefers explicit annotation over constructor inference', () => { const tree = parse('const user: BaseUser = new User();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('BaseUser'); }); it('infers from namespaced constructor: new ns.Service()', () => { // extractSimpleTypeName handles member_expression via property_identifier const tree = parse('const svc = new ns.Service();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'svc')).toBe('Service'); }); it('infers type from new expression with as cast', () => { const tree = parse('const x = new User() as BaseUser;', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Unwraps as_expression to find the inner new_expression → User expect(flatGet(typeEnv, 'x')).toBe('User'); }); it('infers type from new expression with non-null assertion', () => { const tree = parse('const x = new User()!;', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Unwraps non_null_expression to find the inner new_expression → User expect(flatGet(typeEnv, 'x')).toBe('User'); }); it('infers type from double-cast (new X() as unknown as T)', () => { const tree = parse('const x = new User() as unknown as Admin;', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Unwraps nested as_expression to find inner new_expression → User expect(flatGet(typeEnv, 'x')).toBe('User'); }); it('ignores non-new assignments', () => { const tree = parse('const x = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatSize(typeEnv)).toBe(0); }); it('handles mixed annotated + unannotated declarators', () => { const tree = parse('const a: A = getA(), b = new B();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'a')).toBe('A'); expect(flatGet(typeEnv, 'b')).toBe('B'); }); @@ -1358,7 +1337,7 @@ class RepoService { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1373,7 +1352,7 @@ class RepoService { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1388,7 +1367,7 @@ class RepoService { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); }); @@ -1403,7 +1382,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1416,7 +1395,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'config')).toBe('Config'); }); @@ -1429,7 +1408,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // ::default() should be excluded from scanConstructorBinding just like ::new() // extractInitializer already resolves it, so a scanner binding would be redundant const defaultBinding = typeEnv.constructorBindings.find((b) => b.calleeName === 'default'); @@ -1445,7 +1424,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); const newBinding = typeEnv.constructorBindings.find((b) => b.calleeName === 'new'); expect(newBinding).toBeUndefined(); }); @@ -1460,7 +1439,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('BaseUser'); }); @@ -1473,7 +1452,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1489,7 +1468,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'instance')).toBe('User'); }); @@ -1505,7 +1484,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'cfg')).toBe('Config'); }); @@ -1518,7 +1497,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); @@ -1531,7 +1510,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); @@ -1544,7 +1523,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1557,7 +1536,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'config')).toBe('Config'); }); @@ -1570,7 +1549,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('BaseUser'); }); @@ -1586,7 +1565,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'fresh')).toBe('User'); }); @@ -1599,7 +1578,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); }); @@ -1616,7 +1595,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1631,7 +1610,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1646,7 +1625,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'item')).toBe('Config'); }); @@ -1661,7 +1640,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // Option is unwrapped to "User" in TypeEnv via NULLABLE_WRAPPER_TYPES. // extractPatternBinding maps `user` → "User" from the scopeEnv lookup for `opt`. expect(flatGet(typeEnv, 'user')).toBe('User'); @@ -1678,7 +1657,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // 'name' is a field of User — we don't know its type without field-type resolution expect(flatGet(typeEnv, 'name')).toBeUndefined(); // 'val' should still be extracted from the parameter annotation @@ -1696,7 +1675,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // scoped_type_identifier: Message::Data — extractSimpleTypeName returns "Data" expect(flatGet(typeEnv, 'msg')).toBe('Data'); }); @@ -1712,7 +1691,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // Option unwraps to User (nullable wrapper unwrapping) expect(flatGet(typeEnv, 'opt')).toBe('User'); expect(flatGet(typeEnv, 'user')).toBe('User'); @@ -1729,7 +1708,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // opt: Option → scopeEnv stores "User" (NULLABLE_WRAPPER_TYPES unwrapping) // extractPatternBinding maps user → opt's type → "User" expect(flatGet(typeEnv, 'user')).toBe('User'); @@ -1746,7 +1725,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // unknown_var is not in scopeEnv — conservative, produces no binding expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); @@ -1762,7 +1741,7 @@ class RepoService { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // SomeOtherVariant is not a known unwrap wrapper — no binding expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); @@ -1782,7 +1761,7 @@ class RepoService { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1797,7 +1776,7 @@ class RepoService { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); // No pattern variable — b gets its declared type 'boolean', not 'User' expect(flatGet(typeEnv, 'b')).toBe('boolean'); }); @@ -1818,7 +1797,7 @@ class RepoService { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repo'); }); @@ -1832,7 +1811,7 @@ class RepoService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$user')).toBe('User'); }); @@ -1848,7 +1827,7 @@ class RepoService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$a')).toBe('Foo'); expect(flatGet(typeEnv, '$b')).toBe('Foo'); }); @@ -1865,7 +1844,7 @@ class RepoService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$p')).toBe('Bar'); }); @@ -1876,7 +1855,7 @@ class RepoService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$a')).toBeUndefined(); }); @@ -1887,7 +1866,7 @@ class RepoService { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$user')).toBeUndefined(); }); }); @@ -1902,7 +1881,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1916,7 +1895,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1929,7 +1908,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1944,7 +1923,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); // getUser is an identifier but NOT a known class — no inference expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); @@ -1959,7 +1938,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -1973,7 +1952,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'cfg')).toBe('Config'); }); @@ -1987,7 +1966,7 @@ class RepoService { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -2002,7 +1981,7 @@ class RepoService { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -2016,7 +1995,7 @@ class RepoService { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -2030,7 +2009,7 @@ class RepoService { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); // getUser is not a class name — should NOT produce a binding expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); @@ -2047,7 +2026,7 @@ class RepoService { // User is NOT defined in this file, but SemanticModel knows it's a Class const model = createSemanticModel(); model.symbols.add('models.kt', 'User', 'n1', 'Class'); - const typeEnv = buildTypeEnv(tree, 'kotlin', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin, { model }); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -2061,7 +2040,7 @@ class RepoService { Kotlin, ); const model = createSemanticModel(); - const typeEnv = buildTypeEnv(tree, 'kotlin', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin, { model }); expect(flatGet(typeEnv, 'result')).toBeUndefined(); }); @@ -2075,7 +2054,7 @@ class RepoService { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); // Tier 0 (explicit annotation) wins over Tier 1 (constructor inference) expect(flatGet(typeEnv, 'user')).toBe('BaseEntity'); }); @@ -2093,7 +2072,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -2108,7 +2087,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // extractSimpleTypeName extracts "User" from attribute node "models.User" expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -2121,7 +2100,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); }); @@ -2145,7 +2124,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'models.py')], }), @@ -2161,7 +2140,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python, { model: makeClassLookupTable({}), }); expect(flatGet(typeEnv, 'result')).toBeUndefined(); @@ -2175,7 +2154,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'models.py')], }), @@ -2192,7 +2171,7 @@ void run() { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'models.h')], }), @@ -2209,7 +2188,7 @@ void run() { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus, { model: makeClassLookupTable({}), }); expect(flatGet(typeEnv, 'result')).toBeUndefined(); @@ -2224,7 +2203,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'models/user.rb')], }), @@ -2241,7 +2220,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby, { model: makeClassLookupTable({ UserService: [createClassDef('UserService', 'Class', 'models/user_service.rb')], }), @@ -2258,7 +2237,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby, { model: makeClassLookupTable({}), }); expect(flatGet(typeEnv, 'result')).toBeUndefined(); @@ -2273,7 +2252,7 @@ void run() { } `, ); - const typeEnv = buildTypeEnv(tree, 'dart', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Dart, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'models.dart')], }), @@ -2289,7 +2268,7 @@ void run() { } `, ); - const typeEnv = buildTypeEnv(tree, 'dart', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Dart, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'models.dart')], }), @@ -2305,7 +2284,7 @@ void run() { } `, ); - const typeEnv = buildTypeEnv(tree, 'dart', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Dart, { model: makeClassLookupTable({}), }); expect(flatGet(typeEnv, 'result')).toBeUndefined(); @@ -2321,7 +2300,7 @@ fn run() { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust, { model: makeClassLookupTable({ UserService: [createClassDef('UserService', 'Struct', 'models.rs')], }), @@ -2338,7 +2317,7 @@ fn run() { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust, { model: makeClassLookupTable({}), }); expect(flatGet(typeEnv, 'value')).toBeUndefined(); @@ -2353,7 +2332,7 @@ func run() { } `, ); - const typeEnv = buildTypeEnv(tree, 'swift', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Swift, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'Models/User.swift')], }), @@ -2369,7 +2348,7 @@ func run() { } `, ); - const typeEnv = buildTypeEnv(tree, 'swift', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Swift, { model: makeClassLookupTable({ User: [createClassDef('User', 'Class', 'Models/User.swift')], }), @@ -2385,7 +2364,7 @@ func run() { } `, ); - const typeEnv = buildTypeEnv(tree, 'swift', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Swift, { model: makeClassLookupTable({}), }); expect(flatGet(typeEnv, 'result')).toBeUndefined(); @@ -2407,7 +2386,7 @@ function process(user: User) { ownerId: 'class:User', declaredType: 'Address', }); - const typeEnv = buildTypeEnv(tree, 'typescript', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model }); expect(flatGet(typeEnv, 'addr')).toBe('Address'); }); @@ -2421,7 +2400,7 @@ function process(user: User) { TypeScript.typescript, ); const model = createSemanticModel(); - const typeEnv = buildTypeEnv(tree, 'typescript', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model }); expect(flatGet(typeEnv, 'addr')).toBeUndefined(); }); @@ -2441,7 +2420,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model }); expect(flatGet(typeEnv, 'profile')).toBe('Profile'); expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile'); }); @@ -2463,7 +2442,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model, parentMap: new Map([['Repo', ['BaseRepo']]]), }); @@ -2488,7 +2467,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model }); expect(flatGet(typeEnv, 'profile')).toBe('Profile'); expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile'); }); @@ -2511,7 +2490,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model, parentMap: new Map([['Repo', ['BaseRepo']]]), }); @@ -2540,7 +2519,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model }); expect(flatGet(typeEnv, 'profile')).toBeUndefined(); expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile'); }); @@ -2565,7 +2544,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { model }); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model }); expect(flatGet(typeEnv, 'profile')).toBe('Profile'); expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile'); }); @@ -2595,7 +2574,7 @@ function process(repo: Repo) { returnType: 'Admin', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model, parentMap: new Map([['Repo', ['BaseRepo']]]), }); @@ -2624,7 +2603,7 @@ function process(repo: Repo) { returnType: 'Profile', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model, parentMap: new Map([['Repo', ['BaseRepo']]]), }); @@ -2657,7 +2636,7 @@ function process(repo: Repo) { returnType: 'Admin', }); const lookupCallableByName = vi.spyOn(model.symbols, 'lookupCallableByName'); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model, parentMap: new Map([['Repo', ['BaseRepo']]]), }); @@ -2679,7 +2658,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -2692,7 +2671,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'data')).toBeUndefined(); }); }); @@ -2701,7 +2680,7 @@ def main(): describe('edge cases', () => { it('returns empty map for code without type annotations', () => { const tree = parse('const x = 5;', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatSize(typeEnv)).toBe(0); }); @@ -2713,7 +2692,7 @@ def main(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Both declarations are at file level; last one wins expect(flatGet(typeEnv, 'x')).toBeDefined(); }); @@ -2732,7 +2711,7 @@ class User extends BaseModel { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const calls: any[] = []; function findCalls(node: any) { @@ -2759,7 +2738,7 @@ class User extends BaseModel { } }`; const tree = parse(code, Java); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); const calls: any[] = []; function findCalls(node: any) { @@ -2785,7 +2764,7 @@ class User(Model): super().save() `; const tree = parse(code, Python); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); const calls: any[] = []; function findCalls(node: any) { @@ -2811,7 +2790,7 @@ class User : BaseModel { } }`; const tree = parse(code, CSharp); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); const calls: any[] = []; function findCalls(node: any) { @@ -2840,7 +2819,7 @@ class User : BaseModel { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); // Should extract "HttpClient" from the scoped_identifier ns::HttpClient const binding = typeEnv.constructorBindings.find((b) => b.varName === 'client'); expect(binding).toBeDefined(); @@ -2857,7 +2836,7 @@ class User : BaseModel { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); // User() with known class resolves via extractInitializer, not constructor bindings expect(flatGet(typeEnv, 'user')).toBe('User'); // No unresolved bindings since User is locally known @@ -2875,7 +2854,7 @@ class User : BaseModel { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); // UnknownClass is not defined locally — should appear as unverified binding expect(flatGet(typeEnv, 'user')).toBeUndefined(); expect(typeEnv.constructorBindings.length).toBe(1); @@ -2892,7 +2871,7 @@ class User : BaseModel { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); // Explicit annotation resolves it — no unverified binding needed expect(flatGet(typeEnv, 'user')).toBe('User'); expect(typeEnv.constructorBindings.find((b) => b.varName === 'user')).toBeUndefined(); @@ -2905,7 +2884,7 @@ func run() { let user = User.init(name: "alice") } `); - const typeEnv = buildTypeEnv(tree, 'swift'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Swift); expect(flatGet(typeEnv, 'user')).toBeUndefined(); expect(typeEnv.constructorBindings).toEqual([ expect.objectContaining({ @@ -2924,7 +2903,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBeUndefined(); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); @@ -2939,7 +2918,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); expect(typeEnv.constructorBindings[0].calleeName).toBe('User'); @@ -2954,7 +2933,7 @@ def main(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBeUndefined(); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); @@ -2971,7 +2950,7 @@ def main(): `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(typeEnv.constructorBindings).toEqual([]); }); @@ -2982,7 +2961,7 @@ REPO = Repo.new `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('REPO'); expect(typeEnv.constructorBindings[0].calleeName).toBe('Repo'); @@ -2995,7 +2974,7 @@ service = Models::UserService.new `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('service'); expect(typeEnv.constructorBindings[0].calleeName).toBe('UserService'); @@ -3008,7 +2987,7 @@ svc = App::Models::Service.new `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('svc'); expect(typeEnv.constructorBindings[0].calleeName).toBe('Service'); @@ -3023,14 +3002,14 @@ svc = App::Models::Service.new `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].scope).toMatch(/^process@\d+$/); }); it('returns constructor bindings for TypeScript const user = getUser()', () => { const tree = parse('const user = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBeUndefined(); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); @@ -3039,20 +3018,20 @@ svc = App::Models::Service.new it('does NOT emit constructor binding when TypeScript var has explicit type annotation', () => { const tree = parse('const user: User = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(typeEnv.constructorBindings.find((b) => b.varName === 'user')).toBeUndefined(); }); it('skips destructuring patterns (array_pattern) for TypeScript', () => { const tree = parse('const [a, b] = getPair();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(typeEnv.constructorBindings).toEqual([]); }); it('skips destructuring patterns (object_pattern) for TypeScript', () => { const tree = parse('const { name, age } = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(typeEnv.constructorBindings).toEqual([]); }); @@ -3061,7 +3040,7 @@ svc = App::Models::Service.new 'async function f() { const user = await fetchUser(); }', TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); expect(typeEnv.constructorBindings[0].calleeName).toBe('fetchUser'); @@ -3069,7 +3048,7 @@ svc = App::Models::Service.new it('handles qualified callee in TypeScript: const user = repo.getUser()', () => { const tree = parse('const user = repo.getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); expect(typeEnv.constructorBindings[0].calleeName).toBe('getUser'); @@ -3077,7 +3056,7 @@ svc = App::Models::Service.new it('does not emit binding for TypeScript new expression (handled by extractInitializer)', () => { const tree = parse('const user = new User();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(typeEnv.constructorBindings.find((b) => b.varName === 'user')).toBeUndefined(); }); @@ -3094,7 +3073,7 @@ svc = App::Models::Service.new `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); const binding = typeEnv.constructorBindings.find((b) => b.varName === 'user'); expect(binding).toBeDefined(); expect(binding!.calleeName).toBe('GetUser'); @@ -3109,7 +3088,7 @@ svc = App::Models::Service.new `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(typeEnv.constructorBindings.length).toBe(1); expect(typeEnv.constructorBindings[0].varName).toBe('user'); expect(typeEnv.constructorBindings[0].calleeName).toBe('get_user'); @@ -3127,7 +3106,7 @@ svc = App::Models::Service.new `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); const binding = typeEnv.constructorBindings.find((b) => b.varName === 'user'); expect(binding).toBeDefined(); expect(binding!.calleeName).toBe('GetUserAsync'); @@ -3144,7 +3123,7 @@ svc = App::Models::Service.new `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); const binding = typeEnv.constructorBindings.find((b) => b.varName === 'user'); expect(binding).toBeDefined(); expect(binding!.calleeName).toBe('GetUser'); @@ -3160,7 +3139,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'a')).toBe('User'); expect(flatGet(typeEnv, 'b')).toBe('User'); }); @@ -3173,7 +3152,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'a')).toBe('User'); expect(flatGet(typeEnv, 'b')).toBe('User'); }); @@ -3190,7 +3169,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'a')).toBe('User'); expect(flatGet(typeEnv, 'b')).toBe('User'); expect(flatGet(typeEnv, 'c')).toBe('User'); @@ -3206,7 +3185,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // 'alias' should get User from the parameter 'user' const scopeKey = [...typeEnv.allScopes().keys()].find((k) => k.startsWith('process@')); expect(scopeKey).toBeDefined(); @@ -3224,7 +3203,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // cfg in process scope picks up Config from the file-level config binding const scopeKey = [...typeEnv.allScopes().keys()].find((k) => k.startsWith('process@')); expect(scopeKey).toBeDefined(); @@ -3238,7 +3217,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // getUser() is a call_expression — should not create a binding expect(flatGet(typeEnv, 'x')).toBeUndefined(); }); @@ -3338,7 +3317,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const scopeKey = [...typeEnv.allScopes().keys()].find((k) => k.startsWith('process@')); expect(scopeKey).toBeDefined(); expect(typeEnv.allScopes().get(scopeKey!)?.get('a')).toBe('User'); @@ -3360,7 +3339,7 @@ svc = App::Models::Service.new `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const scopeKey = [...typeEnv.allScopes().keys()].find((k) => k.startsWith('process@')); expect(scopeKey).toBeDefined(); expect(typeEnv.allScopes().get(scopeKey!)?.get('c')).toBe('User'); @@ -3384,7 +3363,7 @@ svc = App::Models::Service.new `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'a')).toBe('User'); expect(flatGet(typeEnv, 'b')).toBe('User'); }); @@ -3403,7 +3382,7 @@ svc = App::Models::Service.new `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3420,7 +3399,7 @@ svc = App::Models::Service.new `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3437,7 +3416,7 @@ svc = App::Models::Service.new `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3456,7 +3435,7 @@ svc = App::Models::Service.new `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3472,7 +3451,7 @@ def process(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3487,7 +3466,7 @@ def process(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3504,7 +3483,7 @@ def process(): `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3521,7 +3500,7 @@ def process(): `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$u')).toBe('User'); expect(flatGet(typeEnv, '$alias')).toBe('User'); }); @@ -3542,7 +3521,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); // Without a known type for 'user' (no annotation in Ruby), alias_user stays undefined. // This verifies the extractor doesn't crash or produce false bindings. expect(flatGet(typeEnv, 'alias_user')).toBeUndefined(); @@ -3558,7 +3537,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); // get_user() is a call — user has no resolved type, so alias_user should not resolve either expect(flatGet(typeEnv, 'alias_user')).toBeUndefined(); }); @@ -3576,7 +3555,7 @@ end `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Find the call node for .save() const scopeKey = [...typeEnv.allScopes().keys()].find((k) => k.startsWith('process@')); expect(scopeKey).toBeDefined(); @@ -3592,7 +3571,7 @@ def process(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // Python 3.10+ union syntax is stored as raw text "User | None" // which stripNullable resolves at lookup time const rawVal = flatGet(typeEnv, 'user'); @@ -3616,7 +3595,7 @@ def process(): `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3629,7 +3608,7 @@ def process(): `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3644,7 +3623,7 @@ def process(): `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'users')).toBe('List'); }); @@ -3659,7 +3638,7 @@ def process(): `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'lookup')).toBe('Map'); }); @@ -3674,7 +3653,7 @@ def process(): `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'future')).toBe('CompletableFuture'); }); @@ -3705,7 +3684,7 @@ def process(): `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'alias')).toBe('User'); }); @@ -3725,7 +3704,7 @@ def process(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3740,7 +3719,7 @@ def process(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3755,7 +3734,7 @@ def process(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // for...in yields string keys — extractor must NOT bind 'key' to User expect(flatGet(typeEnv, 'key')).toBeUndefined(); }); @@ -3771,7 +3750,7 @@ def process(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); @@ -3787,7 +3766,7 @@ def process(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Note: users itself is stored with no binding (extractSimpleTypeName returns undefined // for array_type), but the for-loop extractor uses AST walking to resolve the element type. expect(flatGet(typeEnv, 'user')).toBe('User'); @@ -3804,7 +3783,7 @@ def process(): `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -3819,7 +3798,7 @@ def process(users: List[User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3832,7 +3811,7 @@ def process(users: Sequence[User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3845,7 +3824,7 @@ def process(users): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); @@ -3859,7 +3838,7 @@ def process(): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // List[User] → extractSimpleTypeName returns 'List' (base name), stored as 'List' // extractElementTypeFromString('List') → undefined (no brackets in the string) // So user is unresolved unless users is stored as 'List[User]' raw. @@ -3883,7 +3862,7 @@ func process(users []User) { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -3899,7 +3878,7 @@ func process(users []User) { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); // In Go, `for v := range slice` gives the INDEX (int), not the element. expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); @@ -3916,7 +3895,7 @@ func process(myMap map[string]User) { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); expect(flatGet(typeEnv, 'v')).toBe('User'); }); @@ -3932,7 +3911,7 @@ func process(myMap map[string]User) { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); // Single-var map range gives the KEY, not the value expect(flatGet(typeEnv, 'k')).toBeUndefined(); }); @@ -3948,7 +3927,7 @@ func process() { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); // C-style for loop has no range_clause — extractor must return early expect(flatGet(typeEnv, 'i')).toBeUndefined(); }); @@ -3966,7 +3945,7 @@ func process() { `, Go, ); - const typeEnv = buildTypeEnv(tree, 'go'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Go); // users has no type annotation — only a constructor binding candidate // Without a resolved type for users, user cannot be inferred expect(flatGet(typeEnv, 'user')).toBeUndefined(); @@ -3985,7 +3964,7 @@ fn process(users: Vec) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4000,7 +3979,7 @@ fn process(users: &[User]) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4015,7 +3994,7 @@ fn process() { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'i')).toBeUndefined(); }); @@ -4031,7 +4010,7 @@ fn process() { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); }); @@ -4051,7 +4030,7 @@ class Foo { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4068,7 +4047,7 @@ class Foo { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4086,7 +4065,7 @@ class Foo { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); }); @@ -4103,7 +4082,7 @@ fun process(users: List) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4118,7 +4097,7 @@ fun process(users: List) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4134,7 +4113,7 @@ fun process() { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); }); @@ -4153,7 +4132,7 @@ class Foo { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4171,7 +4150,7 @@ class Foo { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBeUndefined(); }); }); @@ -4190,7 +4169,7 @@ function process(entries: Map) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4206,7 +4185,7 @@ def process(data: dict[str, User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4222,7 +4201,7 @@ def process(data: dict[str, User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'v')).toBe('User'); // i is the int index from enumerate — must NOT be bound to User expect(flatGet(typeEnv, 'i')).toBeUndefined(); @@ -4240,7 +4219,7 @@ def process(data: dict[str, User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'v')).toBe('User'); expect(flatGet(typeEnv, 'i')).toBeUndefined(); }); @@ -4257,7 +4236,7 @@ def process(users: List[User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); // enumerate yields (index, element) — k is int (unbound), v is User expect(flatGet(typeEnv, 'v')).toBe('User'); expect(flatGet(typeEnv, 'k')).toBeUndefined(); @@ -4277,7 +4256,7 @@ function process(x) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'x')).toBe('User'); }); @@ -4294,7 +4273,7 @@ fn process(users: Vec) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -4311,7 +4290,7 @@ function process(data: Map) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4326,7 +4305,7 @@ function process(data: Map) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'key')).toBe('string'); }); @@ -4339,7 +4318,7 @@ def process(data: dict[str, User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'key')).toBe('str'); }); @@ -4352,7 +4331,7 @@ def process(data: dict[str, User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4367,7 +4346,7 @@ fn process(data: HashMap) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'key')).toBe('String'); }); @@ -4382,7 +4361,7 @@ fn process(data: HashMap) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -4399,7 +4378,7 @@ fn process(data: HashMap) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'key')).toBe('String'); }); @@ -4414,7 +4393,7 @@ fn process(data: HashMap) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4429,7 +4408,7 @@ fn process(users: Vec) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4445,7 +4424,7 @@ function process(cache: MyCache) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'key')).toBe('string'); }); }); @@ -4462,7 +4441,7 @@ function process(users: User[]) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Object destructuring should NOT produce bindings — field types are unknown expect(flatGet(typeEnv, 'id')).toBeUndefined(); expect(flatGet(typeEnv, 'name')).toBeUndefined(); @@ -4479,7 +4458,7 @@ function process(users: User[]) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4492,7 +4471,7 @@ def process(users: List[User]): `, Python, ); - const typeEnv = buildTypeEnv(tree, 'python'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Python); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4507,7 +4486,7 @@ void process(std::map& map) { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'value')).toBe('User'); }); @@ -4534,7 +4513,7 @@ void processRepoMap(std::map repoMap) { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); expect(flatGet(typeEnv, 'user')).toBe('User'); expect(flatGet(typeEnv, 'repo')).toBe('Repo'); }); @@ -4553,7 +4532,7 @@ end `, Ruby, ); - const typeEnv = buildTypeEnv(tree, 'ruby'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Ruby); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -4570,7 +4549,7 @@ fun process(x: Any) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'x')).toBe('User'); }); @@ -4586,7 +4565,7 @@ fun process(x: Any) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); // allowPatternBindingOverwrite means each arm overwrites — last one wins expect(flatGet(typeEnv, 'x')).toBe('Admin'); }); @@ -4603,7 +4582,7 @@ fun process() { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); // x has type String from its declaration — no pattern binding should narrow it // (else branch has no type_test node, so extractKotlinPatternBinding never fires) expect(flatGet(typeEnv, 'x')).toBe('String'); @@ -4622,7 +4601,7 @@ fun processValues(data: HashMap) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4637,7 +4616,7 @@ fun processList(users: List) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -4656,7 +4635,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'u')).toBe('User'); }); @@ -4674,7 +4653,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'u')).toBe('User'); expect(flatGet(typeEnv, 'a')).toBe('Admin'); }); @@ -4692,7 +4671,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); // Only the parameter x:Object should exist, no extra bindings from case 42 expect(flatGet(typeEnv, 'x')).toBe('Object'); }); @@ -4710,7 +4689,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -4730,7 +4709,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4745,7 +4724,7 @@ fun process(data: MutableMap) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4760,7 +4739,7 @@ fun process(users: MutableList) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4778,7 +4757,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4795,7 +4774,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -4814,7 +4793,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'u')).toBe('User'); }); @@ -4832,7 +4811,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'r')).toBe('Repo'); }); @@ -4848,7 +4827,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); // obj → object from the parameter, but no pattern binding expect(flatGet(typeEnv, 'obj')).toBe('object'); expect(flatSize(typeEnv)).toBe(1); // only the parameter binding @@ -4870,7 +4849,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -4889,7 +4868,7 @@ public class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); // Verify lookup works from the call site (user.Save()) const saveCall = tree.rootNode.descendantsOfType('invocation_expression')[0]; @@ -4912,7 +4891,7 @@ class UserService { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // User[] is an array_type — extractSimpleTypeName returns undefined (no simple base name). // But declarationTypeNodes captures the raw AST node, so for-loop resolution // uses Strategy 1 (extractTsElementTypeFromAnnotation) to resolve the element type. @@ -4928,7 +4907,7 @@ class RepoService { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'repos')).toBe('Map'); }); }); @@ -4949,7 +4928,7 @@ class App { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$user')).toBe('User'); }); @@ -4967,7 +4946,7 @@ class App { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); expect(flatGet(typeEnv, '$item')).toBeUndefined(); }); @@ -4995,7 +4974,7 @@ class B { `, PHP.php, ); - const typeEnv = buildTypeEnv(tree, 'php'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.PHP); // Both $item bindings exist but may share the same key if scoped to method name // Conservative: just verify at least one resolves correctly expect(flatGet(typeEnv, '$item')).toBeDefined(); @@ -5015,7 +4994,7 @@ fn process(opt: Option) { `, Rust, ); - const typeEnv = buildTypeEnv(tree, 'rust'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Rust); // user should be typed from the first arm (Some unwrap) // Known limitation: binding leaks across arms (first-writer-wins) expect(flatGet(typeEnv, 'user')).toBe('User'); @@ -5025,7 +5004,7 @@ fn process(opt: Option) { describe('performance optimizations — coverage for new code paths', () => { it('fastStripNullable: passes through simple identifier without stripping', () => { const tree = parse('function f(user: User) { user.save(); }', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // lookup exercises fastStripNullable — "User" has no | or ? markers const callNode = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('save')); expect(typeEnv.lookup('user', callNode)).toBe('User'); @@ -5033,14 +5012,14 @@ fn process(opt: Option) { it('fastStripNullable: strips nullable union type via full stripNullable', () => { const tree = parse('function f(user: User | null) { user.save(); }', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const callNode = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('save')); expect(typeEnv.lookup('user', callNode)).toBe('User'); }); it('fastStripNullable: rejects bare nullable keyword', () => { const tree = parse('function f(x: null) { x.save(); }', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const callNode = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('save')); expect(typeEnv.lookup('x', callNode)).toBeUndefined(); }); @@ -5056,7 +5035,7 @@ class Foo { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const callNode = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('save')); expect(typeEnv.lookup('user', callNode)).toBe('User'); }); @@ -5071,7 +5050,7 @@ function f(user: User) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -5087,7 +5066,7 @@ function calculate(service: Service) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'service')).toBe('Service'); }); }); @@ -5101,7 +5080,7 @@ function process(x: User | null) { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Inside the if-body, x should resolve to User (nullable stripped) const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save')); expect(typeEnv.lookup('x', saveCall)).toBe('User'); @@ -5115,7 +5094,7 @@ function process(x: User | undefined) { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save')); expect(typeEnv.lookup('x', saveCall)).toBe('User'); }); @@ -5128,7 +5107,7 @@ function process(x: User | null) { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save')); expect(typeEnv.lookup('x', saveCall)).toBe('User'); }); @@ -5143,7 +5122,7 @@ function process(x: User | null) { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // Inside else branch, x should retain original nullable type (User via fastStripNullable) const fallbackCall = tree.rootNode.descendantForIndex( tree.rootNode.text.indexOf('x.fallback'), @@ -5165,7 +5144,7 @@ function process(x: User | null) { x.other(); }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // After the if-block, x should use the flat scopeEnv (User | null → User via fastStripNullable) const otherCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.other')); expect(typeEnv.lookup('x', otherCall)).toBe('User'); @@ -5179,7 +5158,7 @@ function process(x: User) { } }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // x is already non-nullable — no narrowing override is emitted, but lookup still works const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save')); expect(typeEnv.lookup('x', saveCall)).toBe('User'); @@ -5196,7 +5175,7 @@ function process(x) { `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'x')).toBe('User'); }); @@ -5208,7 +5187,7 @@ fun process(x: User?) { } }`; const tree = parse(code, Kotlin); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save')); expect(typeEnv.lookup('x', saveCall)).toBe('User'); }); @@ -5224,7 +5203,7 @@ fun process(x: Any) { `, Kotlin, ); - const typeEnv = buildTypeEnv(tree, 'kotlin'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Kotlin); expect(flatGet(typeEnv, 'x')).toBe('User'); }); @@ -5238,7 +5217,7 @@ class App { } }`; const tree = parse(code, CSharp); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.Save')); expect(typeEnv.lookup('x', saveCall)).toBe('User'); }); @@ -5256,7 +5235,7 @@ class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'user')).toBe('User'); }); }); @@ -5275,7 +5254,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'a')).toBe('User'); expect(flatGet(typeEnv, 'b')).toBe('User'); }); @@ -5295,7 +5274,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); // x should NOT have a type binding (it's untyped via var) expect(flatGet(typeEnv, 'x')).toBeUndefined(); @@ -5314,7 +5293,7 @@ class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); expect(flatGet(typeEnv, 'a')).toBe('User'); expect(flatGet(typeEnv, 'b')).toBe('User'); }); @@ -5331,7 +5310,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'user')).toBe('User'); }); @@ -5351,7 +5330,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); expect(flatGet(typeEnv, 'users')).toBe('List'); expect(flatGet(typeEnv, 'admins')).toBe('List'); expect(flatGet(typeEnv, 'u')).toBe('User'); @@ -5372,7 +5351,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); // Find the entry for variable 'a' let ctorType: string | undefined; for (const [key, value] of typeEnv.constructorTypeMap) { @@ -5396,7 +5375,7 @@ class App { `, Java, ); - const typeEnv = buildTypeEnv(tree, 'java'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.Java); let found = false; for (const [key] of typeEnv.constructorTypeMap) { if (key.endsWith('\0u')) { @@ -5419,7 +5398,7 @@ const a: Animal = new Dog(); `, TypeScript.typescript, ); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); expect(flatGet(typeEnv, 'a')).toBe('Animal'); let found = false; for (const [key] of typeEnv.constructorTypeMap) { @@ -5442,7 +5421,7 @@ void run() { `, CPP, ); - const typeEnv = buildTypeEnv(tree, 'cpp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CPlusPlus); let ctorType: string | undefined; for (const [key, value] of typeEnv.constructorTypeMap) { if (key.endsWith('\0a')) { @@ -5466,7 +5445,7 @@ class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); let ctorType: string | undefined; for (const [key, value] of typeEnv.constructorTypeMap) { if (key.endsWith('\0a')) { @@ -5489,7 +5468,7 @@ class App { `, CSharp, ); - const typeEnv = buildTypeEnv(tree, 'csharp'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.CSharp); // d should be bound via declared type path expect(flatGet(typeEnv, 'd')).toBe('Dog'); // constructorTypeMap should NOT have an entry (same type, no override needed) @@ -5508,7 +5487,7 @@ class App { it('seeds imported bindings into file scope for unbound names', () => { // Source has no local declaration of 'config', so the imported binding wins const tree = parse('', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedBindings: new Map([['config', 'Config']]), }); const fileScope = typeEnv.fileScope(); @@ -5518,7 +5497,7 @@ class App { it('local declarations take precedence over imported bindings', () => { // Source declares config: AppConfig — imported binding for 'config' must not overwrite it const tree = parse('const config: AppConfig = getConfig();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedBindings: new Map([['config', 'Config']]), }); const fileScope = typeEnv.fileScope(); @@ -5528,8 +5507,8 @@ class App { it('does nothing when importedBindings is empty', () => { const tree = parse('const user: User = getUser();', TypeScript.typescript); - const typeEnvWithout = buildTypeEnv(tree, 'typescript'); - const typeEnvWith = buildTypeEnv(tree, 'typescript', { + const typeEnvWithout = buildTypeEnv(tree, SupportedLanguages.TypeScript); + const typeEnvWith = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedBindings: new Map(), }); // Both envs should produce the same file-scope content @@ -5541,7 +5520,7 @@ class App { it('seeds multiple bindings', () => { const tree = parse('', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedBindings: new Map([ ['user', 'User'], ['config', 'Config'], @@ -5559,7 +5538,7 @@ function process() { config.validate(); }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedBindings: new Map([['config', 'Config']]), }); @@ -5577,7 +5556,7 @@ function process() { it('seeds bindings with no conflict when local file has unrelated declarations', () => { // File has 'user' declared locally; 'config' comes from importedBindings const tree = parse('const user: User = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedBindings: new Map([['config', 'Config']]), }); const fileScope = typeEnv.fileScope(); @@ -5609,7 +5588,7 @@ function process() { // importedReturnTypes says getConfig → WrongType — SymbolTable must win const symbolTable = makeSymbolTable([{ name: 'getConfig', returnType: 'Config' }]); const tree = parse('const c = getConfig();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable, importedReturnTypes: new Map([['getConfig', 'WrongType']]), }); @@ -5621,7 +5600,7 @@ function process() { // SymbolTable knows nothing about getConfig const symbolTable = makeSymbolTable([]); const tree = parse('const c = getConfig();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable, importedReturnTypes: new Map([['getConfig', 'Config']]), }); @@ -5636,7 +5615,7 @@ function process() { { name: 'process', returnType: 'Admin' }, ]); const tree = parse('const r = process();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable, importedReturnTypes: new Map([['process', 'User']]), }); @@ -5646,7 +5625,7 @@ function process() { it('no SymbolTable → uses cross-file return types directly', () => { const tree = parse('const c = getConfig();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedReturnTypes: new Map([['getConfig', 'Config']]), }); expect(flatGet(typeEnv, 'c')).toBe('Config'); @@ -5656,7 +5635,7 @@ function process() { // SymbolTable provides the authoritative return type; cross-file entry is ignored const symbolTable = makeSymbolTable([{ name: 'getUser', returnType: 'User' }]); const tree = parse('const u = getUser();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { model: symbolTable, importedReturnTypes: new Map([['getUser', 'CrossFileUser']]), }); @@ -5666,7 +5645,7 @@ function process() { it('does nothing when importedReturnTypes is absent', () => { const tree = parse('const c = getConfig();', TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); // No annotation, no SymbolTable, no cross-file → no binding expect(flatGet(typeEnv, 'c')).toBeUndefined(); }); @@ -5681,7 +5660,7 @@ function process() { c.validate(); }`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript', { + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript, { importedReturnTypes: new Map([['getConfig', 'Config']]), }); @@ -5704,7 +5683,7 @@ function process() { it('flushes file-scope bindings into accumulator', () => { const code = `const user: User = getUser();\nconst count: number = 0;`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv.flush('/src/test.ts', acc); @@ -5726,7 +5705,7 @@ function process() { it('does NOT flush function-scoped bindings into accumulator (file-scope narrowing)', () => { const code = `function process() {\n const result: Response = fetch();\n}`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv.flush('/src/test.ts', acc); @@ -5746,7 +5725,7 @@ function process() { // the primary narrowing-contract assertion for the sequential path. const code = `const dbClient: Database = connectDb();\nfunction handleRequest() {\n const localRequest: Request = parseRequest();\n const localUser: User = loadUser();\n}`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv.flush('/src/service.ts', acc); @@ -5767,7 +5746,7 @@ function process() { it('flushes nothing for an empty TypeEnv', () => { const code = `// empty file`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv.flush('/src/empty.ts', acc); @@ -5780,8 +5759,8 @@ function process() { const code2 = `const b: B = makeB();`; const tree1 = parse(code1, TypeScript.typescript); const tree2 = parse(code2, TypeScript.typescript); - const typeEnv1 = buildTypeEnv(tree1, 'typescript'); - const typeEnv2 = buildTypeEnv(tree2, 'typescript'); + const typeEnv1 = buildTypeEnv(tree1, SupportedLanguages.TypeScript); + const typeEnv2 = buildTypeEnv(tree2, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv1.flush('/src/a.ts', acc); @@ -5795,7 +5774,7 @@ function process() { it('throws on second flush of the same TypeEnv (single-use)', () => { const code = `const x: X = makeX();`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv.flush('/src/a.ts', acc); @@ -5829,7 +5808,7 @@ function internal() { } `; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); // Real flush — exercises the narrowed FILE_SCOPE-only iteration. @@ -5883,7 +5862,7 @@ function internal() { const code = `export const helper: WorkerInferredType = makeHelper();`; const tree = parse(code, TypeScript.typescript); - const typeEnv = buildTypeEnv(tree, 'typescript'); + const typeEnv = buildTypeEnv(tree, SupportedLanguages.TypeScript); const acc = new BindingAccumulator(); typeEnv.flush('src/utils.ts', acc); acc.finalize(); diff --git a/gitnexus/test/unit/upload-ingest.test.ts b/gitnexus/test/unit/upload-ingest.test.ts index 03d04d49d..ed31b37a6 100644 --- a/gitnexus/test/unit/upload-ingest.test.ts +++ b/gitnexus/test/unit/upload-ingest.test.ts @@ -43,7 +43,6 @@ describe('resolveContainedDest', () => { }); it('rejects non-string input', () => { - // @ts-expect-error testing runtime guard expect(() => resolveContainedDest(ROOT, ['a', 'b'])).toThrow(); }); diff --git a/gitnexus/test/unit/variable-extraction.test.ts b/gitnexus/test/unit/variable-extraction.test.ts index b9b2e7279..e0502545c 100644 --- a/gitnexus/test/unit/variable-extraction.test.ts +++ b/gitnexus/test/unit/variable-extraction.test.ts @@ -894,7 +894,7 @@ describeKotlin('VariableExtractor — Kotlin (F51 destructuring)', () => { /** The first property_declaration whose text starts with `prefix`. */ function propertyDecl(src: string, prefix: string): SyntaxNode { - parser.setLanguage(Kotlin as Parser.Language); + parser.setLanguage(Kotlin); const tree = parser.parse(src); let found: SyntaxNode | undefined; const walk = (n: SyntaxNode) => { diff --git a/gitnexus/test/unit/watch-queue.test.ts b/gitnexus/test/unit/watch-queue.test.ts index 260008a96..741778aee 100644 --- a/gitnexus/test/unit/watch-queue.test.ts +++ b/gitnexus/test/unit/watch-queue.test.ts @@ -24,7 +24,9 @@ describe('WatchRefreshQueue', () => { const batches: readonly string[][] = []; const mutable = batches as string[][]; const queue = new WatchRefreshQueue( - async (paths) => mutable.push([...paths]), + async (paths) => { + mutable.push([...paths]); + }, () => {}, 100, ); @@ -102,7 +104,9 @@ describe('WatchRefreshQueue', () => { vi.useFakeTimers(); const batches: string[][] = []; const queue = new WatchRefreshQueue( - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, () => {}, 50, { holdEventsUntilInitialRefresh: true }, @@ -250,7 +254,9 @@ describe('WatchRefreshQueue', () => { vi.useFakeTimers(); const batches: string[][] = []; const queue = new WatchRefreshQueue( - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, () => {}, 100, { maxWaitMs: 250 }, @@ -271,7 +277,9 @@ describe('WatchRefreshQueue', () => { vi.useFakeTimers(); const batches: string[][] = []; const queue = new WatchRefreshQueue( - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, () => {}, 10, { @@ -292,7 +300,9 @@ describe('WatchRefreshQueue', () => { vi.useFakeTimers(); const batches: string[][] = []; const queue = new WatchRefreshQueue( - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, () => {}, 10, { @@ -314,7 +324,9 @@ describe('WatchRefreshQueue', () => { vi.useFakeTimers(); const batches: string[][] = []; const queue = new WatchRefreshQueue( - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, () => {}, 10, { maxPendingPaths: 2 }, @@ -333,7 +345,9 @@ describe('WatchRefreshQueue', () => { vi.useFakeTimers(); const batches: string[][] = []; const queue = new WatchRefreshQueue( - async (paths) => batches.push([...paths]), + async (paths) => { + batches.push([...paths]); + }, () => {}, 10, { maxPendingPaths: 0 }, diff --git a/gitnexus/test/unit/wiki-db-pin.test.ts b/gitnexus/test/unit/wiki-db-pin.test.ts index 8af799710..3cba3d7ae 100644 --- a/gitnexus/test/unit/wiki-db-pin.test.ts +++ b/gitnexus/test/unit/wiki-db-pin.test.ts @@ -84,6 +84,8 @@ describe('WikiGenerator DB pinning', () => { model: 'test-model', baseUrl: 'http://127.0.0.1:1/v1', apiKey: 'test', + maxTokens: 1024, + temperature: 0, }, { force: true, reviewOnly: true }, ); diff --git a/gitnexus/test/unit/wiki-flags.test.ts b/gitnexus/test/unit/wiki-flags.test.ts index 6ffd46d57..935b8953b 100644 --- a/gitnexus/test/unit/wiki-flags.test.ts +++ b/gitnexus/test/unit/wiki-flags.test.ts @@ -1532,7 +1532,7 @@ describe('local agent CLI calls', () => { return stdinText; }); - const spawnSpy = vi.fn(() => child); + const spawnSpy = vi.fn(() => child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('opencode 1.15.13'), spawn: spawnSpy, @@ -1583,7 +1583,7 @@ describe('local agent CLI calls', () => { }); }); - const spawnSpy = vi.fn(() => child); + const spawnSpy = vi.fn(() => child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('opencode 1.15.13'), spawn: spawnSpy, @@ -1780,7 +1780,7 @@ describe('local agent CLI calls', () => { }); }); - const spawnSpy = vi.fn(() => child); + const spawnSpy = vi.fn(() => child); vi.doMock('child_process', () => ({ execFileSync: vi.fn().mockReturnValue('codex-cli 0.132.0'), spawn: spawnSpy, diff --git a/gitnexus/test/unit/worker-pool-ready-timeout-env.test.ts b/gitnexus/test/unit/worker-pool-ready-timeout-env.test.ts index f64135974..b61d68b38 100644 --- a/gitnexus/test/unit/worker-pool-ready-timeout-env.test.ts +++ b/gitnexus/test/unit/worker-pool-ready-timeout-env.test.ts @@ -15,6 +15,7 @@ */ import { describe, expect, it, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'node:events'; +import type { Worker } from 'node:worker_threads'; import path from 'node:path'; import os from 'node:os'; import fs from 'node:fs'; @@ -63,9 +64,10 @@ describe('worker pool — GITNEXUS_WORKER_READY_TIMEOUT_MS override', () => { workerFactory: () => new NeverReadyWorker() as unknown as Worker, }); - const err = await pool - .dispatch([{ path: 'a.ts', content: 'x' }]) - .catch((e: unknown) => e as InstanceType); + const err = await pool.dispatch([{ path: 'a.ts', content: 'x' }]).catch((e: unknown) => e); + if (!(err instanceof WorkerPoolInitializationError)) { + throw new Error('expected worker initialization failure'); + } expect(err).toBeInstanceOf(WorkerPoolInitializationError); expect(err.readinessFailures.join('\n')).toContain('within 50ms'); @@ -79,9 +81,10 @@ describe('worker pool — GITNEXUS_WORKER_READY_TIMEOUT_MS override', () => { workerFactory: () => new NeverReadyWorker() as unknown as Worker, }); - const err = await pool - .dispatch([{ path: 'a.ts', content: 'x' }]) - .catch((e: unknown) => e as InstanceType); + const err = await pool.dispatch([{ path: 'a.ts', content: 'x' }]).catch((e: unknown) => e); + if (!(err instanceof WorkerPoolInitializationError)) { + throw new Error('expected worker initialization failure'); + } expect(err).toBeInstanceOf(WorkerPoolInitializationError); expect(err.readinessFailures.join('\n')).toContain('within 5000ms'); diff --git a/gitnexus/test/unit/worker-pool-resilience.test.ts b/gitnexus/test/unit/worker-pool-resilience.test.ts index 1427f45f1..997773e9a 100644 --- a/gitnexus/test/unit/worker-pool-resilience.test.ts +++ b/gitnexus/test/unit/worker-pool-resilience.test.ts @@ -286,7 +286,7 @@ describe('worker pool resilience', () => { expect(results).toEqual([{ fileCount: 2 }]); // Two bad files quarantined; both pre-crash 'starting-file' targets. - expect(pool.getQuarantinedPaths().sort()).toEqual(['src/a.ts', 'src/b.ts']); + expect([...pool.getQuarantinedPaths()].sort()).toEqual(['src/a.ts', 'src/b.ts']); await pool.terminate(); }); @@ -306,7 +306,7 @@ describe('worker pool resilience', () => { ]), ).rejects.toBeInstanceOf(WorkerPoolDispatchError); - expect(pool.getQuarantinedPaths().sort()).toEqual(['src/x.ts', 'src/y.ts']); + expect([...pool.getQuarantinedPaths()].sort()).toEqual(['src/x.ts', 'src/y.ts']); // Subsequent dispatch on a tripped pool rejects without running anything. await expect( pool.dispatch<{ path: string; content: string }, unknown>([ @@ -348,7 +348,7 @@ describe('worker pool resilience', () => { { path: 'src/ok.ts', content: '' }, ]); expect(r2).toEqual([{ fileCount: 1 }]); - expect(pool.getQuarantinedPaths().sort()).toEqual(['src/bad.ts', 'src/bad2.ts']); + expect([...pool.getQuarantinedPaths()].sort()).toEqual(['src/bad.ts', 'src/bad2.ts']); await pool.terminate(); }); diff --git a/gitnexus/test/unit/worker-pool-startup-stderr.test.ts b/gitnexus/test/unit/worker-pool-startup-stderr.test.ts index b09568e95..67fa0cd7d 100644 --- a/gitnexus/test/unit/worker-pool-startup-stderr.test.ts +++ b/gitnexus/test/unit/worker-pool-startup-stderr.test.ts @@ -15,6 +15,7 @@ */ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'node:events'; +import type { Worker } from 'node:worker_threads'; import path from 'node:path'; import os from 'node:os'; import fs from 'node:fs'; @@ -93,7 +94,10 @@ describe('worker pool — startup stderr surfacing (#1741)', () => { const dispatch = pool.dispatch([{ path: 'src/a.ts', content: 'x' }]); await expect(dispatch).rejects.toBeInstanceOf(WorkerPoolInitializationError); - const err = await dispatch.catch((e: unknown) => e as WorkerPoolInitializationError); + const err = await dispatch.catch((e: unknown) => e); + if (!(err instanceof WorkerPoolInitializationError)) { + throw new Error('expected worker initialization failure'); + } expect(err.readinessFailures.length).toBeGreaterThan(0); // The real crash reason, recovered from the worker's stderr, is present. const joined = err.readinessFailures.join('\n'); @@ -127,7 +131,7 @@ describe('worker pool — startup self-healing (#1741)', () => { expect(pool.getStats().poolBroken).toBe(false); // R1 (clear-on-settle): the ref'd backoff timer self-cleared when it fired, // so no startup timer lingers after the slot's retry loop exited. - expect(pool.getStats().pendingStartupTimers).toBe(0); + expect(pool.getStats()).toHaveProperty('pendingStartupTimers', 0); await pool.terminate().catch(() => undefined); }); @@ -143,9 +147,10 @@ describe('worker pool — startup self-healing (#1741)', () => { }, }); - const err = await pool - .dispatch([{ path: 'a.ts', content: 'x' }]) - .catch((e: unknown) => e as WorkerPoolInitializationError); + const err = await pool.dispatch([{ path: 'a.ts', content: 'x' }]).catch((e: unknown) => e); + if (!(err instanceof WorkerPoolInitializationError)) { + throw new Error('expected worker initialization failure'); + } expect(err).toBeInstanceOf(WorkerPoolInitializationError); expect(err.crashClass).toBe('deterministic-startup'); @@ -193,9 +198,10 @@ describe('worker pool — startup self-healing (#1741)', () => { }, }); - const err = await pool - .dispatch([{ path: 'a.ts', content: 'x' }]) - .catch((e: unknown) => e as WorkerPoolInitializationError); + const err = await pool.dispatch([{ path: 'a.ts', content: 'x' }]).catch((e: unknown) => e); + if (!(err instanceof WorkerPoolInitializationError)) { + throw new Error('expected worker initialization failure'); + } expect(err).toBeInstanceOf(WorkerPoolInitializationError); expect(err.crashClass).toBe('transient-exhausted'); @@ -217,9 +223,10 @@ describe('worker pool — startup self-healing (#1741)', () => { }, }); - const err = await pool - .dispatch([{ path: 'a.ts', content: 'x' }]) - .catch((e: unknown) => e as WorkerPoolInitializationError); + const err = await pool.dispatch([{ path: 'a.ts', content: 'x' }]).catch((e: unknown) => e); + if (!(err instanceof WorkerPoolInitializationError)) { + throw new Error('expected worker initialization failure'); + } expect(err.crashClass).toBe('deterministic-startup'); expect(calls).toBe(2); // attempt 0 + one respawn that reproduced @@ -246,7 +253,7 @@ describe('worker pool — startup self-healing (#1741)', () => { await pool.terminate(); expect(pool.getStats().terminated).toBe(true); // No ref'd backoff timer left pinning the loop after terminate. - expect(pool.getStats().pendingStartupTimers).toBe(0); + expect(pool.getStats()).toHaveProperty('pendingStartupTimers', 0); expect(pool.getStats().activeSlots).toBe(0); // No worker is spawned after terminate — the woken loop sees `terminated` and gives up. diff --git a/gitnexus/test/unit/worker-pool-stdout-forward.test.ts b/gitnexus/test/unit/worker-pool-stdout-forward.test.ts index 79e2654de..a6ab9c871 100644 --- a/gitnexus/test/unit/worker-pool-stdout-forward.test.ts +++ b/gitnexus/test/unit/worker-pool-stdout-forward.test.ts @@ -15,6 +15,7 @@ */ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'node:events'; +import type { Worker } from 'node:worker_threads'; import path from 'node:path'; import os from 'node:os'; import fs from 'node:fs'; diff --git a/gitnexus/test/unit/write-lock-required.test.ts b/gitnexus/test/unit/write-lock-required.test.ts index c4d3a5bd2..617f4205b 100644 --- a/gitnexus/test/unit/write-lock-required.test.ts +++ b/gitnexus/test/unit/write-lock-required.test.ts @@ -16,7 +16,7 @@ vi.mock('../../src/storage/index-lock.js', async (original) => ({ vi.mock('../../src/core/ingestion/pipeline.js', () => ({ runPipelineFromRepo: pipeline })); vi.mock('../../src/storage/repo-manager.js', async (original) => ({ ...(await original()), - resolveBranchPlacement: (...args: unknown[]) => resolvePlacement(...args), + resolveBranchPlacement: resolvePlacement, })); import { runFullAnalysis } from '../../src/core/run-analyze.js'; diff --git a/gitnexus/tsconfig.test.json b/gitnexus/tsconfig.test.json index 425ff7485..3f22f556a 100644 --- a/gitnexus/tsconfig.test.json +++ b/gitnexus/tsconfig.test.json @@ -6,5 +6,5 @@ "types": ["node", "vitest/globals"] }, "include": ["src/**/*", "test/**/*"], - "exclude": ["test/fixtures/mini-repo/**", "test/fixtures/sample-code/**"] + "exclude": ["node_modules", "test/fixtures/**"] }