@outputai/cli 0.8.2-next.e1cd79b.0 → 0.8.2-next.ec4c07d.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.d.ts +1 -0
- package/dist/commands/workflow/dataset/generate.js +15 -6
- package/dist/commands/workflow/dataset/generate.spec.d.ts +1 -0
- package/dist/commands/workflow/dataset/generate.spec.js +69 -0
- 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/generate.spec.js +2 -2
- 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 +5 -12
- package/dist/commands/workflow/run.spec.js +18 -3
- package/dist/commands/workflow/runs/list.d.ts +3 -1
- package/dist/commands/workflow/runs/list.js +11 -15
- package/dist/commands/workflow/start.js +1 -1
- package/dist/commands/workflow/start.spec.js +53 -2
- 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 +23 -23
- package/dist/commands/workflow/test_eval.spec.d.ts +1 -0
- package/dist/commands/workflow/test_eval.spec.js +121 -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/coding_agents.spec.js +10 -10
- package/dist/utils/format_workflow_result.spec.js +0 -13
- package/dist/utils/resolve_input.d.ts +1 -1
- package/dist/utils/resolve_input.js +2 -2
- package/dist/utils/scenario_resolver.d.ts +1 -1
- package/dist/utils/scenario_resolver.js +2 -2
- package/dist/utils/scenario_resolver.spec.js +14 -0
- package/dist/utils/trace_formatter.js +1 -2
- package/dist/utils/workflow_dir.d.ts +1 -1
- package/dist/utils/workflow_dir.js +2 -2
- package/oclif.manifest.json +114 -101
- package/package.json +5 -5
- 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,7 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { postWorkflowRun } from '#api/generated/api.js';
|
|
3
|
-
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
4
|
-
import { formatOutput } from '#utils/output_formatter.js';
|
|
5
3
|
import { formatWorkflowResult, ERROR_STATUSES } from '#utils/format_workflow_result.js';
|
|
6
4
|
import { handleApiError } from '#utils/error_handler.js';
|
|
7
5
|
import { resolveInput } from '#utils/resolve_input.js';
|
|
@@ -30,9 +28,10 @@ async function executeWorkflow(args) {
|
|
|
30
28
|
}
|
|
31
29
|
export default class WorkflowRun extends Command {
|
|
32
30
|
static description = 'Execute a workflow synchronously and wait for completion';
|
|
31
|
+
static enableJsonFlag = true;
|
|
33
32
|
static examples = [
|
|
34
33
|
'<%= config.bin %> <%= command.id %> simple basic_input',
|
|
35
|
-
'<%= config.bin %> <%= command.id %> simple my_scenario --
|
|
34
|
+
'<%= config.bin %> <%= command.id %> simple my_scenario --json',
|
|
36
35
|
'<%= config.bin %> <%= command.id %> simple --input \'{"values":[1,2,3]}\'',
|
|
37
36
|
'<%= config.bin %> <%= command.id %> simple --input input.json',
|
|
38
37
|
'<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog'
|
|
@@ -60,17 +59,11 @@ export default class WorkflowRun extends Command {
|
|
|
60
59
|
deprecateAliases: true,
|
|
61
60
|
description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
62
61
|
env: 'OUTPUT_CATALOG_ID'
|
|
63
|
-
}),
|
|
64
|
-
format: Flags.string({
|
|
65
|
-
char: 'f',
|
|
66
|
-
description: 'Output format',
|
|
67
|
-
options: [OUTPUT_FORMAT.JSON, OUTPUT_FORMAT.TEXT],
|
|
68
|
-
default: OUTPUT_FORMAT.TEXT
|
|
69
62
|
})
|
|
70
63
|
};
|
|
71
64
|
async run() {
|
|
72
65
|
const { args, flags } = await this.parse(WorkflowRun);
|
|
73
|
-
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'run');
|
|
66
|
+
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'run', flags.catalog);
|
|
74
67
|
this.log(`Executing workflow: ${args.workflowName}...`);
|
|
75
68
|
const response = await executeWorkflow({
|
|
76
69
|
body: {
|
|
@@ -85,11 +78,11 @@ export default class WorkflowRun extends Command {
|
|
|
85
78
|
this.error('API returned invalid response', { exit: 1 });
|
|
86
79
|
}
|
|
87
80
|
const data = response.data;
|
|
88
|
-
|
|
89
|
-
this.log(`\n${output}`);
|
|
81
|
+
this.log(`\n${formatWorkflowResult(data)}`);
|
|
90
82
|
if (ERROR_STATUSES.has(data.status)) {
|
|
91
83
|
process.exitCode = 1;
|
|
92
84
|
}
|
|
85
|
+
return data;
|
|
93
86
|
}
|
|
94
87
|
async catch(error) {
|
|
95
88
|
return handleApiError(error, (...args) => this.error(...args), {
|
|
@@ -26,13 +26,11 @@ describe('workflow run command', () => {
|
|
|
26
26
|
expect(WorkflowRun.description).toContain('Execute a workflow');
|
|
27
27
|
expect(WorkflowRun.args).toHaveProperty('workflowName');
|
|
28
28
|
expect(WorkflowRun.flags).toHaveProperty('input');
|
|
29
|
-
expect(WorkflowRun.flags).toHaveProperty('format');
|
|
30
29
|
expect(WorkflowRun.flags).toHaveProperty('catalog');
|
|
31
30
|
});
|
|
32
31
|
it('should have correct flag configuration', async () => {
|
|
33
32
|
const WorkflowRun = (await import('./run.js')).default;
|
|
34
|
-
expect(WorkflowRun.
|
|
35
|
-
expect(WorkflowRun.flags.format.default).toBe('text');
|
|
33
|
+
expect(WorkflowRun.enableJsonFlag).toBe(true);
|
|
36
34
|
expect(WorkflowRun.flags.input.required).toBe(false);
|
|
37
35
|
});
|
|
38
36
|
it('should have optional scenario argument', async () => {
|
|
@@ -66,11 +64,28 @@ describe('workflow run command', () => {
|
|
|
66
64
|
headers: new Headers()
|
|
67
65
|
});
|
|
68
66
|
await cmd.run();
|
|
67
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'run', undefined);
|
|
69
68
|
expect(postWorkflowRun).toHaveBeenCalledTimes(1);
|
|
70
69
|
expect(postWorkflowRun).toHaveBeenCalledWith({ workflowName: 'my_workflow', input: { key: 'value' }, catalog: undefined }, expect.objectContaining({ config: { timeout: 600000 } }));
|
|
71
70
|
expect(cmd.log).toHaveBeenCalledWith('Executing workflow: my_workflow...');
|
|
72
71
|
expect(cmd.log).toHaveBeenCalledWith(expect.stringMatching(/\n/));
|
|
73
72
|
});
|
|
73
|
+
it('threads the resolved catalog to resolveInput and postWorkflowRun', async () => {
|
|
74
|
+
const { cmd, postWorkflowRun, resolveInput } = await createCommand();
|
|
75
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
76
|
+
args: { workflowName: 'my_workflow', scenario: 'basic' },
|
|
77
|
+
flags: { input: undefined, catalog: 'my-catalog', format: 'text' }
|
|
78
|
+
});
|
|
79
|
+
resolveInput.mockResolvedValue({ key: 'value' });
|
|
80
|
+
postWorkflowRun.mockResolvedValue({
|
|
81
|
+
data: { status: 'completed', result: {} },
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: new Headers()
|
|
84
|
+
});
|
|
85
|
+
await cmd.run();
|
|
86
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', 'basic', undefined, 'run', 'my-catalog');
|
|
87
|
+
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ catalog: 'my-catalog' }), expect.anything());
|
|
88
|
+
});
|
|
74
89
|
it('retries when response has Retry-After and succeeds on second attempt', async () => {
|
|
75
90
|
const { cmd, postWorkflowRun, resolveInput } = await createCommand();
|
|
76
91
|
resolveInput.mockResolvedValue({});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import { type WorkflowRun } from '#services/workflow_runs.js';
|
|
2
3
|
export default class WorkflowRunsList extends Command {
|
|
3
4
|
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
4
6
|
static examples: string[];
|
|
5
7
|
static args: {
|
|
6
8
|
workflowName: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
@@ -10,6 +12,6 @@ export default class WorkflowRunsList extends Command {
|
|
|
10
12
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
};
|
|
13
|
-
run(): Promise<
|
|
15
|
+
run(): Promise<WorkflowRun[]>;
|
|
14
16
|
catch(error: Error): Promise<void>;
|
|
15
17
|
}
|
|
@@ -5,7 +5,6 @@ import { formatDate, formatDurationFromTimestamps } from '#utils/date_formatter.
|
|
|
5
5
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
6
|
const OUTPUT_FORMAT = {
|
|
7
7
|
TABLE: 'table',
|
|
8
|
-
JSON: 'json',
|
|
9
8
|
TEXT: 'text'
|
|
10
9
|
};
|
|
11
10
|
function createRunsTable(runs) {
|
|
@@ -37,13 +36,7 @@ function formatRunsAsText(runs) {
|
|
|
37
36
|
return `${run.workflowId} (${run.workflowType}) - ${run.status} [${duration}]`;
|
|
38
37
|
}).join('\n');
|
|
39
38
|
}
|
|
40
|
-
function formatRunsAsJson(runs) {
|
|
41
|
-
return JSON.stringify(runs, null, 2);
|
|
42
|
-
}
|
|
43
39
|
function formatRuns(runs, format) {
|
|
44
|
-
if (format === OUTPUT_FORMAT.JSON) {
|
|
45
|
-
return formatRunsAsJson(runs);
|
|
46
|
-
}
|
|
47
40
|
if (format === OUTPUT_FORMAT.TABLE) {
|
|
48
41
|
return createRunsTable(runs);
|
|
49
42
|
}
|
|
@@ -51,12 +44,13 @@ function formatRuns(runs, format) {
|
|
|
51
44
|
}
|
|
52
45
|
export default class WorkflowRunsList extends Command {
|
|
53
46
|
static description = 'List workflow runs with optional filtering by workflow type';
|
|
47
|
+
static enableJsonFlag = true;
|
|
54
48
|
static examples = [
|
|
55
49
|
'<%= config.bin %> <%= command.id %>',
|
|
56
50
|
'<%= config.bin %> <%= command.id %> simple',
|
|
57
51
|
'<%= config.bin %> <%= command.id %> simple --limit 10',
|
|
58
52
|
'<%= config.bin %> <%= command.id %> --catalog my-catalog',
|
|
59
|
-
'<%= config.bin %> <%= command.id %> --
|
|
53
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
60
54
|
'<%= config.bin %> <%= command.id %> --format table'
|
|
61
55
|
];
|
|
62
56
|
static args = {
|
|
@@ -78,8 +72,8 @@ export default class WorkflowRunsList extends Command {
|
|
|
78
72
|
}),
|
|
79
73
|
format: Flags.string({
|
|
80
74
|
char: 'f',
|
|
81
|
-
description: 'Output format',
|
|
82
|
-
options: [OUTPUT_FORMAT.TABLE, OUTPUT_FORMAT.
|
|
75
|
+
description: 'Output format (use --json for JSON output)',
|
|
76
|
+
options: [OUTPUT_FORMAT.TABLE, OUTPUT_FORMAT.TEXT],
|
|
83
77
|
default: OUTPUT_FORMAT.TABLE
|
|
84
78
|
})
|
|
85
79
|
};
|
|
@@ -90,17 +84,19 @@ export default class WorkflowRunsList extends Command {
|
|
|
90
84
|
catalog: flags.catalog,
|
|
91
85
|
limit: flags.limit
|
|
92
86
|
});
|
|
87
|
+
if (this.jsonEnabled()) {
|
|
88
|
+
return runs;
|
|
89
|
+
}
|
|
93
90
|
if (runs.length === 0) {
|
|
94
91
|
const filterMsg = args.workflowName ? ` for workflow type "${args.workflowName}"` : '';
|
|
95
92
|
this.log(`No workflow runs found${filterMsg}.`);
|
|
96
|
-
return;
|
|
93
|
+
return runs;
|
|
97
94
|
}
|
|
98
95
|
const output = formatRuns(runs, flags.format);
|
|
99
96
|
this.log(output);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
97
|
+
const filterMsg = args.workflowName ? ` of type "${args.workflowName}"` : '';
|
|
98
|
+
this.log(`\nFound ${count} run(s)${filterMsg}`);
|
|
99
|
+
return runs;
|
|
104
100
|
}
|
|
105
101
|
async catch(error) {
|
|
106
102
|
return handleApiError(error, (...args) => this.error(...args), {
|
|
@@ -37,7 +37,7 @@ export default class WorkflowStart extends Command {
|
|
|
37
37
|
};
|
|
38
38
|
async run() {
|
|
39
39
|
const { args, flags } = await this.parse(WorkflowStart);
|
|
40
|
-
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'start');
|
|
40
|
+
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'start', flags.catalog);
|
|
41
41
|
this.log(`Starting workflow: ${args.workflowName}...`);
|
|
42
42
|
const response = await postWorkflowStart({
|
|
43
43
|
workflowName: args.workflowName,
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
vi.mock('
|
|
3
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
3
4
|
postWorkflowStart: vi.fn()
|
|
4
5
|
}));
|
|
6
|
+
vi.mock('#utils/resolve_input.js', () => ({
|
|
7
|
+
resolveInput: vi.fn()
|
|
8
|
+
}));
|
|
5
9
|
describe('workflow start command', () => {
|
|
6
|
-
beforeEach(() => {
|
|
10
|
+
beforeEach(async () => {
|
|
7
11
|
vi.clearAllMocks();
|
|
8
12
|
delete process.env.OUTPUT_CATALOG_ID;
|
|
13
|
+
const { resolveInput } = await import('#utils/resolve_input.js');
|
|
14
|
+
vi.mocked(resolveInput).mockResolvedValue({});
|
|
9
15
|
});
|
|
10
16
|
describe('command definition', () => {
|
|
11
17
|
it('should export a valid OCLIF command', async () => {
|
|
@@ -25,5 +31,50 @@ describe('workflow start command', () => {
|
|
|
25
31
|
expect(WorkflowStart.args).toHaveProperty('scenario');
|
|
26
32
|
expect(WorkflowStart.args.scenario.required).toBe(false);
|
|
27
33
|
});
|
|
34
|
+
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
35
|
+
const WorkflowStart = (await import('./start.js')).default;
|
|
36
|
+
expect(WorkflowStart.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
37
|
+
expect(WorkflowStart.flags.catalog.char).toBe('c');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('run()', () => {
|
|
41
|
+
const createCommand = async (flagOverrides = {}) => {
|
|
42
|
+
const WorkflowStart = (await import('./start.js')).default;
|
|
43
|
+
const { postWorkflowStart } = await import('#api/generated/api.js');
|
|
44
|
+
const { resolveInput } = await import('#utils/resolve_input.js');
|
|
45
|
+
const cmd = new WorkflowStart(['my_workflow'], {});
|
|
46
|
+
cmd.log = vi.fn();
|
|
47
|
+
cmd.error = vi.fn(() => {
|
|
48
|
+
throw new Error('error called');
|
|
49
|
+
});
|
|
50
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
51
|
+
args: { workflowName: 'my_workflow', scenario: undefined },
|
|
52
|
+
flags: { input: undefined, catalog: undefined, ...flagOverrides }
|
|
53
|
+
});
|
|
54
|
+
return { cmd, postWorkflowStart: vi.mocked(postWorkflowStart), resolveInput: vi.mocked(resolveInput) };
|
|
55
|
+
};
|
|
56
|
+
it('threads the resolved catalog to resolveInput and postWorkflowStart', async () => {
|
|
57
|
+
const { cmd, postWorkflowStart, resolveInput } = await createCommand({ catalog: 'my-catalog' });
|
|
58
|
+
resolveInput.mockResolvedValue({ key: 'value' });
|
|
59
|
+
postWorkflowStart.mockResolvedValue({
|
|
60
|
+
data: { workflowId: 'wf-123' },
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: new Headers()
|
|
63
|
+
});
|
|
64
|
+
await cmd.run();
|
|
65
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'start', 'my-catalog');
|
|
66
|
+
expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }));
|
|
67
|
+
});
|
|
68
|
+
it('passes undefined catalog through when none is set', async () => {
|
|
69
|
+
const { cmd, postWorkflowStart, resolveInput } = await createCommand();
|
|
70
|
+
resolveInput.mockResolvedValue({});
|
|
71
|
+
postWorkflowStart.mockResolvedValue({
|
|
72
|
+
data: { workflowId: 'wf-123' },
|
|
73
|
+
status: 200,
|
|
74
|
+
headers: new Headers()
|
|
75
|
+
});
|
|
76
|
+
await cmd.run();
|
|
77
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'start', undefined);
|
|
78
|
+
});
|
|
28
79
|
});
|
|
29
80
|
});
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import { WorkflowStatusResponse } from '#api/generated/api.js';
|
|
2
3
|
export default class WorkflowStatus extends Command {
|
|
3
4
|
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
4
6
|
static examples: string[];
|
|
5
7
|
static args: {
|
|
6
8
|
workflowId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
9
|
};
|
|
8
|
-
|
|
9
|
-
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
};
|
|
11
|
-
run(): Promise<void>;
|
|
10
|
+
run(): Promise<WorkflowStatusResponse>;
|
|
12
11
|
catch(error: Error): Promise<void>;
|
|
13
12
|
}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { Args, Command
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
2
|
import { getWorkflowIdStatus } from '#api/generated/api.js';
|
|
3
|
-
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
4
|
-
import { formatOutput } from '#utils/output_formatter.js';
|
|
5
3
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
4
|
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
5
|
+
function formatStatusText(result) {
|
|
6
|
+
const lines = [
|
|
7
|
+
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
8
|
+
`Status: ${result.status || 'unknown'}`,
|
|
9
|
+
''
|
|
10
|
+
];
|
|
11
|
+
if (result.startedAt) {
|
|
12
|
+
lines.push(`Started At: ${new Date(result.startedAt).toISOString()}`);
|
|
13
|
+
}
|
|
14
|
+
if (result.completedAt) {
|
|
15
|
+
lines.push(`Completed At: ${new Date(result.completedAt).toISOString()}`);
|
|
16
|
+
}
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
7
19
|
export default class WorkflowStatus extends Command {
|
|
8
20
|
static description = 'Get workflow execution status';
|
|
21
|
+
static enableJsonFlag = true;
|
|
9
22
|
static examples = [
|
|
10
23
|
'<%= config.bin %> <%= command.id %> wf-12345',
|
|
11
|
-
'<%= config.bin %> <%= command.id %> wf-12345 --
|
|
24
|
+
'<%= config.bin %> <%= command.id %> wf-12345 --json'
|
|
12
25
|
];
|
|
13
26
|
static args = {
|
|
14
27
|
workflowId: Args.string({
|
|
@@ -16,16 +29,8 @@ export default class WorkflowStatus extends Command {
|
|
|
16
29
|
required: true
|
|
17
30
|
})
|
|
18
31
|
};
|
|
19
|
-
static flags = {
|
|
20
|
-
format: Flags.string({
|
|
21
|
-
char: 'f',
|
|
22
|
-
description: 'Output format',
|
|
23
|
-
options: [OUTPUT_FORMAT.JSON, OUTPUT_FORMAT.TEXT],
|
|
24
|
-
default: OUTPUT_FORMAT.TEXT
|
|
25
|
-
})
|
|
26
|
-
};
|
|
27
32
|
async run() {
|
|
28
|
-
const { args
|
|
33
|
+
const { args } = await this.parse(WorkflowStatus);
|
|
29
34
|
this.log(`Fetching status for workflow: ${args.workflowId}...`);
|
|
30
35
|
const response = await getWorkflowIdStatus(args.workflowId);
|
|
31
36
|
if (!response || !response.data) {
|
|
@@ -36,23 +41,8 @@ export default class WorkflowStatus extends Command {
|
|
|
36
41
|
...rawData,
|
|
37
42
|
status: normalizeWorkflowStatus(rawData.status)
|
|
38
43
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
42
|
-
`Status: ${result.status || 'unknown'}`,
|
|
43
|
-
''
|
|
44
|
-
];
|
|
45
|
-
if (result.startedAt) {
|
|
46
|
-
const startDate = new Date(result.startedAt);
|
|
47
|
-
lines.push(`Started At: ${startDate.toISOString()}`);
|
|
48
|
-
}
|
|
49
|
-
if (result.completedAt) {
|
|
50
|
-
const completedDate = new Date(result.completedAt);
|
|
51
|
-
lines.push(`Completed At: ${completedDate.toISOString()}`);
|
|
52
|
-
}
|
|
53
|
-
return lines.join('\n');
|
|
54
|
-
});
|
|
55
|
-
this.log(`\n${output}`);
|
|
44
|
+
this.log(`\n${formatStatusText(data)}`);
|
|
45
|
+
return data;
|
|
56
46
|
}
|
|
57
47
|
async catch(error) {
|
|
58
48
|
return handleApiError(error, (...args) => this.error(...args), {
|
|
@@ -22,12 +22,10 @@ describe('workflow status command', () => {
|
|
|
22
22
|
expect(WorkflowStatus).toBeDefined();
|
|
23
23
|
expect(WorkflowStatus.description).toContain('Get workflow execution status');
|
|
24
24
|
expect(WorkflowStatus.args).toHaveProperty('workflowId');
|
|
25
|
-
expect(WorkflowStatus.flags).toHaveProperty('format');
|
|
26
25
|
});
|
|
27
|
-
it('
|
|
26
|
+
it('enables the built-in --json flag', async () => {
|
|
28
27
|
const WorkflowStatus = (await import('./status.js')).default;
|
|
29
|
-
expect(WorkflowStatus.
|
|
30
|
-
expect(WorkflowStatus.flags.format.default).toBe('text');
|
|
28
|
+
expect(WorkflowStatus.enableJsonFlag).toBe(true);
|
|
31
29
|
});
|
|
32
30
|
});
|
|
33
31
|
});
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import type { EvalOutput } from '@outputai/evals';
|
|
2
3
|
export default class WorkflowTest extends Command {
|
|
3
4
|
static aliases: string[];
|
|
4
5
|
static description: string;
|
|
6
|
+
static enableJsonFlag: boolean;
|
|
5
7
|
static examples: string[];
|
|
6
8
|
static args: {
|
|
7
9
|
workflowName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
10
|
};
|
|
9
11
|
static flags: {
|
|
12
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
13
|
cached: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
14
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
15
|
dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
16
|
};
|
|
15
|
-
run(): Promise<
|
|
17
|
+
run(): Promise<EvalOutput>;
|
|
16
18
|
private ensureEvalWorkflowRegistered;
|
|
17
19
|
private validateDatasets;
|
|
18
20
|
private runWorkflowForDatasets;
|
|
@@ -9,12 +9,13 @@ import { getEvalWorkflowName, renderEvalOutput, computeExitCode, EvalOutputSchem
|
|
|
9
9
|
export default class WorkflowTest extends Command {
|
|
10
10
|
static aliases = ['workflow:test'];
|
|
11
11
|
static description = 'Run evaluations against a workflow using its datasets';
|
|
12
|
+
static enableJsonFlag = true;
|
|
12
13
|
static examples = [
|
|
13
14
|
'<%= config.bin %> <%= command.id %> simple',
|
|
14
15
|
'<%= config.bin %> <%= command.id %> simple --cached',
|
|
15
16
|
'<%= config.bin %> <%= command.id %> simple --save',
|
|
16
17
|
'<%= config.bin %> <%= command.id %> simple --dataset basic_input,edge_case',
|
|
17
|
-
'<%= config.bin %> <%= command.id %> simple --
|
|
18
|
+
'<%= config.bin %> <%= command.id %> simple --json'
|
|
18
19
|
];
|
|
19
20
|
static args = {
|
|
20
21
|
workflowName: Args.string({
|
|
@@ -23,6 +24,14 @@ export default class WorkflowTest extends Command {
|
|
|
23
24
|
})
|
|
24
25
|
};
|
|
25
26
|
static flags = {
|
|
27
|
+
catalog: Flags.string({
|
|
28
|
+
char: 'c',
|
|
29
|
+
aliases: ['task-queue'],
|
|
30
|
+
charAliases: ['q'],
|
|
31
|
+
deprecateAliases: true,
|
|
32
|
+
description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
33
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
34
|
+
}),
|
|
26
35
|
cached: Flags.boolean({
|
|
27
36
|
description: 'Use cached output from dataset files (skip workflow execution)',
|
|
28
37
|
default: false,
|
|
@@ -36,19 +45,13 @@ export default class WorkflowTest extends Command {
|
|
|
36
45
|
dataset: Flags.string({
|
|
37
46
|
description: 'Comma-separated list of dataset names to run',
|
|
38
47
|
char: 'd'
|
|
39
|
-
}),
|
|
40
|
-
format: Flags.string({
|
|
41
|
-
char: 'f',
|
|
42
|
-
description: 'Output format',
|
|
43
|
-
options: ['json', 'text'],
|
|
44
|
-
default: 'text'
|
|
45
48
|
})
|
|
46
49
|
};
|
|
47
50
|
async run() {
|
|
48
51
|
const { args, flags } = await this.parse(WorkflowTest);
|
|
49
52
|
const filterNames = flags.dataset?.split(',').map(s => s.trim());
|
|
50
53
|
const evalName = getEvalWorkflowName(args.workflowName);
|
|
51
|
-
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName);
|
|
54
|
+
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName, flags.catalog);
|
|
52
55
|
const { datasets, dir } = await readAllDatasets(args.workflowName, filterNames);
|
|
53
56
|
if (datasets.length === 0) {
|
|
54
57
|
this.error(`No datasets found for workflow "${args.workflowName}".\n` +
|
|
@@ -56,11 +59,12 @@ export default class WorkflowTest extends Command {
|
|
|
56
59
|
}
|
|
57
60
|
const preparedDatasets = flags.cached ?
|
|
58
61
|
this.validateDatasets(datasets) :
|
|
59
|
-
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir);
|
|
62
|
+
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir, flags.catalog);
|
|
60
63
|
this.log(`Running eval workflow "${evalName}"...\n`);
|
|
61
64
|
const response = await postWorkflowRun({
|
|
62
65
|
workflowName: evalName,
|
|
63
|
-
input: { datasets: preparedDatasets }
|
|
66
|
+
input: { datasets: preparedDatasets },
|
|
67
|
+
catalog: flags.catalog
|
|
64
68
|
}, {
|
|
65
69
|
config: { timeout: 600000 }
|
|
66
70
|
});
|
|
@@ -72,20 +76,15 @@ export default class WorkflowTest extends Command {
|
|
|
72
76
|
if (flags.save) {
|
|
73
77
|
await this.saveEvalResults(evalOutput, preparedDatasets, dir);
|
|
74
78
|
}
|
|
75
|
-
if (
|
|
76
|
-
this.log(JSON.stringify(evalOutput, null, 2));
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
+
if (!this.jsonEnabled()) {
|
|
79
80
|
this.log(renderEvalOutput(evalOutput, evalName));
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.exit(exitCode);
|
|
84
|
-
}
|
|
82
|
+
process.exitCode = computeExitCode(evalOutput);
|
|
83
|
+
return evalOutput;
|
|
85
84
|
}
|
|
86
|
-
async ensureEvalWorkflowRegistered(workflowName, evalName) {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
85
|
+
async ensureEvalWorkflowRegistered(workflowName, evalName, catalog) {
|
|
86
|
+
const workflows = await fetchWorkflowCatalog(catalog).catch(() => null);
|
|
87
|
+
if (workflows && !workflows.some(w => w.name === evalName)) {
|
|
89
88
|
this.error(await diagnoseMissingEvalWorkflow(workflowName), { exit: 1 });
|
|
90
89
|
}
|
|
91
90
|
}
|
|
@@ -98,7 +97,7 @@ export default class WorkflowTest extends Command {
|
|
|
98
97
|
}
|
|
99
98
|
return datasets;
|
|
100
99
|
}
|
|
101
|
-
async runWorkflowForDatasets(workflowName, datasets, save, dir) {
|
|
100
|
+
async runWorkflowForDatasets(workflowName, datasets, save, dir, catalog) {
|
|
102
101
|
this.log(`Running workflow "${workflowName}" for ${datasets.length} dataset(s)...\n`);
|
|
103
102
|
const results = [];
|
|
104
103
|
for (const dataset of datasets) {
|
|
@@ -106,7 +105,8 @@ export default class WorkflowTest extends Command {
|
|
|
106
105
|
const startMs = Date.now();
|
|
107
106
|
const response = await postWorkflowRun({
|
|
108
107
|
workflowName,
|
|
109
|
-
input: dataset.input
|
|
108
|
+
input: dataset.input,
|
|
109
|
+
catalog
|
|
110
110
|
}, {
|
|
111
111
|
config: { timeout: 600000 }
|
|
112
112
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { getEvalWorkflowName, renderEvalOutput } from '@outputai/evals';
|
|
4
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
5
|
+
postWorkflowRun: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('#api/workflow_catalog.js', () => ({
|
|
8
|
+
fetchWorkflowCatalog: vi.fn()
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('#services/datasets.js', () => ({
|
|
11
|
+
readAllDatasets: vi.fn(),
|
|
12
|
+
writeDataset: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('#utils/eval_diagnostics.js', () => ({
|
|
15
|
+
diagnoseMissingEvalWorkflow: vi.fn().mockResolvedValue('missing eval workflow')
|
|
16
|
+
}));
|
|
17
|
+
const passingOutput = {
|
|
18
|
+
cases: [{ datasetName: 'd1', verdict: 'pass', evaluators: [] }],
|
|
19
|
+
summary: { total: 1, passed: 1, partial: 0, failed: 0, acceptableRate: 1 }
|
|
20
|
+
};
|
|
21
|
+
const failingOutput = {
|
|
22
|
+
cases: [{ datasetName: 'd1', verdict: 'fail', evaluators: [] }],
|
|
23
|
+
summary: { total: 1, passed: 0, partial: 0, failed: 1, acceptableRate: 0 }
|
|
24
|
+
};
|
|
25
|
+
describe('workflow test command', () => {
|
|
26
|
+
const exitState = { original: undefined };
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
exitState.original = process.exitCode;
|
|
30
|
+
process.exitCode = undefined;
|
|
31
|
+
const { readAllDatasets } = await import('#services/datasets.js');
|
|
32
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
33
|
+
vi.mocked(readAllDatasets).mockResolvedValue({
|
|
34
|
+
datasets: [{ name: 'd1', input: {}, last_output: { output: {}, date: '2026-01-01' } }],
|
|
35
|
+
dir: '/tmp/datasets'
|
|
36
|
+
});
|
|
37
|
+
// Catalog includes both eval names so ensureEvalWorkflowRegistered passes deterministically.
|
|
38
|
+
vi.mocked(fetchWorkflowCatalog).mockResolvedValue([
|
|
39
|
+
{ name: getEvalWorkflowName('simple') },
|
|
40
|
+
{ name: getEvalWorkflowName('my_workflow') }
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.exitCode = exitState.original;
|
|
45
|
+
});
|
|
46
|
+
describe('command definition', () => {
|
|
47
|
+
it('enables the built-in --json flag', async () => {
|
|
48
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
49
|
+
expect(WorkflowTest.enableJsonFlag).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
52
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
53
|
+
expect(WorkflowTest.flags).toHaveProperty('catalog');
|
|
54
|
+
expect(WorkflowTest.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
55
|
+
expect(WorkflowTest.flags.catalog.char).toBe('c');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('run()', () => {
|
|
59
|
+
const createCommand = async (jsonEnabled) => {
|
|
60
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
61
|
+
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
62
|
+
const cmd = new WorkflowTest(['simple'], {});
|
|
63
|
+
cmd.log = vi.fn();
|
|
64
|
+
cmd.jsonEnabled = vi.fn().mockReturnValue(jsonEnabled);
|
|
65
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
66
|
+
args: { workflowName: 'simple' },
|
|
67
|
+
flags: { cached: true, save: false, dataset: undefined }
|
|
68
|
+
});
|
|
69
|
+
return { cmd, postWorkflowRun: vi.mocked(postWorkflowRun) };
|
|
70
|
+
};
|
|
71
|
+
it('sets a non-zero exit code and returns the eval output when a case fails', async () => {
|
|
72
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
73
|
+
postWorkflowRun.mockResolvedValue({ data: { output: failingOutput } });
|
|
74
|
+
const result = await cmd.run();
|
|
75
|
+
expect(result).toEqual(failingOutput);
|
|
76
|
+
expect(process.exitCode).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
it('leaves the exit code at zero and returns the eval output when all cases pass', async () => {
|
|
79
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
80
|
+
postWorkflowRun.mockResolvedValue({ data: { output: passingOutput } });
|
|
81
|
+
const result = await cmd.run();
|
|
82
|
+
expect(result).toEqual(passingOutput);
|
|
83
|
+
expect(process.exitCode).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
it('renders the human-readable summary in text mode', async () => {
|
|
86
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
87
|
+
postWorkflowRun.mockResolvedValue({ data: { output: passingOutput } });
|
|
88
|
+
await cmd.run();
|
|
89
|
+
const rendered = renderEvalOutput(passingOutput, getEvalWorkflowName('simple'));
|
|
90
|
+
expect(cmd.log).toHaveBeenCalledWith(rendered);
|
|
91
|
+
});
|
|
92
|
+
it('suppresses the rendered summary in JSON mode but still returns and sets exit code', async () => {
|
|
93
|
+
const { cmd, postWorkflowRun } = await createCommand(true);
|
|
94
|
+
postWorkflowRun.mockResolvedValue({ data: { output: failingOutput } });
|
|
95
|
+
const result = await cmd.run();
|
|
96
|
+
const rendered = renderEvalOutput(failingOutput, getEvalWorkflowName('simple'));
|
|
97
|
+
expect(cmd.log).not.toHaveBeenCalledWith(rendered);
|
|
98
|
+
expect(result).toEqual(failingOutput);
|
|
99
|
+
expect(process.exitCode).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
it('routes registration, dataset runs, and the eval run to the resolved catalog', async () => {
|
|
102
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
103
|
+
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
104
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
105
|
+
const cmd = new WorkflowTest(['my_workflow'], {});
|
|
106
|
+
cmd.log = vi.fn();
|
|
107
|
+
cmd.jsonEnabled = vi.fn().mockReturnValue(false);
|
|
108
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
109
|
+
args: { workflowName: 'my_workflow' },
|
|
110
|
+
flags: { catalog: 'my-catalog', cached: false, save: false, dataset: undefined }
|
|
111
|
+
});
|
|
112
|
+
vi.mocked(postWorkflowRun)
|
|
113
|
+
.mockResolvedValueOnce({ data: { output: {} }, status: 200, headers: new Headers() })
|
|
114
|
+
.mockResolvedValueOnce({ data: { output: passingOutput }, status: 200, headers: new Headers() });
|
|
115
|
+
await cmd.run();
|
|
116
|
+
expect(vi.mocked(fetchWorkflowCatalog)).toHaveBeenCalledWith('my-catalog');
|
|
117
|
+
expect(postWorkflowRun).toHaveBeenNthCalledWith(1, expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }), expect.anything());
|
|
118
|
+
expect(postWorkflowRun).toHaveBeenNthCalledWith(2, expect.objectContaining({ workflowName: getEvalWorkflowName('my_workflow'), catalog: 'my-catalog' }), expect.anything());
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
package/dist/hooks/init.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Hook } from '@oclif/core';
|
|
|
2
2
|
export declare const INTERACTIVE_FLAGS: string[];
|
|
3
3
|
export declare const GLOBAL_FLAGS: Set<string>;
|
|
4
4
|
export declare const hasInteractiveFlag: (argv: string[]) => boolean;
|
|
5
|
+
export declare const hasJsonFlag: (argv: string[]) => boolean;
|
|
5
6
|
export declare const stripGlobalFlags: (argv: string[]) => void;
|
|
6
7
|
declare const hook: Hook<'init'>;
|
|
7
8
|
export default hook;
|