diff --git a/.github/workflows/ci-scope-parity.yml b/.github/workflows/ci-scope-parity.yml index e8efde9f15..cffdeb3415 100644 --- a/.github/workflows/ci-scope-parity.yml +++ b/.github/workflows/ci-scope-parity.yml @@ -11,7 +11,8 @@ name: Scope Resolution Parity # TWICE on every PR: # # 1. `REGISTRY_PRIMARY_=0` — legacy DAG path (guarantees we haven't -# broken the old path while migrating). +# broken the old path while migrating). Known legacy gaps may be skipped +# through the resolver test helper's expected-failure list. # 2. `REGISTRY_PRIMARY_=1` — registry-primary path (guarantees the # new path carries the same behavior — the parity gate). # diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 1d41bb1708..3b8feb812a 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -1,11 +1,12 @@ /** * C#: heritage resolution via base_list + ambiguous namespace-import refusal */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, expect, beforeAll } from 'vitest'; import path from 'path'; import { FIXTURES, CROSS_FILE_FIXTURES, + createResolverParityIt, getRelationships, getNodesByLabel, getNodesByLabelFull, @@ -14,6 +15,8 @@ import { type PipelineResult, } from './helpers.js'; +const it = createResolverParityIt('csharp'); + // --------------------------------------------------------------------------- // Heritage: class + interface resolution via base_list // --------------------------------------------------------------------------- diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index 4e394c0f0c..710fcddeb1 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -2,10 +2,54 @@ * Shared test helpers for language resolution integration tests. */ import path from 'path'; +import { it as vitestIt } from 'vitest'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; import type { PipelineOptions } from '../../../src/core/ingestion/pipeline.js'; import type { PipelineResult } from '../../../src/types/pipeline.js'; -import type { GraphRelationship } from '../../../src/core/graph/types.js'; +import type { GraphRelationship } from 'gitnexus-shared'; + +const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly>> = { + csharp: new Set([ + 'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path', + ]), +}; + +type ResolverParityEnv = Readonly>; +type VitestIt = typeof vitestIt; +type CallableIt = (name: string, ...args: unknown[]) => unknown; + +export function resolverParityFlagName(languageSlug: string): string { + return `REGISTRY_PRIMARY_${languageSlug.toUpperCase().replace(/-/g, '_')}`; +} + +export function isLegacyResolverParityRun( + languageSlug: string, + env: ResolverParityEnv = process.env, +): boolean { + const value = env[resolverParityFlagName(languageSlug)]?.trim().toLowerCase(); + return value === '0' || value === 'false' || value === 'no'; +} + +export function isLegacyResolverParityExpectedFailure( + languageSlug: string, + testName: string, + env: ResolverParityEnv = process.env, +): boolean { + if (!isLegacyResolverParityRun(languageSlug, env)) return false; + return LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES[languageSlug]?.has(testName) ?? false; +} + +export function createResolverParityIt(languageSlug: string): VitestIt { + const wrapped = ((name: string, ...args: unknown[]) => { + const runner = isLegacyResolverParityExpectedFailure(languageSlug, name) + ? vitestIt.skip + : vitestIt; + return (runner as unknown as CallableIt)(name, ...args); + }) as VitestIt; + + Object.assign(wrapped, vitestIt); + return wrapped; +} export const FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution'); export const CROSS_FILE_FIXTURES = path.resolve( diff --git a/gitnexus/test/unit/scope-resolution/resolver-parity-expected-failures.test.ts b/gitnexus/test/unit/scope-resolution/resolver-parity-expected-failures.test.ts new file mode 100644 index 0000000000..eae68e09d9 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/resolver-parity-expected-failures.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { + isLegacyResolverParityExpectedFailure, + isLegacyResolverParityRun, + resolverParityFlagName, +} from '../../integration/resolvers/helpers.js'; + +const csharpNamespaceRootImportTest = + 'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path'; + +describe('resolver parity expected legacy failures', () => { + it('uses the same env var convention as the parity workflow', () => { + expect(resolverParityFlagName('csharp')).toBe('REGISTRY_PRIMARY_CSHARP'); + expect(resolverParityFlagName('c-plus-plus')).toBe('REGISTRY_PRIMARY_C_PLUS_PLUS'); + }); + + it('recognizes only legacy parity runs', () => { + expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: '0' })).toBe(true); + expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: 'false' })).toBe(true); + expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: '1' })).toBe(false); + expect(isLegacyResolverParityRun('csharp', {})).toBe(false); + }); + + it('matches configured expected failures only during the legacy run', () => { + expect( + isLegacyResolverParityExpectedFailure('csharp', csharpNamespaceRootImportTest, { + REGISTRY_PRIMARY_CSHARP: '0', + }), + ).toBe(true); + + expect( + isLegacyResolverParityExpectedFailure('csharp', csharpNamespaceRootImportTest, { + REGISTRY_PRIMARY_CSHARP: '1', + }), + ).toBe(false); + + expect( + isLegacyResolverParityExpectedFailure( + 'csharp', + 'detects exactly 3 classes and 2 interfaces', + { + REGISTRY_PRIMARY_CSHARP: '0', + }, + ), + ).toBe(false); + }); +});