@outputai/cli 0.8.1-next.e92f632.0 → 0.8.2-next.2da7213.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.
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/workflow/cost.d.ts +3 -2
- package/dist/commands/workflow/cost.js +4 -11
- package/dist/commands/workflow/cost.spec.js +1 -3
- package/dist/commands/workflow/dataset/generate.js +3 -3
- package/dist/commands/workflow/dataset/list.d.ts +3 -1
- package/dist/commands/workflow/dataset/list.js +10 -14
- package/dist/commands/workflow/debug.d.ts +2 -6
- package/dist/commands/workflow/debug.js +10 -29
- package/dist/commands/workflow/debug.spec.js +2 -5
- package/dist/commands/workflow/list.d.ts +4 -1
- package/dist/commands/workflow/list.js +15 -19
- package/dist/commands/workflow/list.spec.js +2 -1
- package/dist/commands/workflow/result.d.ts +3 -4
- package/dist/commands/workflow/result.js +6 -15
- package/dist/commands/workflow/result.spec.js +2 -4
- package/dist/commands/workflow/run.d.ts +3 -2
- package/dist/commands/workflow/run.js +4 -11
- package/dist/commands/workflow/run.spec.js +1 -3
- package/dist/commands/workflow/runs/list.d.ts +3 -1
- package/dist/commands/workflow/runs/list.js +11 -15
- package/dist/commands/workflow/status.d.ts +3 -4
- package/dist/commands/workflow/status.js +20 -30
- package/dist/commands/workflow/status.spec.js +2 -4
- package/dist/commands/workflow/test_eval.d.ts +4 -2
- package/dist/commands/workflow/test_eval.js +14 -15
- package/dist/commands/workflow/test_eval.spec.d.ts +1 -0
- package/dist/commands/workflow/test_eval.spec.js +84 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.d.ts +1 -0
- package/dist/hooks/init.js +40 -30
- package/dist/hooks/init.spec.js +28 -4
- package/dist/services/datasets.d.ts +2 -2
- package/dist/services/datasets.js +19 -17
- package/dist/services/datasets.test.js +37 -2
- package/dist/utils/eval_diagnostics.d.ts +7 -0
- package/dist/utils/eval_diagnostics.js +35 -0
- package/dist/utils/format_workflow_result.spec.js +0 -13
- package/dist/utils/scenario_resolver.d.ts +1 -2
- package/dist/utils/scenario_resolver.js +3 -33
- package/dist/utils/trace_formatter.js +1 -2
- package/dist/utils/workflow_dir.d.ts +13 -0
- package/dist/utils/workflow_dir.js +58 -0
- package/dist/utils/workflow_dir.spec.d.ts +1 -0
- package/dist/utils/workflow_dir.spec.js +60 -0
- package/oclif.manifest.json +182 -201
- package/package.json +4 -4
- package/dist/utils/constants.d.ts +0 -5
- package/dist/utils/constants.js +0 -4
- package/dist/utils/output_formatter.d.ts +0 -2
- package/dist/utils/output_formatter.js +0 -11
|
@@ -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,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { formatWorkflowResult } from './format_workflow_result.js';
|
|
3
|
-
import { formatOutput } from './output_formatter.js';
|
|
4
3
|
describe('formatWorkflowResult', () => {
|
|
5
4
|
it('should display output for completed workflows', () => {
|
|
6
5
|
const result = formatWorkflowResult({
|
|
@@ -76,16 +75,4 @@ describe('formatWorkflowResult', () => {
|
|
|
76
75
|
expect(result).toContain('Status: failed');
|
|
77
76
|
expect(result).not.toContain('Error:');
|
|
78
77
|
});
|
|
79
|
-
it('should work with formatOutput for json format', () => {
|
|
80
|
-
const data = {
|
|
81
|
-
workflowId: 'wf-456',
|
|
82
|
-
status: 'failed',
|
|
83
|
-
output: null,
|
|
84
|
-
error: 'Activity task failed'
|
|
85
|
-
};
|
|
86
|
-
const output = formatOutput(data, 'json', formatWorkflowResult);
|
|
87
|
-
const parsed = JSON.parse(output);
|
|
88
|
-
expect(parsed.status).toBe('failed');
|
|
89
|
-
expect(parsed.error).toBe('Activity task failed');
|
|
90
|
-
});
|
|
91
78
|
});
|
|
@@ -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 {
|
|
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) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Table from 'cli-table3';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
|
-
import { formatOutput } from '#utils/output_formatter.js';
|
|
4
3
|
import { formatDuration } from '#utils/date_formatter.js';
|
|
5
4
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
6
5
|
import { isTraceEvent, isValidTimestamp } from '#types/trace.js';
|
|
@@ -369,7 +368,7 @@ const formatAsText = (trace) => {
|
|
|
369
368
|
export function format(traceData, outputFormat = 'text') {
|
|
370
369
|
const trace = typeof traceData === 'string' ? JSON.parse(traceData) : traceData;
|
|
371
370
|
if (outputFormat === 'json') {
|
|
372
|
-
return
|
|
371
|
+
return JSON.stringify(trace, null, 2);
|
|
373
372
|
}
|
|
374
373
|
return formatAsText(trace);
|
|
375
374
|
}
|
|
@@ -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
|
+
});
|