From 4e84ad91ed0eff97caa15bc6e45a779cf90692a9 Mon Sep 17 00:00:00 2001 From: Ben Church Date: Tue, 23 Jun 2026 13:15:20 -0700 Subject: [PATCH] feat(cli): honor --catalog/OUTPUT_CATALOG_ID across workflow commands Thread the resolved catalog through scenario resolution and add --catalog to `test` and `dataset generate`, so start/run/test/dataset generate use the same catalog rule as `list`. Removes the ~30s default-catalog preflight stall in worktrees where the default catalog has no worker. --- ...out-491-cli-catalog-scenario-resolution.md | 5 + .../workflow/dataset/generate.spec.ts | 84 ++++++++++++++++ .../src/commands/workflow/dataset/generate.ts | 25 +++-- sdk/cli/src/commands/workflow/run.spec.ts | 23 +++++ sdk/cli/src/commands/workflow/run.ts | 2 +- sdk/cli/src/commands/workflow/start.spec.ts | 68 ++++++++++++- sdk/cli/src/commands/workflow/start.ts | 2 +- .../src/commands/workflow/test_eval.spec.ts | 99 +++++++++++++++++++ sdk/cli/src/commands/workflow/test_eval.ts | 28 ++++-- sdk/cli/src/utils/resolve_input.ts | 5 +- sdk/cli/src/utils/scenario_resolver.spec.ts | 20 ++++ sdk/cli/src/utils/scenario_resolver.ts | 5 +- sdk/cli/src/utils/workflow_dir.ts | 4 +- 13 files changed, 346 insertions(+), 24 deletions(-) create mode 100644 .changeset/out-491-cli-catalog-scenario-resolution.md create mode 100644 sdk/cli/src/commands/workflow/dataset/generate.spec.ts create mode 100644 sdk/cli/src/commands/workflow/test_eval.spec.ts diff --git a/.changeset/out-491-cli-catalog-scenario-resolution.md b/.changeset/out-491-cli-catalog-scenario-resolution.md new file mode 100644 index 00000000..4aca5bd1 --- /dev/null +++ b/.changeset/out-491-cli-catalog-scenario-resolution.md @@ -0,0 +1,5 @@ +--- +"@outputai/cli": minor +--- + +CLI `start`/`run`/`test`/`dataset generate` now resolve scenarios and route execution against `--catalog`/`OUTPUT_CATALOG_ID` instead of the API server's default catalog. This removes the ~30s scenario-resolution stall in worktrees where the default catalog has no worker polling it. `workflow test` and `workflow dataset generate` also gain a `--catalog` flag (env: `OUTPUT_CATALOG_ID`), matching `list`/`start`/`run`. diff --git a/sdk/cli/src/commands/workflow/dataset/generate.spec.ts b/sdk/cli/src/commands/workflow/dataset/generate.spec.ts new file mode 100644 index 00000000..1c7f399b --- /dev/null +++ b/sdk/cli/src/commands/workflow/dataset/generate.spec.ts @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock( '#api/generated/api.js', () => ( { + postWorkflowRun: vi.fn() +} ) ); + +vi.mock( '#utils/scenario_resolver.js', () => ( { + resolveScenarioPath: vi.fn(), + getScenarioNotFoundMessage: vi.fn().mockReturnValue( 'not found' ) +} ) ); + +vi.mock( '#utils/input_parser.js', () => ( { + parseInputFlag: vi.fn() +} ) ); + +vi.mock( '#services/datasets.js', () => ( { + writeDataset: vi.fn(), + resolveDefaultDatasetsDir: vi.fn().mockResolvedValue( '/datasets' ), + buildDataset: vi.fn().mockReturnValue( { name: 'basic' } ), + getExecutionTime: vi.fn().mockResolvedValue( 100 ), + extractDatasetName: vi.fn() +} ) ); + +describe( 'workflow dataset generate command', () => { + beforeEach( () => { + vi.clearAllMocks(); + delete process.env.OUTPUT_CATALOG_ID; + } ); + + describe( 'command definition', () => { + it( 'binds the catalog flag to OUTPUT_CATALOG_ID', async () => { + const DatasetGenerate = ( await import( './generate.js' ) ).default; + expect( DatasetGenerate.flags ).toHaveProperty( 'catalog' ); + expect( DatasetGenerate.flags.catalog.env ).toBe( 'OUTPUT_CATALOG_ID' ); + expect( DatasetGenerate.flags.catalog.char ).toBe( 'c' ); + } ); + } ); + + describe( 'run()', () => { + const createCommand = async ( flagOverrides: Record = {} ) => { + const DatasetGenerate = ( await import( './generate.js' ) ).default; + const { postWorkflowRun } = await import( '#api/generated/api.js' ); + const { resolveScenarioPath } = await import( '#utils/scenario_resolver.js' ); + const { parseInputFlag } = await import( '#utils/input_parser.js' ); + + const cmd = new DatasetGenerate( [ 'my_workflow' ], {} as any ); + cmd.log = vi.fn(); + cmd.error = vi.fn( () => { + throw new Error( 'error called' ); + } ) as any; + ( cmd as any ).parse = vi.fn().mockResolvedValue( { + args: { workflowName: 'my_workflow', scenario: 'basic' }, + flags: { catalog: undefined, trace: undefined, name: undefined, download: false, limit: 5, input: undefined, ...flagOverrides } + } ); + + return { + cmd, + postWorkflowRun: vi.mocked( postWorkflowRun ), + resolveScenarioPath: vi.mocked( resolveScenarioPath ), + parseInputFlag: vi.mocked( parseInputFlag ) + }; + }; + + it( 'resolves the scenario and runs the workflow against the resolved catalog', async () => { + const { cmd, postWorkflowRun, resolveScenarioPath, parseInputFlag } = await createCommand( { catalog: 'my-catalog' } ); + resolveScenarioPath.mockResolvedValue( { found: true, path: '/scenarios/basic.json', searchedPaths: [] } ); + parseInputFlag.mockResolvedValue( { foo: 'bar' } as any ); + postWorkflowRun.mockResolvedValue( { + data: { workflowId: 'wf-1', output: { ok: true } }, + status: 200, + headers: new Headers() + } as any ); + + await cmd.run(); + + expect( resolveScenarioPath ).toHaveBeenCalledWith( 'my_workflow', 'basic', undefined, undefined, 'my-catalog' ); + expect( postWorkflowRun ).toHaveBeenCalledWith( + expect.objectContaining( { workflowName: 'my_workflow', catalog: 'my-catalog' } ), + expect.anything() + ); + } ); + } ); +} ); diff --git a/sdk/cli/src/commands/workflow/dataset/generate.ts b/sdk/cli/src/commands/workflow/dataset/generate.ts index e636fe9c..135a1422 100644 --- a/sdk/cli/src/commands/workflow/dataset/generate.ts +++ b/sdk/cli/src/commands/workflow/dataset/generate.ts @@ -37,6 +37,14 @@ export default class DatasetGenerate extends Command { }; static override flags = { + catalog: Flags.string( { + char: 'c', + aliases: [ 'task-queue' ], + charAliases: [ 'q' ], + deprecateAliases: true, + description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)', + env: 'OUTPUT_CATALOG_ID' + } ), trace: Flags.string( { char: 't', description: 'Path to a local trace file to extract dataset from', @@ -84,7 +92,8 @@ export default class DatasetGenerate extends Command { args.workflowName, args.scenario, flags.input, - flags.name + flags.name, + flags.catalog ); } @@ -92,12 +101,14 @@ export default class DatasetGenerate extends Command { workflowName: string, scenario: string | undefined, inputFlag: string | undefined, - nameOverride: string | undefined + nameOverride: string | undefined, + catalog: string | undefined ): Promise { const resolvedInput = await this.resolveScenarioInput( workflowName, scenario, - inputFlag + inputFlag, + catalog ); const datasetName = nameOverride ?? scenario ?? 'dataset'; @@ -106,7 +117,8 @@ export default class DatasetGenerate extends Command { const response = await postWorkflowRun( { workflowName, - input: resolvedInput + input: resolvedInput, + catalog }, { config: { timeout: 600000 } } ); @@ -199,7 +211,8 @@ export default class DatasetGenerate extends Command { private async resolveScenarioInput( workflowName: string, scenario: string | undefined, - inputFlag: string | undefined + inputFlag: string | undefined, + catalog: string | undefined ): Promise { if ( inputFlag && scenario ) { return ux.error( @@ -213,7 +226,7 @@ export default class DatasetGenerate extends Command { } if ( scenario ) { - const resolution = await resolveScenarioPath( workflowName, scenario ); + const resolution = await resolveScenarioPath( workflowName, scenario, undefined, undefined, catalog ); if ( !resolution.found ) { return ux.error( getScenarioNotFoundMessage( workflowName, scenario, resolution.searchedPaths ), diff --git a/sdk/cli/src/commands/workflow/run.spec.ts b/sdk/cli/src/commands/workflow/run.spec.ts index 6f73d56b..0a55cf0f 100644 --- a/sdk/cli/src/commands/workflow/run.spec.ts +++ b/sdk/cli/src/commands/workflow/run.spec.ts @@ -79,6 +79,7 @@ describe( 'workflow run command', () => { await cmd.run(); + expect( resolveInput ).toHaveBeenCalledWith( 'my_workflow', undefined, undefined, 'run', undefined ); expect( postWorkflowRun ).toHaveBeenCalledTimes( 1 ); expect( postWorkflowRun ).toHaveBeenCalledWith( { workflowName: 'my_workflow', input: { key: 'value' }, catalog: undefined }, @@ -88,6 +89,28 @@ describe( 'workflow run command', () => { expect( cmd.log ).toHaveBeenCalledWith( expect.stringMatching( /\n/ ) ); } ); + it( 'threads the resolved catalog to resolveInput and postWorkflowRun', async () => { + const { cmd, postWorkflowRun, resolveInput } = await createCommand(); + ( cmd as any ).parse = vi.fn().mockResolvedValue( { + args: { workflowName: 'my_workflow', scenario: 'basic' }, + flags: { input: undefined, catalog: 'my-catalog', format: 'text' } + } ); + resolveInput.mockResolvedValue( { key: 'value' } ); + postWorkflowRun.mockResolvedValue( { + data: { status: 'completed', result: {} }, + status: 200, + headers: new Headers() + } as any ); + + await cmd.run(); + + expect( resolveInput ).toHaveBeenCalledWith( 'my_workflow', 'basic', undefined, 'run', 'my-catalog' ); + expect( postWorkflowRun ).toHaveBeenCalledWith( + expect.objectContaining( { catalog: 'my-catalog' } ), + expect.anything() + ); + } ); + it( 'retries when response has Retry-After and succeeds on second attempt', async () => { const { cmd, postWorkflowRun, resolveInput } = await createCommand(); resolveInput.mockResolvedValue( {} ); diff --git a/sdk/cli/src/commands/workflow/run.ts b/sdk/cli/src/commands/workflow/run.ts index 4c073411..ad48deec 100644 --- a/sdk/cli/src/commands/workflow/run.ts +++ b/sdk/cli/src/commands/workflow/run.ts @@ -87,7 +87,7 @@ export default class WorkflowRun extends Command { async run(): Promise { const { args, flags } = await this.parse( WorkflowRun ); - const input = await resolveInput( args.workflowName, args.scenario, flags.input, 'run' ); + const input = await resolveInput( args.workflowName, args.scenario, flags.input, 'run', flags.catalog ); this.log( `Executing workflow: ${args.workflowName}...` ); diff --git a/sdk/cli/src/commands/workflow/start.spec.ts b/sdk/cli/src/commands/workflow/start.spec.ts index 8204e11d..c2f4a32c 100644 --- a/sdk/cli/src/commands/workflow/start.spec.ts +++ b/sdk/cli/src/commands/workflow/start.spec.ts @@ -1,13 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -vi.mock( '../../api/generated/api.js', () => ( { +vi.mock( '#api/generated/api.js', () => ( { postWorkflowStart: vi.fn() } ) ); +vi.mock( '#utils/resolve_input.js', () => ( { + resolveInput: vi.fn() +} ) ); + describe( 'workflow start command', () => { - beforeEach( () => { + beforeEach( async () => { vi.clearAllMocks(); delete process.env.OUTPUT_CATALOG_ID; + const { resolveInput } = await import( '#utils/resolve_input.js' ); + vi.mocked( resolveInput ).mockResolvedValue( {} ); } ); describe( 'command definition', () => { @@ -30,5 +37,62 @@ describe( 'workflow start command', () => { expect( WorkflowStart.args ).toHaveProperty( 'scenario' ); expect( WorkflowStart.args.scenario.required ).toBe( false ); } ); + + it( 'binds the catalog flag to OUTPUT_CATALOG_ID', async () => { + const WorkflowStart = ( await import( './start.js' ) ).default; + expect( WorkflowStart.flags.catalog.env ).toBe( 'OUTPUT_CATALOG_ID' ); + expect( WorkflowStart.flags.catalog.char ).toBe( 'c' ); + } ); + } ); + + describe( 'run()', () => { + const createCommand = async ( flagOverrides: Record = {} ) => { + const WorkflowStart = ( await import( './start.js' ) ).default; + const { postWorkflowStart } = await import( '#api/generated/api.js' ); + const { resolveInput } = await import( '#utils/resolve_input.js' ); + + const cmd = new WorkflowStart( [ 'my_workflow' ], {} as any ); + cmd.log = vi.fn(); + cmd.error = vi.fn( () => { + throw new Error( 'error called' ); + } ) as any; + ( cmd as any ).parse = vi.fn().mockResolvedValue( { + args: { workflowName: 'my_workflow', scenario: undefined }, + flags: { input: undefined, catalog: undefined, ...flagOverrides } + } ); + + return { cmd, postWorkflowStart: vi.mocked( postWorkflowStart ), resolveInput: vi.mocked( resolveInput ) }; + }; + + it( 'threads the resolved catalog to resolveInput and postWorkflowStart', async () => { + const { cmd, postWorkflowStart, resolveInput } = await createCommand( { catalog: 'my-catalog' } ); + resolveInput.mockResolvedValue( { key: 'value' } ); + postWorkflowStart.mockResolvedValue( { + data: { workflowId: 'wf-123' }, + status: 200, + headers: new Headers() + } as any ); + + await cmd.run(); + + expect( resolveInput ).toHaveBeenCalledWith( 'my_workflow', undefined, undefined, 'start', 'my-catalog' ); + expect( postWorkflowStart ).toHaveBeenCalledWith( + expect.objectContaining( { workflowName: 'my_workflow', catalog: 'my-catalog' } ) + ); + } ); + + it( 'passes undefined catalog through when none is set', async () => { + const { cmd, postWorkflowStart, resolveInput } = await createCommand(); + resolveInput.mockResolvedValue( {} ); + postWorkflowStart.mockResolvedValue( { + data: { workflowId: 'wf-123' }, + status: 200, + headers: new Headers() + } as any ); + + await cmd.run(); + + expect( resolveInput ).toHaveBeenCalledWith( 'my_workflow', undefined, undefined, 'start', undefined ); + } ); } ); } ); diff --git a/sdk/cli/src/commands/workflow/start.ts b/sdk/cli/src/commands/workflow/start.ts index 6efb48f4..2dd2b681 100644 --- a/sdk/cli/src/commands/workflow/start.ts +++ b/sdk/cli/src/commands/workflow/start.ts @@ -43,7 +43,7 @@ export default class WorkflowStart extends Command { async run(): Promise { const { args, flags } = await this.parse( WorkflowStart ); - const input = await resolveInput( args.workflowName, args.scenario, flags.input, 'start' ); + const input = await resolveInput( args.workflowName, args.scenario, flags.input, 'start', flags.catalog ); this.log( `Starting workflow: ${args.workflowName}...` ); diff --git a/sdk/cli/src/commands/workflow/test_eval.spec.ts b/sdk/cli/src/commands/workflow/test_eval.spec.ts new file mode 100644 index 00000000..9809f7ac --- /dev/null +++ b/sdk/cli/src/commands/workflow/test_eval.spec.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock( '#api/generated/api.js', () => ( { + postWorkflowRun: vi.fn() +} ) ); + +vi.mock( '#api/workflow_catalog.js', () => ( { + fetchWorkflowCatalog: vi.fn() +} ) ); + +vi.mock( '#services/datasets.js', () => ( { + readAllDatasets: vi.fn(), + writeDataset: vi.fn() +} ) ); + +vi.mock( '#utils/eval_diagnostics.js', () => ( { + diagnoseMissingEvalWorkflow: vi.fn().mockResolvedValue( 'missing eval workflow' ) +} ) ); + +vi.mock( '@outputai/evals', () => ( { + getEvalWorkflowName: ( name: string ) => `${name}_eval`, + renderEvalOutput: vi.fn().mockReturnValue( 'rendered' ), + computeExitCode: vi.fn().mockReturnValue( 0 ), + EvalOutputSchema: { parse: ( value: unknown ) => value } +} ) ); + +const RUN_RESULT = { data: { output: { ok: true } }, status: 200, headers: new Headers() }; +const EVAL_RESULT = { data: { output: { cases: [] } }, status: 200, headers: new Headers() }; + +describe( 'workflow test command', () => { + beforeEach( async () => { + vi.clearAllMocks(); + delete process.env.OUTPUT_CATALOG_ID; + + const { fetchWorkflowCatalog } = await import( '#api/workflow_catalog.js' ); + const { readAllDatasets } = await import( '#services/datasets.js' ); + vi.mocked( fetchWorkflowCatalog ).mockResolvedValue( [ { name: 'my_workflow_eval' } ] as any ); + vi.mocked( readAllDatasets ).mockResolvedValue( { + datasets: [ { name: 'case1', input: { foo: 'bar' } } ], + dir: '/datasets' + } as any ); + } ); + + describe( 'command definition', () => { + it( 'binds the catalog flag to OUTPUT_CATALOG_ID', async () => { + const WorkflowTest = ( await import( './test_eval.js' ) ).default; + expect( WorkflowTest.flags ).toHaveProperty( 'catalog' ); + expect( WorkflowTest.flags.catalog.env ).toBe( 'OUTPUT_CATALOG_ID' ); + expect( WorkflowTest.flags.catalog.char ).toBe( 'c' ); + } ); + } ); + + describe( 'run()', () => { + const createCommand = async ( flagOverrides: Record = {} ) => { + const WorkflowTest = ( await import( './test_eval.js' ) ).default; + const { postWorkflowRun } = await import( '#api/generated/api.js' ); + const { fetchWorkflowCatalog } = await import( '#api/workflow_catalog.js' ); + + const cmd = new WorkflowTest( [ 'my_workflow' ], {} as any ); + cmd.log = vi.fn(); + cmd.error = vi.fn( () => { + throw new Error( 'error called' ); + } ) as any; + ( cmd as any ).exit = vi.fn(); + ( cmd as any ).parse = vi.fn().mockResolvedValue( { + args: { workflowName: 'my_workflow' }, + flags: { catalog: undefined, cached: false, save: false, dataset: undefined, format: 'text', ...flagOverrides } + } ); + + return { + cmd, + postWorkflowRun: vi.mocked( postWorkflowRun ), + fetchWorkflowCatalog: vi.mocked( fetchWorkflowCatalog ) + }; + }; + + it( 'routes registration, dataset runs, and the eval run to the resolved catalog', async () => { + const { cmd, postWorkflowRun, fetchWorkflowCatalog } = await createCommand( { catalog: 'my-catalog' } ); + postWorkflowRun + .mockResolvedValueOnce( RUN_RESULT as any ) + .mockResolvedValueOnce( EVAL_RESULT as any ); + + await cmd.run(); + + expect( fetchWorkflowCatalog ).toHaveBeenCalledWith( 'my-catalog' ); + expect( postWorkflowRun ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { workflowName: 'my_workflow', catalog: 'my-catalog' } ), + expect.anything() + ); + expect( postWorkflowRun ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { workflowName: 'my_workflow_eval', catalog: 'my-catalog' } ), + expect.anything() + ); + } ); + } ); +} ); diff --git a/sdk/cli/src/commands/workflow/test_eval.ts b/sdk/cli/src/commands/workflow/test_eval.ts index 7432ff90..58244305 100644 --- a/sdk/cli/src/commands/workflow/test_eval.ts +++ b/sdk/cli/src/commands/workflow/test_eval.ts @@ -35,6 +35,14 @@ export default class WorkflowTest extends Command { }; static override flags = { + catalog: Flags.string( { + char: 'c', + aliases: [ 'task-queue' ], + charAliases: [ 'q' ], + deprecateAliases: true, + description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)', + env: 'OUTPUT_CATALOG_ID' + } ), cached: Flags.boolean( { description: 'Use cached output from dataset files (skip workflow execution)', default: false, @@ -62,7 +70,7 @@ export default class WorkflowTest extends Command { const filterNames = flags.dataset?.split( ',' ).map( s => s.trim() ); const evalName = getEvalWorkflowName( args.workflowName ); - await this.ensureEvalWorkflowRegistered( args.workflowName, evalName ); + await this.ensureEvalWorkflowRegistered( args.workflowName, evalName, flags.catalog ); const { datasets, dir } = await readAllDatasets( args.workflowName, filterNames ); @@ -76,13 +84,14 @@ export default class WorkflowTest extends Command { const preparedDatasets = flags.cached ? this.validateDatasets( datasets ) : - await this.runWorkflowForDatasets( args.workflowName, datasets, flags.save, dir ); + await this.runWorkflowForDatasets( args.workflowName, datasets, flags.save, dir, flags.catalog ); this.log( `Running eval workflow "${evalName}"...\n` ); const response = await postWorkflowRun( { workflowName: evalName, - input: { datasets: preparedDatasets } + input: { datasets: preparedDatasets }, + catalog: flags.catalog }, { config: { timeout: 600000 } } ); @@ -112,10 +121,11 @@ export default class WorkflowTest extends Command { private async ensureEvalWorkflowRegistered( workflowName: string, - evalName: string + evalName: string, + catalog?: string ): Promise { - const catalog = await fetchWorkflowCatalog().catch( () => null ); - if ( catalog && !catalog.some( w => w.name === evalName ) ) { + const workflows = await fetchWorkflowCatalog( catalog ).catch( () => null ); + if ( workflows && !workflows.some( w => w.name === evalName ) ) { this.error( await diagnoseMissingEvalWorkflow( workflowName ), { exit: 1 } ); } } @@ -137,7 +147,8 @@ export default class WorkflowTest extends Command { workflowName: string, datasets: Dataset[], save: boolean, - dir: string + dir: string, + catalog?: string ): Promise { this.log( `Running workflow "${workflowName}" for ${datasets.length} dataset(s)...\n` ); @@ -149,7 +160,8 @@ export default class WorkflowTest extends Command { const startMs = Date.now(); const response = await postWorkflowRun( { workflowName, - input: dataset.input + input: dataset.input, + catalog }, { config: { timeout: 600000 } } ); diff --git a/sdk/cli/src/utils/resolve_input.ts b/sdk/cli/src/utils/resolve_input.ts index e1ee532f..55809c7a 100644 --- a/sdk/cli/src/utils/resolve_input.ts +++ b/sdk/cli/src/utils/resolve_input.ts @@ -6,7 +6,8 @@ export async function resolveInput( workflowName: string, scenario: string | undefined, inputFlag: string | undefined, - commandName: string + commandName: string, + catalog?: string ): Promise { if ( inputFlag && scenario ) { return ux.error( @@ -20,7 +21,7 @@ export async function resolveInput( } if ( scenario ) { - const resolution = await resolveScenarioPath( workflowName, scenario ); + const resolution = await resolveScenarioPath( workflowName, scenario, undefined, undefined, catalog ); if ( !resolution.found ) { return ux.error( getScenarioNotFoundMessage( workflowName, scenario, resolution.searchedPaths ), diff --git a/sdk/cli/src/utils/scenario_resolver.spec.ts b/sdk/cli/src/utils/scenario_resolver.spec.ts index 0eb496a0..97a03f40 100644 --- a/sdk/cli/src/utils/scenario_resolver.spec.ts +++ b/sdk/cli/src/utils/scenario_resolver.spec.ts @@ -211,6 +211,26 @@ describe( 'resolveScenarioPath', () => { expect( result.path ).toContain( 'complex/deep_test.json' ); } ); } ); + + describe( 'catalog routing', () => { + it( 'forwards the provided catalog to the catalog lookup', async () => { + mockCatalog( [ { name: 'my_workflow', path: '/app/dist/workflows/my_workflow/workflow.js' } ] ); + vi.mocked( fs.existsSync ).mockReturnValue( false ); + + await resolveScenarioPath( 'my_workflow', 'test', '/project', undefined, 'os-workflows' ); + + expect( catalog.fetchWorkflowCatalog ).toHaveBeenCalledWith( 'os-workflows' ); + } ); + + it( 'looks up the default catalog when no catalog is provided', async () => { + mockCatalog( [ { name: 'my_workflow', path: '/app/dist/workflows/my_workflow/workflow.js' } ] ); + vi.mocked( fs.existsSync ).mockReturnValue( false ); + + await resolveScenarioPath( 'my_workflow', 'test', '/project' ); + + expect( catalog.fetchWorkflowCatalog ).toHaveBeenCalledWith( undefined ); + } ); + } ); } ); describe( 'listScenariosForWorkflow', () => { diff --git a/sdk/cli/src/utils/scenario_resolver.ts b/sdk/cli/src/utils/scenario_resolver.ts index aa57fc9b..2bc69870 100644 --- a/sdk/cli/src/utils/scenario_resolver.ts +++ b/sdk/cli/src/utils/scenario_resolver.ts @@ -62,7 +62,8 @@ export async function resolveScenarioPath( workflowName: string, scenarioName: string, basePath: string = getWorkflowsBasePath(), - workflowPath?: string + workflowPath?: string, + catalog?: string ): Promise { const scenarioFileName = scenarioName.endsWith( '.json' ) ? scenarioName : @@ -78,7 +79,7 @@ export async function resolveScenarioPath( } } - const catalogPath = workflowPath ? null : await fetchWorkflowPath( workflowName ); + const catalogPath = workflowPath ? null : await fetchWorkflowPath( workflowName, catalog ); if ( catalogPath ) { const result = resolveScenarioFromScenarioDirs( diff --git a/sdk/cli/src/utils/workflow_dir.ts b/sdk/cli/src/utils/workflow_dir.ts index 2d0d09c6..4c57f707 100644 --- a/sdk/cli/src/utils/workflow_dir.ts +++ b/sdk/cli/src/utils/workflow_dir.ts @@ -38,9 +38,9 @@ export function findWorkflowDirectoryFromPath( return candidateWorkflowDirsFromPath( workflowPath, basePath ).find( existsSync ) ?? null; } -export async function fetchWorkflowPath( workflowName: string ): Promise { +export async function fetchWorkflowPath( workflowName: string, catalog?: string ): Promise { try { - const workflows = await fetchWorkflowCatalog(); + const workflows = await fetchWorkflowCatalog( catalog ); const workflow = workflows.find( w => w.name === workflowName ); return workflow?.path ?? null; } catch {