diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 24e80bc5..907d90c3 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -54,7 +54,8 @@ async function main() { const projectId = process.env.PROJECT_ID!; // Which program to drive — PROGRAM env from the workbench e2e runner; - // defaults to the integration flow. getProgramConfig throws on unknown ids. + // defaults to the integration flow. getProgramConfig throws and names the id + // when PROGRAM holds a typo. const programId = (process.env.PROGRAM || Program.PostHogIntegration) as ProgramId; const programConfig = getProgramConfig(programId); diff --git a/src/lib/programs/__tests__/program-registry.test.ts b/src/lib/programs/__tests__/program-registry.test.ts index 4a1baa37..2b1f8804 100644 --- a/src/lib/programs/__tests__/program-registry.test.ts +++ b/src/lib/programs/__tests__/program-registry.test.ts @@ -4,6 +4,7 @@ import { getProgramConfig, getSubcommandPrograms, } from '@lib/programs/program-registry'; +import type { ProgramId } from '@lib/programs/program-registry'; import type { WizardSession } from '@lib/wizard-session'; describe('PROGRAM_REGISTRY', () => { @@ -27,6 +28,15 @@ describe('getProgramConfig', () => { 'revenue-analytics', ); }); + + it('throws a named error for an unknown id', () => { + // A string-sourced id (e.g. a mistyped PROGRAM env var) reaches this + // function via a cast. It must name the bad id here, not crash opaquely + // deeper in the pipeline. + expect(() => getProgramConfig('does-not-exist' as ProgramId)).toThrow( + /does-not-exist/, + ); + }); }); describe('getSubcommandPrograms', () => { diff --git a/src/lib/programs/program-registry.ts b/src/lib/programs/program-registry.ts index 2633c295..eb969c76 100644 --- a/src/lib/programs/program-registry.ts +++ b/src/lib/programs/program-registry.ts @@ -118,12 +118,24 @@ export const Program = { export type ProgramId = (typeof PROGRAM_REGISTRY)[number]['id']; /** - * Look up a program config by its id. `ProgramId` is a union of every - * registered id, so the lookup is statically guaranteed to find a match - * — the `!` is a load-bearing assertion of that invariant, not a hope. + * Look up a program config by its id, throwing when the id is unknown. + * + * `ProgramId` is meant to be the union of every registered id, but because + * `ProgramConfig.id` is typed `string` the union collapses to `string` — so + * the parameter type does not statically reject an unknown id. The runtime + * check below is the real guard: it names the bad id when a caller passes an + * untyped string (e.g. a mistyped env var), so a typo fails loudly here + * instead of surfacing as an opaque crash deeper in the pipeline. */ export function getProgramConfig(id: ProgramId): ProgramConfig { - return PROGRAM_REGISTRY.find((c) => c.id === id)!; + const config = PROGRAM_REGISTRY.find((c) => c.id === id); + if (!config) { + const known = PROGRAM_REGISTRY.map((c) => c.id).join(', '); + throw new Error( + `Unknown program id "${id}". Registered program ids: ${known}.`, + ); + } + return config; } /** A program config that is exposed as a CLI subcommand. */ diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index 262c5f56..4a374018 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -101,8 +101,7 @@ export function runWizard( return; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tui = startTUI(WIZARD_VERSION, config.id as any); + tui = startTUI(WIZARD_VERSION, config.id); const activeTui = tui; const session = buildSession({