diff --git a/gitnexus/test/helpers/optional-grammar.ts b/gitnexus/test/helpers/optional-grammar.ts index 1e573e947..d3901e1a2 100644 --- a/gitnexus/test/helpers/optional-grammar.ts +++ b/gitnexus/test/helpers/optional-grammar.ts @@ -19,15 +19,24 @@ import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js * platform its grammar publishes a prebuild for; otherwise the gate would fail * a job it cannot satisfy. */ -export const OPTIONAL_GRAMMAR_ENV: Partial> = { +export const OPTIONAL_GRAMMAR_ENV: Readonly>> = { // @tree-sitter-grammars/tree-sitter-zig@1.1.2 publishes prebuilds for // {darwin,linux,win32}-{x64,arm64} — every OS in the CI matrix. [SupportedLanguages.Zig]: 'GITNEXUS_REQUIRE_ZIG', -}; +} satisfies Partial>; -/** True when CI declared `language`'s optional grammar mandatory on this runner. */ -export const isOptionalGrammarRequired = (language: SupportedLanguages): boolean => { - const envVar = OPTIONAL_GRAMMAR_ENV[language]; +/** + * True when CI declared this grammar mandatory on the current runner. + * + * Keyed by GRAMMAR key, not by `SupportedLanguages`: the registry the ABI + * load-smoke walks (`listGrammarSources()`) yields one row per `SOURCES` entry, + * which includes variants such as `typescript:tsx` that are not enum members. + * Widening the parameter is what keeps that call honest — narrowing the key + * with a cast would claim every grammar row is a language, which is false. + * `satisfies` above still pins every key WE write to a real language. + */ +export const isOptionalGrammarRequired = (grammarKey: string): boolean => { + const envVar = OPTIONAL_GRAMMAR_ENV[grammarKey]; return envVar !== undefined && process.env[envVar] === '1'; }; diff --git a/gitnexus/test/integration/structural-pair-coverage.test.ts b/gitnexus/test/integration/structural-pair-coverage.test.ts index cfa1171b9..56af148c1 100644 --- a/gitnexus/test/integration/structural-pair-coverage.test.ts +++ b/gitnexus/test/integration/structural-pair-coverage.test.ts @@ -248,7 +248,10 @@ describeIfWorkerBuilt('RELATION_SCHEMA covers the non-bridge emitters', () => { }, ); - it.concurrent.each(OPTIONAL_GRAMMAR_CORPUS)( + // `.for`, not `.each`: only `for` passes the test context as a second + // argument (`each`'s callback is `(...args: T[])`), and the context is what + // carries the dynamic `skip()` this per-language gate needs. + it.concurrent.for(OPTIONAL_GRAMMAR_CORPUS)( '$fixture emits only declared FROM/TO pairs, and still reaches $emitter', async ({ fixture, language, sentinels }, ctx) => { if (!isLanguageAvailable(language)) ctx.skip(); diff --git a/gitnexus/test/unit/optional-grammar-gate.test.ts b/gitnexus/test/unit/optional-grammar-gate.test.ts index ddfbeb3c3..a118b8354 100644 --- a/gitnexus/test/unit/optional-grammar-gate.test.ts +++ b/gitnexus/test/unit/optional-grammar-gate.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, afterEach } from 'vitest'; import { SupportedLanguages } from 'gitnexus-shared'; +import { listGrammarSources } from '../../src/core/tree-sitter/parser-loader.js'; import { OPTIONAL_GRAMMAR_ENV, isOptionalGrammarRequired } from '../helpers/optional-grammar.js'; /** @@ -45,6 +46,25 @@ describe('optional grammar required-gate', () => { expect(isOptionalGrammarRequired(SupportedLanguages.Zig)).toBe(false); }); + // The ABI load-smoke passes a GRAMMAR key, not a SupportedLanguages value: + // listGrammarSources() has one row per SOURCES entry, including variants like + // `typescript:tsx`. A registry key no row ever yields would leave the gate + // permanently inert — it would look configured and require nothing. + it('registers only keys the grammar registry actually yields', () => { + const sources = listGrammarSources(); + for (const key of Object.keys(OPTIONAL_GRAMMAR_ENV)) { + const source = sources.find((s) => s.key === key); + expect(source, `${key} is not a grammar key listGrammarSources() yields`).toBeDefined(); + // Requiring a grammar that is already mandatory would be a no-op gate. + expect(source?.optional, `${key} is not an optional grammar`).toBe(true); + } + }); + + it('is inert for a grammar-key variant nobody registered', () => { + process.env[envVar] = '1'; + expect(isOptionalGrammarRequired('typescript:tsx')).toBe(false); + }); + // Languages with no entry must never be gated — an unregistered language // reading a stray env var would fail jobs on platforms with no prebuild. it('never requires a language that is not registered', () => { diff --git a/gitnexus/test/unit/zig-extractors.test.ts b/gitnexus/test/unit/zig-extractors.test.ts index 621712ca3..40bbd7474 100644 --- a/gitnexus/test/unit/zig-extractors.test.ts +++ b/gitnexus/test/unit/zig-extractors.test.ts @@ -78,7 +78,7 @@ describe.skipIf(!isOptionalGrammarRequired(SupportedLanguages.Zig))( const parser = new Parser(); const parse = (code: string) => { - parser.setLanguage(Zig as Parser.Language); + parser.setLanguage(Zig as Parameters[0]); return parser.parse(code); }; @@ -1178,8 +1178,11 @@ fn f() void { const imports = emitZigScopeCaptures(src, 'lp.zig') .filter((m) => m['@import.source'] !== undefined) .map((m) => interpretZigImport(m)) - .filter((i) => i !== null && (i.kind === 'named' || i.kind === 'alias')) - .map((i) => [i!.localName, (i as { reexportsName?: boolean }).reexportsName === true]); + .filter( + (i): i is Extract, { kind: 'named' | 'alias' }> => + i !== null && (i.kind === 'named' || i.kind === 'alias'), + ) + .map((i) => [i.localName, (i as { reexportsName?: boolean }).reexportsName === true]); expect(imports).toEqual([ ['Arena', true], // the file-struct TYPE twin of a pub namespace import ['Foo', true],