/** * Unit tests for the import resolver factory and per-strategy composition. * * Validates that: * - createImportResolver chains strategies in order * - First non-null result wins * - Empty files array stops the chain (absorbing sentinel) * - All per-language configs produce valid resolvers */ import { describe, it, expect } from 'vitest'; import { createImportResolver } from '../../src/core/ingestion/import-resolvers/resolver-factory.js'; import { createStandardStrategy } from '../../src/core/ingestion/import-resolvers/standard.js'; import type { ImportResolutionConfig, ImportResolverStrategy, ResolveCtx, } from '../../src/core/ingestion/import-resolvers/types.js'; import { SupportedLanguages } from 'gitnexus-shared'; import { buildSuffixIndex } from '../../src/core/ingestion/import-resolvers/utils.js'; // ── Per-language strategy imports (from config files) ────────────────── import { goPackageStrategy } from '../../src/core/ingestion/import-resolvers/configs/go.js'; import { javaJvmStrategy, kotlinJvmStrategy, } from '../../src/core/ingestion/import-resolvers/configs/jvm.js'; import { pythonImportStrategy } from '../../src/core/ingestion/import-resolvers/configs/python.js'; import { csharpNamespaceStrategy } from '../../src/core/ingestion/import-resolvers/configs/csharp.js'; import { dartPackageStrategy } from '../../src/core/ingestion/import-resolvers/configs/dart.js'; import { rustModuleStrategy } from '../../src/core/ingestion/import-resolvers/configs/rust.js'; import { phpPsr4Strategy } from '../../src/core/ingestion/import-resolvers/configs/php.js'; import { swiftPackageStrategy } from '../../src/core/ingestion/import-resolvers/configs/swift.js'; import { rubyRequireStrategy } from '../../src/core/ingestion/import-resolvers/configs/ruby.js'; // ── Per-language config imports ──────────────────────────────────────── import { typescriptImportConfig, javascriptImportConfig, vueImportConfig, } from '../../src/core/ingestion/import-resolvers/configs/typescript-javascript.js'; import { cImportConfig, cppImportConfig, } from '../../src/core/ingestion/import-resolvers/configs/c-cpp.js'; import { goImportConfig } from '../../src/core/ingestion/import-resolvers/configs/go.js'; import { javaImportConfig, kotlinImportConfig, } from '../../src/core/ingestion/import-resolvers/configs/jvm.js'; import { pythonImportConfig } from '../../src/core/ingestion/import-resolvers/configs/python.js'; import { rustImportConfig } from '../../src/core/ingestion/import-resolvers/configs/rust.js'; import { csharpImportConfig } from '../../src/core/ingestion/import-resolvers/configs/csharp.js'; import { phpImportConfig } from '../../src/core/ingestion/import-resolvers/configs/php.js'; import { swiftImportConfig } from '../../src/core/ingestion/import-resolvers/configs/swift.js'; import { dartImportConfig } from '../../src/core/ingestion/import-resolvers/configs/dart.js'; import { rubyImportConfig } from '../../src/core/ingestion/import-resolvers/configs/ruby.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeCtx(files: string[], overrides: Partial = {}): ResolveCtx { const allFileList = files; const normalizedFileList = files.map((p) => p.replace(/\\/g, '/')); const allFilePaths = new Set(allFileList); const index = buildSuffixIndex(normalizedFileList, allFileList); return { allFilePaths, allFileList, normalizedFileList, index, resolveCache: new Map(), configs: { tsconfigPaths: null, goModule: null, composerConfig: null, swiftPackageConfig: null, csharpConfigs: [], ...overrides, }, }; } // --------------------------------------------------------------------------- // createImportResolver — factory behavior // --------------------------------------------------------------------------- describe('createImportResolver', () => { it('returns null when no strategies match', () => { const config: ImportResolutionConfig = { language: SupportedLanguages.TypeScript, strategies: [], }; const resolver = createImportResolver(config); expect(resolver('./foo', 'src/index.ts', makeCtx([]))).toBeNull(); }); it('returns first non-null result from strategy chain', () => { const strategyA: ImportResolverStrategy = () => null; const strategyB: ImportResolverStrategy = () => ({ kind: 'files', files: ['b.ts'] }); const strategyC: ImportResolverStrategy = () => ({ kind: 'files', files: ['c.ts'] }); const resolver = createImportResolver({ language: SupportedLanguages.TypeScript, strategies: [strategyA, strategyB, strategyC], }); const result = resolver('./foo', 'src/index.ts', makeCtx(['b.ts', 'c.ts'])); expect(result).toEqual({ kind: 'files', files: ['b.ts'] }); }); it('stops chain when strategy returns result with empty files (absorbing sentinel)', () => { const absorber: ImportResolverStrategy = () => ({ kind: 'files', files: [] }); const shouldNotRun: ImportResolverStrategy = () => ({ kind: 'files', files: ['should-not.ts'], }); const resolver = createImportResolver({ language: SupportedLanguages.TypeScript, strategies: [absorber, shouldNotRun], }); const result = resolver('./foo', 'src/index.ts', makeCtx([])); expect(result).toEqual({ kind: 'files', files: [] }); }); it('passes correct arguments to strategies', () => { const ctx = makeCtx(['src/utils.ts']); const captured: { raw: string; fp: string }[] = []; const spy: ImportResolverStrategy = (raw, fp) => { captured.push({ raw, fp }); return null; }; const resolver = createImportResolver({ language: SupportedLanguages.TypeScript, strategies: [spy], }); resolver('./utils', 'src/index.ts', ctx); expect(captured).toEqual([{ raw: './utils', fp: 'src/index.ts' }]); }); }); // --------------------------------------------------------------------------- // Per-language strategies — behavioral coverage // // The previous `typeof strategy === 'function'` assertions were tautological: // TypeScript's `ImportResolverStrategy` type enforces the function shape at // compile time, so those tests could never fail. Each strategy now has at // least one behavioral assertion below; the Go and C# full-chain tests also // double as strategy-order guards because their `kind: 'package'` output is // unreachable through `createStandardStrategy`. // --------------------------------------------------------------------------- describe('goPackageStrategy', () => { it('resolves go.mod package imports to a package result with dirSuffix', () => { const files = ['cmd/server/main.go', 'cmd/server/handler.go']; const ctx = makeCtx(files, { goModule: { modulePath: 'example.com/app' } }); const result = goPackageStrategy('example.com/app/cmd/server', 'main.go', ctx); // `kind: 'package'` + `dirSuffix` is unique to goPackageStrategy — the // standard strategy always returns `kind: 'files'`. Asserting the exact // shape makes format regressions observable (e.g. prefix/suffix slash // normalization) and makes strategy ordering observable via the // full-chain test in `goImportConfig` below. expect(result).toEqual({ kind: 'package', files: expect.arrayContaining(files), dirSuffix: '/cmd/server/', }); }); it('returns null for imports outside the go module (allows chain to continue)', () => { const ctx = makeCtx(['vendor/other/pkg/foo.go'], { goModule: { modulePath: 'example.com/app' }, }); const result = goPackageStrategy('github.com/other/pkg', 'main.go', ctx); expect(result).toBeNull(); }); it('returns null when goModule is not configured', () => { const ctx = makeCtx(['cmd/server/main.go']); expect(ctx.configs.goModule).toBeNull(); const result = goPackageStrategy('example.com/app/cmd/server', 'main.go', ctx); expect(result).toBeNull(); }); it('returns null when module prefix matches but package directory has no .go files', () => { // Documented fall-through in configs/go.ts:27 — when resolveGoPackageDir // returns non-null but resolveGoPackage returns an empty files list // (external module coincidentally sharing the prefix, or a subpackage with // no .go sources), the strategy returns null so the chain can continue. const ctx = makeCtx(['README.md', 'cmd/server/config.yaml'], { goModule: { modulePath: 'example.com/app' }, }); const result = goPackageStrategy('example.com/app/cmd/server', 'main.go', ctx); expect(result).toBeNull(); }); it('goImportConfig full chain produces the package-kind result (strategy-order guard)', () => { const files = ['cmd/server/main.go', 'cmd/server/handler.go']; const ctx = makeCtx(files, { goModule: { modulePath: 'example.com/app' } }); const resolver = createImportResolver(goImportConfig); const result = resolver('example.com/app/cmd/server', 'main.go', ctx); // If goPackageStrategy were moved after createStandardStrategy, the // standard strategy's suffix resolution would return a single file with // `kind: 'files'` (or null), not `kind: 'package'` with a dirSuffix. // Asserting the full shape (not just kind) guards against a future // regression that emits `kind: 'package'` with mismatched files. expect(result).toEqual({ kind: 'package', files: expect.arrayContaining(files), dirSuffix: '/cmd/server/', }); }); }); describe('javaJvmStrategy', () => { it('resolves wildcard imports to .java files in the package directory', () => { const files = [ 'src/main/java/com/example/foo/Bar.java', 'src/main/java/com/example/foo/Baz.java', 'src/main/java/com/example/other/Unrelated.java', ]; const ctx = makeCtx(files); const result = javaJvmStrategy('com.example.foo.*', 'App.java', ctx); expect(result?.kind).toBe('files'); expect(result?.files).toEqual( expect.arrayContaining([ 'src/main/java/com/example/foo/Bar.java', 'src/main/java/com/example/foo/Baz.java', ]), ); expect(result?.files).not.toContain('src/main/java/com/example/other/Unrelated.java'); }); it('resolves member imports to the owning class file', () => { const files = ['src/main/java/com/example/Constants.java']; const ctx = makeCtx(files); // JVM member import: last segment is an ALL_CAPS constant / lowercase // member; resolver walks back to the class path `com/example/Constants`. const result = javaJvmStrategy('com.example.Constants.VALUE', 'App.java', ctx); expect(result).toEqual({ kind: 'files', files }); }); it('returns null when no matching .java files exist', () => { const ctx = makeCtx(['src/main/java/other/Foo.java']); const result = javaJvmStrategy('com.example.missing.*', 'App.java', ctx); expect(result).toBeNull(); }); }); describe('kotlinJvmStrategy', () => { it('resolves wildcard imports to files in the package directory', () => { const files = [ 'src/main/kotlin/com/example/foo/Bar.kt', 'src/main/kotlin/com/example/foo/Baz.kt', 'src/main/kotlin/com/example/other/Unrelated.kt', ]; const ctx = makeCtx(files); const result = kotlinJvmStrategy('com.example.foo.*', 'App.kt', ctx); expect(result?.kind).toBe('files'); expect(result?.files).toEqual( expect.arrayContaining([ 'src/main/kotlin/com/example/foo/Bar.kt', 'src/main/kotlin/com/example/foo/Baz.kt', ]), ); expect(result?.files).not.toContain('src/main/kotlin/com/example/other/Unrelated.kt'); }); it('resolves member imports to the owning class file', () => { const files = ['src/main/kotlin/com/example/Constants.kt']; const ctx = makeCtx(files); const result = kotlinJvmStrategy('com.example.Constants.VALUE', 'App.kt', ctx); expect(result).toEqual({ kind: 'files', files }); }); it('returns null for wildcard with no matching files (allows chain to continue)', () => { const ctx = makeCtx(['src/main/kotlin/com/example/other/Foo.kt']); const result = kotlinJvmStrategy('com.example.missing.*', 'App.kt', ctx); expect(result).toBeNull(); }); it('kotlinImportConfig full chain resolves wildcard via the JVM strategy', () => { // Note: this is a behavioral smoke test, NOT a strategy-order guard. // `createStandardStrategy(Kotlin)` explicitly returns null for any import // ending in `.*` (see standard.ts:137), so reordering `kotlinImportConfig` // strategies would still produce the same result — the standard strategy // would return null and the chain would fall through to kotlinJvmStrategy // regardless of position. A genuine ordering guard for Kotlin would need // a non-wildcard fixture where both strategies resolve to different files. const files = ['src/main/kotlin/com/example/foo/Bar.kt']; const ctx = makeCtx(files); const resolver = createImportResolver(kotlinImportConfig); const result = resolver('com.example.foo.*', 'App.kt', ctx); expect(result).toEqual({ kind: 'files', files }); }); }); describe('rustModuleStrategy', () => { it('resolves scoped grouped imports to per-item files', () => { const files = ['src/models/User.rs', 'src/models/Repo.rs']; const ctx = makeCtx(files); // `use crate::models::{User, Repo}` — the strategy resolves each item // individually against the allFilePaths set via resolveRustImportInternal. const result = rustModuleStrategy('crate::models::{User, Repo}', 'src/lib.rs', ctx); // Exact file discovery depends on resolveRustImportInternal's search // heuristics; we assert the shape + that at least one item resolved. expect(result?.kind).toBe('files'); expect(result?.files.length).toBeGreaterThan(0); }); it('returns null for non-grouped imports (delegates to standard fallback)', () => { const ctx = makeCtx(['src/models.rs']); // Plain `use crate::models` — rustModuleStrategy only handles grouped // forms; standard strategy handles the crate:: -> path translation. const result = rustModuleStrategy('crate::models', 'src/lib.rs', ctx); expect(result).toBeNull(); }); }); describe('csharpNamespaceStrategy', () => { it('resolves namespace imports via .csproj root-namespace mapping', () => { const files = ['src/Services/Auth/AuthService.cs', 'src/Services/Auth/TokenService.cs']; const ctx = makeCtx(files, { csharpConfigs: [{ rootNamespace: 'MyCo', projectDir: 'src' }], }); const result = csharpNamespaceStrategy('MyCo.Services.Auth', 'App.cs', ctx); // Multi-file namespace resolution produces `kind: 'package'` with // dirSuffix — unique to csharpNamespaceStrategy; the standard strategy // always emits `kind: 'files'`. Asserting the full shape (including the // exact dirSuffix format) guards against format regressions. expect(result).toEqual({ kind: 'package', files: expect.arrayContaining(files), dirSuffix: '/src/Services/Auth/', }); }); it('returns null when no csharpConfigs are configured', () => { const ctx = makeCtx(['src/Services/Auth/AuthService.cs']); expect(ctx.configs.csharpConfigs).toEqual([]); const result = csharpNamespaceStrategy('MyCo.Services.Auth', 'App.cs', ctx); expect(result).toBeNull(); }); it('no csproj + non-aligned BCL import: stops the chain instead of the ungated standard strategy (#2)', () => { // Parity with the registry leg's no-csproj path. Without csproj configs the // generic strategy would suffix-match `System.Threading.Tasks` onto the // coincidental local `Legacy/Tasks.cs`. The gate sees the import aligns // with no declared namespace, so the strategy returns an absorbing sentinel // (`{ kind: 'files', files: [] }`) that STOPS the chain — the standard // strategy never runs and no spurious edge is emitted. const ctx = makeCtx(['Services/OrderService.cs', 'Legacy/Tasks.cs'], { csharpNamespaces: { declaredNamespaces: new Set(['MyApp.Services', 'MyApp.Legacy']), rootNamespaces: new Set(['MyApp']), truncated: false, }, }); const result = csharpNamespaceStrategy( 'System.Threading.Tasks', 'Services/OrderService.cs', ctx, ); expect(result).toEqual({ kind: 'files', files: [] }); }); it('no csproj + in-repo-aligned import: keeps delegating to the standard strategy (#2)', () => { // An import that DOES align with a declared namespace must keep returning // null so the generic strategy resolves it — legitimate no-csproj behavior // is unchanged; only non-aligned (BCL) imports are stopped. const ctx = makeCtx(['Services/OrderService.cs', 'Models/User.cs'], { csharpNamespaces: { declaredNamespaces: new Set(['MyApp.Models', 'MyApp.Services']), rootNamespaces: new Set(['MyApp']), truncated: false, }, }); const result = csharpNamespaceStrategy('MyApp.Models', 'Services/OrderService.cs', ctx); expect(result).toBeNull(); }); it('returns an empty files result (chain-stop) for a gated BCL import when csproj configs exist (#1881, #8)', () => { // Legacy DAG leg of #1881: with csproj configs present, a BCL using like // `System.Threading.Tasks` must NOT suffix-match the coincidental local // `Legacy/Tasks.cs`. The strategy returns `{ kind: 'files', files: [] }` // (absorbing sentinel) to STOP the chain, NOT null — null would let the // generic suffix fallback re-introduce the spurious edge. const ctx = makeCtx(['Services/OrderService.cs', 'Legacy/Tasks.cs'], { csharpConfigs: [{ rootNamespace: 'MyApp', projectDir: '' }], csharpNamespaces: { declaredNamespaces: new Set(['MyApp.Services', 'MyApp.Legacy']), rootNamespaces: new Set(['MyApp']), truncated: false, }, }); const result = csharpNamespaceStrategy( 'System.Threading.Tasks', 'Services/OrderService.cs', ctx, ); expect(result).toEqual({ kind: 'files', files: [] }); }); it('csharpImportConfig full chain produces package-kind (strategy-order guard)', () => { const files = ['src/Services/Auth/AuthService.cs', 'src/Services/Auth/TokenService.cs']; const ctx = makeCtx(files, { csharpConfigs: [{ rootNamespace: 'MyCo', projectDir: 'src' }], }); const resolver = createImportResolver(csharpImportConfig); const result = resolver('MyCo.Services.Auth', 'App.cs', ctx); // If csharpNamespaceStrategy were reordered after createStandardStrategy, // the result would be `kind: 'files'` (suffix match) or null, never // `kind: 'package'` with a dirSuffix. Asserting the full shape (not just // kind) guards against a regression that emits `kind: 'package'` with // mismatched files. expect(result).toEqual({ kind: 'package', files: expect.arrayContaining(files), dirSuffix: '/src/Services/Auth/', }); }); }); describe('phpPsr4Strategy', () => { it('resolves PSR-4 namespace imports via composer.json autoload map', () => { const files = ['app/Services/UserService.php']; const ctx = makeCtx(files, { composerConfig: { psr4: new Map([['App\\', 'app/']]), }, }); const result = phpPsr4Strategy('App\\Services\\UserService', 'index.php', ctx); expect(result).toEqual({ kind: 'files', files }); }); it('returns null when no file matches the namespace via PSR-4 or suffix fallback', () => { const ctx = makeCtx(['app/Services/OtherService.php'], { composerConfig: { psr4: new Map([['App\\', 'app/']]) }, }); const result = phpPsr4Strategy('App\\Services\\UserService', 'index.php', ctx); expect(result).toBeNull(); }); }); describe('swiftPackageStrategy', () => { it('resolves SwiftPM target imports to files in the target directory', () => { const files = [ 'Package/Sources/SiuperModel/Foo.swift', 'Package/Sources/SiuperModel/Bar.swift', 'Package/Sources/Other/Unrelated.swift', ]; const ctx = makeCtx(files, { swiftPackageConfig: { targets: new Map([['SiuperModel', 'Package/Sources/SiuperModel']]), }, }); const result = swiftPackageStrategy('SiuperModel', 'App.swift', ctx); expect(result?.kind).toBe('files'); expect(result?.files).toEqual( expect.arrayContaining([ 'Package/Sources/SiuperModel/Foo.swift', 'Package/Sources/SiuperModel/Bar.swift', ]), ); expect(result?.files).not.toContain('Package/Sources/Other/Unrelated.swift'); }); it('returns null for unknown targets (external frameworks like Foundation)', () => { const ctx = makeCtx(['Package/Sources/SiuperModel/Foo.swift'], { swiftPackageConfig: { targets: new Map([['SiuperModel', 'Package/Sources/SiuperModel']]), }, }); const result = swiftPackageStrategy('Foundation', 'App.swift', ctx); expect(result).toBeNull(); }); it('does not resolve inferred grouping folders when the declaration is empty', () => { const ctx = makeCtx(['Sources/Foundation/Thing.swift', 'Sources/App/main.swift'], { swiftPackageConfig: { origin: 'package.swift', targets: new Map([ ['Foundation', 'Sources/Foundation'], ['App', 'Sources/App'], ]), declaredTargets: new Map(), }, }); expect(swiftPackageStrategy('Foundation', 'Sources/App/main.swift', ctx)).toBeNull(); expect(swiftPackageStrategy('App', 'Sources/App/main.swift', ctx)).toBeNull(); }); it('ignores an inferred directories origin', () => { const ctx = makeCtx(['Sources/Models/User.swift'], { swiftPackageConfig: { origin: 'directories', targets: new Map([['Models', 'Sources/Models']]), }, }); expect(swiftPackageStrategy('Models', 'Sources/App/main.swift', ctx)).toBeNull(); }); }); describe('rubyRequireStrategy', () => { it('resolves require_relative paths via suffix matching', () => { const files = ['lib/models/user.rb']; const ctx = makeCtx(files); const result = rubyRequireStrategy('./models/user', 'lib/app.rb', ctx); expect(result).toEqual({ kind: 'files', files }); }); it('returns null when no file matches the require path', () => { const ctx = makeCtx(['lib/models/user.rb']); const result = rubyRequireStrategy('./missing/file', 'lib/app.rb', ctx); expect(result).toBeNull(); }); }); // --------------------------------------------------------------------------- // Per-language configs — all construct cleanly // --------------------------------------------------------------------------- describe('per-language import configs', () => { const configs: { name: string; config: ImportResolutionConfig }[] = [ { name: 'TypeScript', config: typescriptImportConfig }, { name: 'JavaScript', config: javascriptImportConfig }, { name: 'Vue', config: vueImportConfig }, { name: 'C', config: cImportConfig }, { name: 'C++', config: cppImportConfig }, { name: 'Go', config: goImportConfig }, { name: 'Java', config: javaImportConfig }, { name: 'Kotlin', config: kotlinImportConfig }, { name: 'Python', config: pythonImportConfig }, { name: 'Rust', config: rustImportConfig }, { name: 'C#', config: csharpImportConfig }, { name: 'PHP', config: phpImportConfig }, { name: 'Swift', config: swiftImportConfig }, { name: 'Dart', config: dartImportConfig }, { name: 'Ruby', config: rubyImportConfig }, ]; for (const { name, config } of configs) { it(`${name} config has strategies and constructs a resolver`, () => { expect(config.strategies.length).toBeGreaterThan(0); expect(() => createImportResolver(config)).not.toThrow(); }); } }); // --------------------------------------------------------------------------- // createStandardStrategy — reusable across languages // --------------------------------------------------------------------------- describe('createStandardStrategy', () => { it('creates a reusable strategy from a language', () => { const strategy = createStandardStrategy(SupportedLanguages.TypeScript); expect(typeof strategy).toBe('function'); }); it('resolves relative imports', () => { const strategy = createStandardStrategy(SupportedLanguages.TypeScript); const ctx = makeCtx(['src/utils.ts']); const result = strategy('./utils', 'src/index.ts', ctx); expect(result).toEqual({ kind: 'files', files: ['src/utils.ts'] }); }); it('returns null for unresolvable imports', () => { const strategy = createStandardStrategy(SupportedLanguages.TypeScript); const ctx = makeCtx([]); const result = strategy('./nonexistent', 'src/index.ts', ctx); expect(result).toBeNull(); }); }); // --------------------------------------------------------------------------- // Python strategy — absorbs unresolved relative imports // --------------------------------------------------------------------------- describe('pythonImportStrategy', () => { it('absorbs unresolved relative imports with empty-files sentinel', () => { const ctx = makeCtx([]); const result = pythonImportStrategy('.nonexistent', 'src/app.py', ctx); expect(result).toEqual({ kind: 'files', files: [] }); }); it('returns null for non-relative imports (allows chain to continue)', () => { const ctx = makeCtx([]); const result = pythonImportStrategy('os', 'src/app.py', ctx); expect(result).toBeNull(); }); it('absorbs unresolved external dotted imports instead of suffix-matching local basename files', () => { const ctx = makeCtx(['accounts/apps.py', 'billing/apps.py']); const result = pythonImportStrategy('django.apps', 'accounts/apps.py', ctx); expect(result).toEqual({ kind: 'files', files: [] }); }); it('absorbs unresolved nested external dotted imports like django.urls and django.core.*', () => { const ctx = makeCtx(['config/asgi.py', 'config/urls.py', 'config/wsgi.py']); expect(pythonImportStrategy('django.urls', 'config/urls.py', ctx)).toEqual({ kind: 'files', files: [], }); expect(pythonImportStrategy('django.core.asgi', 'config/asgi.py', ctx)).toEqual({ kind: 'files', files: [], }); expect(pythonImportStrategy('django.core.wsgi', 'config/wsgi.py', ctx)).toEqual({ kind: 'files', files: [], }); }); it('keeps dotted internal imports unresolved here when the leading package exists in-repo', () => { const ctx = makeCtx(['accounts/models.py', 'billing/models.py']); const result = pythonImportStrategy('accounts.models', 'billing/models.py', ctx); expect(result).toBeNull(); }); }); // --------------------------------------------------------------------------- // Dart strategies — absorbs SDK / external package imports // --------------------------------------------------------------------------- describe('dartPackageStrategy', () => { it('absorbs dart: SDK imports', () => { const ctx = makeCtx([]); const result = dartPackageStrategy("'dart:async'", 'lib/main.dart', ctx); expect(result).toEqual({ kind: 'files', files: [] }); }); it('absorbs external package: imports', () => { const ctx = makeCtx([]); const result = dartPackageStrategy("'package:http/http.dart'", 'lib/main.dart', ctx); expect(result).toEqual({ kind: 'files', files: [] }); }); it('returns null for relative imports (chains to dartRelativeStrategy)', () => { const ctx = makeCtx([]); const result = dartPackageStrategy("'models.dart'", 'lib/main.dart', ctx); expect(result).toBeNull(); }); });