/** * Production-path regression guard for PR #1918 review finding P1. * * The Python file index (`getPythonFileIndex` in * `import-resolvers/python-file-index.ts`) is * memoized on the `allFilePaths` Set identity via a WeakMap. The registry- * primary path reaches it through `pythonScopeResolver.resolveImportTarget` * (the orchestrator adapter) — NOT by calling `resolvePythonImportTarget` * directly the way the unit parity test does. Before the fix, that adapter * copied the set (`new Set(allFilePaths)`) on every import, handing a fresh * WeakMap key per call so the index rebuilt every import (O(imports × files)). * * This test drives the adapter exactly as the orchestrator does and asserts the * file set is traversed ONCE across many imports on a stable set. It fails * (one traversal per import) if the per-import copy is reintroduced. * * ## Why this counts TRAVERSALS and not index builds (#2909) * * This guard used to read a build counter that shipped in production purely so * a test could read it (now deleted). `CountingSet` * (`test/helpers/counting-file-set.ts`) replaces it, and the swap is not a * wash: * * - STRICTLY MORE COVERAGE. A scan added BESIDE a reused index moves no build * count — the cache still hits, the counter still reads 1 — but it does move * the traversal count. That mutation is the one `bench/import-target/` * provably cannot see either: `baselines.json` `_blind_spot` records a full * workspace scan on 1-in-32 imports passing every timing arm. * - LESS PRODUCTION SURFACE. ~30 lines shipped in the bundle whose only caller * outside a cache miss was this file. * - PARALLEL-SAFE. The counter lives on the instance the test built, so there * is no module-global to `reset()` and no ordering hazard between tests. * * The traversal-count assertions are the perf guard. They are paired with result * assertions on purpose: a count of 1 is equally true of an adapter that has * stopped resolving anything at all, so counting alone would stay green while * every Python IMPORTS edge disappeared. */ import { describe, it, expect } from 'vitest'; import { pythonScopeResolver } from '../../src/core/ingestion/languages/python/scope-resolver.js'; import { CountingSet, expectDistinctFileSetsGetOwnIndex } from '../helpers/counting-file-set.js'; const { resolveImportTarget } = pythonScopeResolver; const FROM_FILE = 'app/main.py'; /** * A synthetic workspace: a real package (`realpkg/__init__.py`, so the * `hasRepoCandidate` gate passes) plus many unrelated modules. The imports * below are multi-segment and miss every fast path, so each call reaches both * `hasRepoCandidate` and `resolveAbsoluteFromFiles` — the two index consumers. */ function buildWorkspace(fileCount: number): CountingSet { const files: string[] = []; for (let i = 0; i < fileCount; i++) { files.push(`pkg/sub/mod${String(i).padStart(5, '0')}.py`); } files.push('realpkg/__init__.py'); files.push('realpkg/widget.py'); return new CountingSet(files); } describe('Python import resolution — index reuse across imports (PR #1918 P1)', () => { it('builds the file index once for many imports over a stable file set', () => { const files = buildWorkspace(300); const resolved: (string | readonly string[] | null)[] = []; for (let i = 0; i < 300; i++) { // Multi-segment, candidate-passing, suffix-miss → reaches the index. resolved.push(resolveImportTarget(`realpkg.ghost${i}`, FROM_FILE, files, undefined)); } resolved.push(resolveImportTarget('realpkg.widget', FROM_FILE, files, undefined)); // The whole point of PR #1918: O(imports + files), not O(imports × files). // Pre-fix this was 300 (one rebuild per import via the adapter's Set copy). expect(files.scans).toBe(1); // Paired result assertions — a count of 1 must not be the count of an // adapter that resolves nothing. expect(resolved[0]).toBeNull(); expect(resolved[300]).toBe('realpkg/widget.py'); }); it('a distinct file set gets its own index (per-run isolation, no stale reuse)', () => { expectDistinctFileSetsGetOwnIndex({ resolveImportTarget, buildWorkspace: () => buildWorkspace(50), targetRaw: 'realpkg.widget', fromFile: FROM_FILE, resolutionConfig: undefined, expected: 'realpkg/widget.py', expectedScans: 1, }); }); it('still resolves real imports correctly (the perf test is not vacuous)', () => { const files = buildWorkspace(20); // Suffix-fallback hit through the adapter: realpkg.widget → realpkg/widget.py. expect(resolveImportTarget('realpkg.widget', FROM_FILE, files, undefined)).toBe( 'realpkg/widget.py', ); // Gated-out / unresolvable import returns null. expect(resolveImportTarget('realpkg.ghost', FROM_FILE, files, undefined)).toBeNull(); // All of it off one traversal. expect(files.scans).toBe(1); }); });