@outputai/cli 0.8.0 → 0.8.1-dev.945f2f2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,7 +80,7 @@ services:
80
80
  condition: service_healthy
81
81
  worker:
82
82
  condition: service_healthy
83
- image: outputai/api:${OUTPUT_API_VERSION:-0.8.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.8.1-dev.945f2f2.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -79,7 +79,7 @@ export default class DatasetGenerate extends Command {
79
79
  const { workflowId, output } = response.data;
80
80
  const executionTimeMs = await getExecutionTime(workflowId);
81
81
  const dataset = buildDataset(datasetName, resolvedInput, output, executionTimeMs);
82
- const dir = resolveDefaultDatasetsDir(workflowName);
82
+ const dir = await resolveDefaultDatasetsDir(workflowName);
83
83
  const filePath = join(dir, `${datasetName}.yml`);
84
84
  await writeDataset(dataset, filePath);
85
85
  this.log(`Dataset saved: ${filePath}`);
@@ -91,7 +91,7 @@ export default class DatasetGenerate extends Command {
91
91
  const extracted = extractDatasetFromTrace(traceData);
92
92
  const datasetName = nameOverride ?? extractDatasetName(tracePath);
93
93
  const dataset = buildDataset(datasetName, extracted.input, extracted.output, extracted.executionTimeMs);
94
- const dir = resolveDefaultDatasetsDir(workflowName);
94
+ const dir = await resolveDefaultDatasetsDir(workflowName);
95
95
  const filePath = join(dir, `${datasetName}.yml`);
96
96
  await writeDataset(dataset, filePath);
97
97
  this.log(`Dataset saved: ${filePath}`);
@@ -104,7 +104,7 @@ export default class DatasetGenerate extends Command {
104
104
  return;
105
105
  }
106
106
  this.log(`Found ${traces.length} trace(s). Downloading...`);
107
- const dir = resolveDefaultDatasetsDir(workflowName);
107
+ const dir = await resolveDefaultDatasetsDir(workflowName);
108
108
  for (const trace of traces) {
109
109
  const traceData = await downloadRemoteTrace(trace.key);
110
110
  const extracted = extractDatasetFromTrace(traceData);
@@ -13,6 +13,7 @@ export default class WorkflowTest extends Command {
13
13
  format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
14
  };
15
15
  run(): Promise<void>;
16
+ private ensureEvalWorkflowRegistered;
16
17
  private validateDatasets;
17
18
  private runWorkflowForDatasets;
18
19
  private saveEvalResults;
@@ -2,6 +2,8 @@ import { join } from 'node:path';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
3
  import { postWorkflowRun } from '#api/generated/api.js';
4
4
  import { readAllDatasets, writeDataset } from '#services/datasets.js';
5
+ import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
6
+ import { diagnoseMissingEvalWorkflow } from '#utils/eval_diagnostics.js';
5
7
  import { handleApiError } from '#utils/error_handler.js';
6
8
  import { getEvalWorkflowName, renderEvalOutput, computeExitCode, EvalOutputSchema } from '@outputai/evals';
7
9
  export default class WorkflowTest extends Command {
@@ -45,6 +47,8 @@ export default class WorkflowTest extends Command {
45
47
  async run() {
46
48
  const { args, flags } = await this.parse(WorkflowTest);
47
49
  const filterNames = flags.dataset?.split(',').map(s => s.trim());
50
+ const evalName = getEvalWorkflowName(args.workflowName);
51
+ await this.ensureEvalWorkflowRegistered(args.workflowName, evalName);
48
52
  const { datasets, dir } = await readAllDatasets(args.workflowName, filterNames);
49
53
  if (datasets.length === 0) {
50
54
  this.error(`No datasets found for workflow "${args.workflowName}".\n` +
@@ -53,7 +57,6 @@ export default class WorkflowTest extends Command {
53
57
  const preparedDatasets = flags.cached ?
54
58
  this.validateDatasets(datasets) :
55
59
  await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir);
56
- const evalName = getEvalWorkflowName(args.workflowName);
57
60
  this.log(`Running eval workflow "${evalName}"...\n`);
58
61
  const response = await postWorkflowRun({
59
62
  workflowName: evalName,
@@ -80,6 +83,12 @@ export default class WorkflowTest extends Command {
80
83
  this.exit(exitCode);
81
84
  }
82
85
  }
86
+ async ensureEvalWorkflowRegistered(workflowName, evalName) {
87
+ const catalog = await fetchWorkflowCatalog().catch(() => null);
88
+ if (catalog && !catalog.some(w => w.name === evalName)) {
89
+ this.error(await diagnoseMissingEvalWorkflow(workflowName), { exit: 1 });
90
+ }
91
+ }
83
92
  validateDatasets(datasets) {
84
93
  const missing = datasets.filter(d => d.last_output?.output === undefined);
85
94
  if (missing.length > 0) {
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.8.0"
2
+ "framework": "0.8.1-dev.945f2f2.0"
3
3
  }
@@ -6,8 +6,8 @@ export interface DatasetInfo {
6
6
  lastOutputDate?: string;
7
7
  lastEvalDate?: string;
8
8
  }
9
- export declare function resolveDatasetsDir(workflowName: string, basePath?: string): string | null;
10
- export declare function resolveDefaultDatasetsDir(workflowName: string, basePath?: string): string;
9
+ export declare function resolveDatasetsDir(workflowName: string, basePath?: string, workflowPath?: string): Promise<string | null>;
10
+ export declare function resolveDefaultDatasetsDir(workflowName: string, basePath?: string, workflowPath?: string): Promise<string>;
11
11
  export declare function readDatasetFile(filePath: string): Promise<Dataset[]>;
12
12
  export declare function readAllDatasets(workflowName: string, filterNames?: string[], basePath?: string): Promise<{
13
13
  datasets: Dataset[];
@@ -5,24 +5,26 @@ import yaml from 'js-yaml';
5
5
  import { DatasetSchema } from '@outputai/evals';
6
6
  import { getTrace } from '#services/trace_reader.js';
7
7
  import { sanitizeSecrets } from '#utils/secret_sanitizer.js';
8
+ import { resolveWorkflowDir, WORKFLOWS_PATHS } from '#utils/workflow_dir.js';
9
+ import { getWorkflowsBasePath } from '#utils/paths.js';
8
10
  const DATASETS_DIR = 'tests/datasets';
9
- const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
10
- export function resolveDatasetsDir(workflowName, basePath = process.cwd()) {
11
- for (const workflowsDir of WORKFLOWS_PATHS) {
12
- const candidate = resolve(basePath, workflowsDir, workflowName, DATASETS_DIR);
13
- if (existsSync(candidate)) {
14
- return candidate;
15
- }
11
+ export async function resolveDatasetsDir(workflowName, basePath, workflowPath) {
12
+ const workflowDir = await resolveWorkflowDir(workflowName, basePath, workflowPath);
13
+ if (!workflowDir) {
14
+ return null;
16
15
  }
17
- return null;
16
+ const datasetsDir = resolve(workflowDir, DATASETS_DIR);
17
+ return existsSync(datasetsDir) ? datasetsDir : null;
18
18
  }
19
- export function resolveDefaultDatasetsDir(workflowName, basePath = process.cwd()) {
20
- const existing = resolveDatasetsDir(workflowName, basePath);
21
- if (existing) {
22
- return existing;
19
+ export async function resolveDefaultDatasetsDir(workflowName, basePath, workflowPath) {
20
+ // Write into the resolved workflow's tests/datasets — even when it doesn't
21
+ // exist yet (first generation) — so nested workflows get the right location.
22
+ const workflowDir = await resolveWorkflowDir(workflowName, basePath, workflowPath);
23
+ if (workflowDir) {
24
+ return resolve(workflowDir, DATASETS_DIR);
23
25
  }
24
- // Default to first workflows path
25
- return resolve(basePath, WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
26
+ // Workflow not found (unknown name / API unavailable): flat convention.
27
+ return resolve(basePath ?? getWorkflowsBasePath(), WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
26
28
  }
27
29
  export async function readDatasetFile(filePath) {
28
30
  const raw = yaml.load(await readFile(filePath, 'utf-8'));
@@ -39,9 +41,9 @@ export async function readDatasetFile(filePath) {
39
41
  });
40
42
  }
41
43
  export async function readAllDatasets(workflowName, filterNames, basePath) {
42
- const dir = resolveDatasetsDir(workflowName, basePath);
44
+ const dir = await resolveDatasetsDir(workflowName, basePath);
43
45
  if (!dir) {
44
- return { datasets: [], dir: resolveDefaultDatasetsDir(workflowName, basePath) };
46
+ return { datasets: [], dir: await resolveDefaultDatasetsDir(workflowName, basePath) };
45
47
  }
46
48
  const files = await readdir(dir);
47
49
  const ymlFiles = files.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
@@ -76,7 +78,7 @@ export async function writeDataset(dataset, filePath) {
76
78
  await writeFile(filePath, yaml.dump(fileObj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
77
79
  }
78
80
  export async function listDatasets(workflowName, basePath) {
79
- const dir = resolveDatasetsDir(workflowName, basePath);
81
+ const dir = await resolveDatasetsDir(workflowName, basePath);
80
82
  if (!dir) {
81
83
  return [];
82
84
  }
@@ -1,12 +1,20 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import yaml from 'js-yaml';
6
- import { readDatasetFile, readAllDatasets, writeDataset, listDatasets } from './datasets.js';
6
+ import { readDatasetFile, readAllDatasets, writeDataset, listDatasets, resolveDefaultDatasetsDir } from './datasets.js';
7
+ import * as catalog from '#api/workflow_catalog.js';
8
+ vi.mock('#api/workflow_catalog.js', () => ({
9
+ fetchWorkflowCatalog: vi.fn()
10
+ }));
7
11
  const ctx = { tmpDir: '' };
8
12
  beforeEach(async () => {
9
13
  ctx.tmpDir = await mkdtemp(join(tmpdir(), 'output-datasets-test-'));
14
+ // Default: no API. Flat-layout cases resolve offline before this is hit;
15
+ // nested cases override with a resolved catalog.
16
+ vi.mocked(catalog.fetchWorkflowCatalog).mockReset();
17
+ vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
10
18
  });
11
19
  afterEach(async () => {
12
20
  await rm(ctx.tmpDir, { recursive: true, force: true });
@@ -102,6 +110,17 @@ describe('readAllDatasets', () => {
102
110
  expect(datasets).toHaveLength(2);
103
111
  expect(datasets.map(d => d.name).sort()).toEqual(['case_2', 'case_3']);
104
112
  });
113
+ it('resolves a nested workflow folder via the catalog', async () => {
114
+ const datasetsDir = join(ctx.tmpDir, 'src', 'workflows', 'a', 'b', 'c', 'tests', 'datasets');
115
+ await mkdir(datasetsDir, { recursive: true });
116
+ await writeYaml(join(datasetsDir, 'cases.yml'), {
117
+ nested_case: { input: { x: 1 } }
118
+ });
119
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue([{ name: 'a_b_c', path: '/app/dist/workflows/a/b/c/workflow.js' }]);
120
+ const { datasets, dir } = await readAllDatasets('a_b_c', undefined, ctx.tmpDir);
121
+ expect(datasets.map(d => d.name)).toEqual(['nested_case']);
122
+ expect(dir).toBe(datasetsDir);
123
+ });
105
124
  it('returns empty datasets and a default dir when workflow has no datasets dir', async () => {
106
125
  const { datasets, dir } = await readAllDatasets('nonexistent_workflow', undefined, ctx.tmpDir);
107
126
  expect(datasets).toHaveLength(0);
@@ -200,3 +219,19 @@ describe('listDatasets', () => {
200
219
  expect(infos).toHaveLength(0);
201
220
  });
202
221
  });
222
+ // ---------------------------------------------------------------------------
223
+ // resolveDefaultDatasetsDir
224
+ // ---------------------------------------------------------------------------
225
+ describe('resolveDefaultDatasetsDir', () => {
226
+ it('returns the nested workflow tests/datasets for first-time generation via catalog', async () => {
227
+ // Workflow dir exists; its tests/datasets does not yet (first generation).
228
+ await mkdir(join(ctx.tmpDir, 'src', 'workflows', 'a', 'b', 'c'), { recursive: true });
229
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue([{ name: 'a_b_c', path: '/app/dist/workflows/a/b/c/workflow.js' }]);
230
+ const dir = await resolveDefaultDatasetsDir('a_b_c', ctx.tmpDir);
231
+ expect(dir).toBe(join(ctx.tmpDir, 'src', 'workflows', 'a', 'b', 'c', 'tests', 'datasets'));
232
+ });
233
+ it('falls back to the flat convention when the workflow cannot be resolved', async () => {
234
+ const dir = await resolveDefaultDatasetsDir('unknown_flow', ctx.tmpDir);
235
+ expect(dir).toBe(join(ctx.tmpDir, 'src', 'workflows', 'unknown_flow', 'tests', 'datasets'));
236
+ });
237
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Explain why a `<wf>_eval` workflow isn't registered. When the eval source
3
+ * exists on disk but isn't in the catalog, it almost always means tests/evals
4
+ * never compiled to dist (a tsconfig exclude dropped it) — so point at that
5
+ * instead of a bare WorkflowNotFoundError.
6
+ */
7
+ export declare function diagnoseMissingEvalWorkflow(workflowName: string, basePath?: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { getEvalWorkflowName } from '@outputai/evals';
4
+ import { resolveWorkflowDir } from '#utils/workflow_dir.js';
5
+ const EVAL_WORKFLOW_FILES = ['tests/evals/workflow.ts', 'tests/evals/workflow.js'];
6
+ /**
7
+ * Explain why a `<wf>_eval` workflow isn't registered. When the eval source
8
+ * exists on disk but isn't in the catalog, it almost always means tests/evals
9
+ * never compiled to dist (a tsconfig exclude dropped it) — so point at that
10
+ * instead of a bare WorkflowNotFoundError.
11
+ */
12
+ export async function diagnoseMissingEvalWorkflow(workflowName, basePath) {
13
+ const evalName = getEvalWorkflowName(workflowName);
14
+ const workflowDir = await resolveWorkflowDir(workflowName, basePath);
15
+ const evalSource = workflowDir ?
16
+ EVAL_WORKFLOW_FILES.map(file => resolve(workflowDir, file)).find(existsSync) :
17
+ undefined;
18
+ if (evalSource) {
19
+ return [
20
+ `Eval workflow "${evalName}" is not registered, but its source exists at:`,
21
+ ` ${evalSource}`,
22
+ '',
23
+ 'This usually means tests/evals did not compile to dist. Check your tsconfig:',
24
+ ' - Ensure tests/evals is not excluded (avoid excluding "src/**/tests").',
25
+ ' - Prefer excluding "**/*.spec.ts" and "**/*.test.ts" instead.',
26
+ '',
27
+ 'Rebuild the worker so dist/.../tests/evals/workflow.js exists, then retry.'
28
+ ].join('\n');
29
+ }
30
+ return [
31
+ `No eval workflow defined for "${workflowName}".`,
32
+ '',
33
+ `Create an eval workflow at tests/evals/workflow.ts that registers "${evalName}".`
34
+ ].join('\n');
35
+ }
@@ -1,10 +1,9 @@
1
+ export { extractWorkflowRelativePath, findWorkflowDirectoryFromPath } from '#utils/workflow_dir.js';
1
2
  export interface ScenarioResolutionResult {
2
3
  found: boolean;
3
4
  path?: string;
4
5
  searchedPaths: string[];
5
6
  }
6
- export declare function extractWorkflowRelativePath(path: string): string | null;
7
- export declare function findWorkflowDirectoryFromPath(workflowPath: string | undefined, basePath?: string): string | null;
8
7
  export declare function resolveScenarioPath(workflowName: string, scenarioName: string, basePath?: string, workflowPath?: string): Promise<ScenarioResolutionResult>;
9
8
  export declare function listScenariosForWorkflow(workflowName: string, workflowPath?: string, basePath?: string): string[];
10
9
  export declare function getScenarioNotFoundMessage(workflowName: string, scenarioName: string, searchedPaths: string[]): string;
@@ -1,43 +1,13 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
2
+ import { resolve } from 'node:path';
4
3
  import { getWorkflowsBasePath } from '#utils/paths.js';
4
+ import { WORKFLOWS_PATHS, candidateWorkflowDirsFromPath, fetchWorkflowPath } from '#utils/workflow_dir.js';
5
+ export { extractWorkflowRelativePath, findWorkflowDirectoryFromPath } from '#utils/workflow_dir.js';
5
6
  const SCENARIOS_DIR = 'scenarios';
6
- const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
7
- export function extractWorkflowRelativePath(path) {
8
- const match = path.match(/(?:^|\/)workflows\/(.+)\/workflow\.[jt]s$/);
9
- return match ? match[1] : null;
10
- }
11
- function unique(values) {
12
- return [...new Set(values)];
13
- }
14
- function workflowPathSuffixes(workflowPath) {
15
- const parts = dirname(workflowPath).split(/[/\\]+/).filter(Boolean);
16
- return parts.map((_, index) => parts.slice(index));
17
- }
18
- function candidateWorkflowDirsFromPath(workflowPath, basePath) {
19
- return unique(workflowPathSuffixes(workflowPath).flatMap(suffix => WORKFLOWS_PATHS.map(workflowsDir => resolve(basePath, workflowsDir, ...suffix))));
20
- }
21
7
  function candidateScenarioDirsFromPath(workflowPath, basePath) {
22
8
  return candidateWorkflowDirsFromPath(workflowPath, basePath)
23
9
  .map(workflowDir => resolve(workflowDir, SCENARIOS_DIR));
24
10
  }
25
- export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkflowsBasePath()) {
26
- if (!workflowPath) {
27
- return null;
28
- }
29
- return candidateWorkflowDirsFromPath(workflowPath, basePath).find(existsSync) ?? null;
30
- }
31
- async function fetchWorkflowPath(workflowName) {
32
- try {
33
- const workflows = await fetchWorkflowCatalog();
34
- const workflow = workflows.find(w => w.name === workflowName);
35
- return workflow?.path ?? null;
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
11
  function resolveScenarioFromDirectory(relativeDir, scenarioFileName, basePath) {
42
12
  const searchedPaths = [];
43
13
  for (const workflowsDir of WORKFLOWS_PATHS) {
@@ -0,0 +1,13 @@
1
+ export declare const WORKFLOWS_PATHS: string[];
2
+ export declare function extractWorkflowRelativePath(path: string): string | null;
3
+ export declare function candidateWorkflowDirsFromPath(workflowPath: string, basePath: string): string[];
4
+ export declare function findWorkflowDirectoryFromPath(workflowPath: string | undefined, basePath?: string): string | null;
5
+ export declare function fetchWorkflowPath(workflowName: string): Promise<string | null>;
6
+ /**
7
+ * Resolve the on-disk directory of a registered workflow by name.
8
+ *
9
+ * Flat-first so flat-layout projects resolve offline (no catalog round-trip)
10
+ * exactly as before; nested folders fall through to the worker catalog, whose
11
+ * `path` is re-rooted under `src/workflows` / `workflows`.
12
+ */
13
+ export declare function resolveWorkflowDir(workflowName: string, basePath?: string, workflowPath?: string): Promise<string | null>;
@@ -0,0 +1,58 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
4
+ import { getWorkflowsBasePath } from '#utils/paths.js';
5
+ export const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
6
+ export function extractWorkflowRelativePath(path) {
7
+ const match = path.match(/(?:^|\/)workflows\/(.+)\/workflow\.[jt]s$/);
8
+ return match ? match[1] : null;
9
+ }
10
+ function unique(values) {
11
+ return [...new Set(values)];
12
+ }
13
+ function workflowPathSuffixes(workflowPath) {
14
+ const parts = dirname(workflowPath).split(/[/\\]+/).filter(Boolean);
15
+ return parts.map((_, index) => parts.slice(index));
16
+ }
17
+ export function candidateWorkflowDirsFromPath(workflowPath, basePath) {
18
+ return unique(workflowPathSuffixes(workflowPath).flatMap(suffix => WORKFLOWS_PATHS.map(workflowsDir => resolve(basePath, workflowsDir, ...suffix))));
19
+ }
20
+ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkflowsBasePath()) {
21
+ if (!workflowPath) {
22
+ return null;
23
+ }
24
+ return candidateWorkflowDirsFromPath(workflowPath, basePath).find(existsSync) ?? null;
25
+ }
26
+ export async function fetchWorkflowPath(workflowName) {
27
+ try {
28
+ const workflows = await fetchWorkflowCatalog();
29
+ const workflow = workflows.find(w => w.name === workflowName);
30
+ return workflow?.path ?? null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Resolve the on-disk directory of a registered workflow by name.
38
+ *
39
+ * Flat-first so flat-layout projects resolve offline (no catalog round-trip)
40
+ * exactly as before; nested folders fall through to the worker catalog, whose
41
+ * `path` is re-rooted under `src/workflows` / `workflows`.
42
+ */
43
+ export async function resolveWorkflowDir(workflowName, basePath = getWorkflowsBasePath(), workflowPath) {
44
+ for (const workflowsDir of WORKFLOWS_PATHS) {
45
+ const candidate = resolve(basePath, workflowsDir, workflowName);
46
+ if (existsSync(candidate)) {
47
+ return candidate;
48
+ }
49
+ }
50
+ if (workflowPath) {
51
+ const found = findWorkflowDirectoryFromPath(workflowPath, basePath);
52
+ if (found) {
53
+ return found;
54
+ }
55
+ }
56
+ const catalogPath = await fetchWorkflowPath(workflowName);
57
+ return findWorkflowDirectoryFromPath(catalogPath ?? undefined, basePath);
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { resolveWorkflowDir } from './workflow_dir.js';
3
+ import * as fs from 'node:fs';
4
+ import * as catalog from '#api/workflow_catalog.js';
5
+ vi.mock('node:fs', () => ({
6
+ existsSync: vi.fn()
7
+ }));
8
+ vi.mock('#api/workflow_catalog.js', () => ({
9
+ fetchWorkflowCatalog: vi.fn()
10
+ }));
11
+ function mockCatalog(workflows) {
12
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue(workflows);
13
+ }
14
+ function mockCatalogFailure() {
15
+ vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
16
+ }
17
+ describe('resolveWorkflowDir', () => {
18
+ beforeEach(() => {
19
+ vi.resetAllMocks();
20
+ });
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+ it('resolves a flat layout without querying the catalog', async () => {
25
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/simple');
26
+ const result = await resolveWorkflowDir('simple', '/project');
27
+ expect(result).toBe('/project/src/workflows/simple');
28
+ expect(catalog.fetchWorkflowCatalog).not.toHaveBeenCalled();
29
+ });
30
+ it('resolves the workflows/ fallback without querying the catalog', async () => {
31
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/workflows/simple');
32
+ const result = await resolveWorkflowDir('simple', '/project');
33
+ expect(result).toBe('/project/workflows/simple');
34
+ expect(catalog.fetchWorkflowCatalog).not.toHaveBeenCalled();
35
+ });
36
+ it('resolves a nested folder via the catalog path', async () => {
37
+ mockCatalog([{ name: 'a_b_c', path: '/app/dist/workflows/a/b/c/workflow.js' }]);
38
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/a/b/c');
39
+ const result = await resolveWorkflowDir('a_b_c', '/project');
40
+ expect(result).toBe('/project/src/workflows/a/b/c');
41
+ });
42
+ it('returns null when the catalog is unavailable and no flat dir exists', async () => {
43
+ mockCatalogFailure();
44
+ vi.mocked(fs.existsSync).mockReturnValue(false);
45
+ const result = await resolveWorkflowDir('a_b_c', '/project');
46
+ expect(result).toBeNull();
47
+ });
48
+ it('returns null when the workflow is not in the catalog', async () => {
49
+ mockCatalog([{ name: 'other', path: '/app/dist/workflows/other/workflow.js' }]);
50
+ vi.mocked(fs.existsSync).mockReturnValue(false);
51
+ const result = await resolveWorkflowDir('a_b_c', '/project');
52
+ expect(result).toBeNull();
53
+ });
54
+ it('uses a provided workflowPath without querying the catalog', async () => {
55
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/writing/editor');
56
+ const result = await resolveWorkflowDir('writing_editor', '/project', '/app/build-output/writing/editor/workflow.js');
57
+ expect(result).toBe('/project/src/workflows/writing/editor');
58
+ expect(catalog.fetchWorkflowCatalog).not.toHaveBeenCalled();
59
+ });
60
+ });
@@ -1441,5 +1441,5 @@
1441
1441
  ]
1442
1442
  }
1443
1443
  },
1444
- "version": "0.8.0"
1444
+ "version": "0.8.1-dev.945f2f2.0"
1445
1445
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1-dev.945f2f2.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,9 +36,9 @@
36
36
  "semver": "7.7.4",
37
37
  "undici": "8.1.0",
38
38
  "yaml": "^2.8.3",
39
- "@outputai/credentials": "0.8.0",
40
- "@outputai/llm": "0.8.0",
41
- "@outputai/evals": "0.8.0"
39
+ "@outputai/evals": "0.8.1-dev.945f2f2.0",
40
+ "@outputai/llm": "0.8.1-dev.945f2f2.0",
41
+ "@outputai/credentials": "0.8.1-dev.945f2f2.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/cli-progress": "3.11.6",