import { describe, expect, it } from 'vitest'; import type { ParsedFile } from 'gitnexus-shared'; import { pythonScopeResolver } from '../../../../src/core/ingestion/languages/python/scope-resolver.js'; import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js'; import { finalizeScopeModel } from '../../../../src/core/ingestion/finalize-orchestrator.js'; import { resolveCompoundReceiverClass } from '../../../../src/core/ingestion/scope-resolution/passes/compound-receiver.js'; import { collectNamespaceTargets } from '../../../../src/core/ingestion/scope-resolution/scope/namespace-targets.js'; import { buildWorkspaceResolutionIndex } from '../../../../src/core/ingestion/scope-resolution/workspace-index.js'; const SOURCES = new Map([ [ 'pkg/app.py', `from pkg import models from decoy import Missing def valid(): return models.User() def missing(): return models.Missing() def shadowed(models): return models.User() def locally_shadowed(): models = object() return models.User() `, ], [ 'pkg/models.py', `class User: pass `, ], ['pkg/__init__.py', '# package marker\n'], [ 'decoy.py', `class Missing: pass `, ], ]); function build() { const parsedFiles: ParsedFile[] = []; for (const [filePath, source] of SOURCES) { const parsed = extractParsedFile(pythonScopeResolver.languageProvider, source, filePath); if (parsed === undefined) throw new Error(`scope extraction failed for ${filePath}`); pythonScopeResolver.populateOwners(parsed); parsedFiles.push(parsed); } const allFilePaths = new Set(parsedFiles.map((file) => file.filePath)); const scopes = finalizeScopeModel(parsedFiles, { hooks: { resolveImportTarget: (targetRaw, fromFile, _workspaceIndex, parsedImport) => pythonScopeResolver.resolveImportTarget(targetRaw, fromFile, allFilePaths, undefined, { parsedFiles, parsedImport, }), isNamespaceImport: (parsedImport, targetFile, fromFile) => pythonScopeResolver.isNamespaceImport?.(parsedImport, targetFile, fromFile) ?? false, mergeBindings: (existing, incoming, scopeId) => pythonScopeResolver.mergeBindings(existing, incoming, scopeId), }, }); const index = buildWorkspaceResolutionIndex(parsedFiles); const app = parsedFiles.find((file) => file.filePath === 'pkg/app.py'); if (app === undefined) throw new Error('missing app fixture'); const namespaceTargets = collectNamespaceTargets(app, scopes); const resolveIn = (functionName: string, expression: string) => { const functionScope = app.scopes.find( (scope) => scope.kind === 'Function' && scope.ownedDefs.some((def) => def.qualifiedName === functionName), ); if (functionScope === undefined) throw new Error(`missing scope for ${functionName}`); return resolveCompoundReceiverClass(expression, functionScope.id, scopes, index, { constructionSyntax: { bare: true }, namespaceTargets, }); }; return { resolveIn }; } describe('Python module namespace construction', () => { const { resolveIn } = build(); it('resolves an exported class from the verified module target', () => { expect(resolveIn('valid', 'models.User()')).toMatchObject({ filePath: 'pkg/models.py', qualifiedName: 'User', }); }); it('fails closed when the module lacks the requested class', () => { expect(resolveIn('missing', 'models.Missing()')).toBeUndefined(); }); it('does not reuse a file-level namespace when a parameter shadows it', () => { expect(resolveIn('shadowed', 'models.User()')).toBeUndefined(); }); it('does not reuse a file-level namespace when a local shadows it', () => { expect(resolveIn('locally_shadowed', 'models.User()')).toBeUndefined(); }); });