@output.ai/cli 0.3.1-dev.pr156.0 → 0.4.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/README.md +2 -28
- package/dist/api/generated/api.d.ts +35 -59
- package/dist/api/generated/api.js +4 -13
- package/dist/assets/docker/docker-compose-dev.yml +2 -2
- package/dist/commands/workflow/debug.d.ts +2 -8
- package/dist/commands/workflow/debug.js +24 -164
- package/dist/commands/workflow/debug.spec.js +36 -0
- package/dist/commands/workflow/generate.js +3 -10
- package/dist/commands/workflow/generate.spec.js +6 -4
- package/dist/commands/workflow/{output.d.ts → result.d.ts} +1 -1
- package/dist/commands/workflow/{output.js → result.js} +8 -8
- package/dist/commands/workflow/result.test.js +23 -0
- package/dist/commands/workflow/start.js +1 -1
- package/dist/services/coding_agents.js +30 -0
- package/dist/services/coding_agents.spec.js +36 -61
- package/dist/services/messages.d.ts +1 -0
- package/dist/services/messages.js +65 -1
- package/dist/services/trace_reader.d.ts +14 -0
- package/dist/services/trace_reader.js +67 -0
- package/dist/services/trace_reader.spec.d.ts +1 -0
- package/dist/services/trace_reader.spec.js +164 -0
- package/dist/services/workflow_generator.spec.d.ts +1 -0
- package/dist/services/workflow_generator.spec.js +77 -0
- package/dist/templates/agent_instructions/AGENTS.md.template +209 -19
- package/dist/templates/agent_instructions/agents/context_fetcher.md.template +82 -0
- package/dist/templates/agent_instructions/agents/prompt_writer.md.template +595 -0
- package/dist/templates/agent_instructions/agents/workflow_planner.md.template +13 -4
- package/dist/templates/agent_instructions/agents/workflow_quality.md.template +244 -0
- package/dist/templates/agent_instructions/commands/build_workflow.md.template +52 -9
- package/dist/templates/agent_instructions/commands/plan_workflow.md.template +4 -4
- package/dist/templates/project/package.json.template +2 -2
- package/dist/templates/project/src/simple/scenarios/question_ada_lovelace.json.template +3 -0
- package/dist/templates/workflow/scenarios/test_input.json.template +7 -0
- package/dist/types/trace.d.ts +161 -0
- package/dist/types/trace.js +18 -0
- package/dist/utils/date_formatter.d.ts +8 -0
- package/dist/utils/date_formatter.js +19 -0
- package/dist/utils/template.spec.js +6 -0
- package/dist/utils/trace_formatter.d.ts +11 -61
- package/dist/utils/trace_formatter.js +384 -239
- package/package.json +2 -2
- package/dist/commands/workflow/debug.test.js +0 -107
- package/dist/commands/workflow/output.test.js +0 -23
- package/dist/utils/s3_downloader.d.ts +0 -49
- package/dist/utils/s3_downloader.js +0 -154
- /package/dist/commands/workflow/{debug.test.d.ts → debug.spec.d.ts} +0 -0
- /package/dist/commands/workflow/{output.test.d.ts → result.test.d.ts} +0 -0
- /package/dist/templates/workflow/{prompt@v1.prompt.template → prompts/prompt@v1.prompt.template} +0 -0
|
@@ -29,6 +29,21 @@ export const AGENT_CONFIGS = {
|
|
|
29
29
|
from: 'agents/workflow_planner.md.template',
|
|
30
30
|
to: `${AGENT_CONFIG_DIR}/agents/workflow_planner.md`
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
type: 'template',
|
|
34
|
+
from: 'agents/workflow_quality.md.template',
|
|
35
|
+
to: `${AGENT_CONFIG_DIR}/agents/workflow_quality.md`
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'template',
|
|
39
|
+
from: 'agents/context_fetcher.md.template',
|
|
40
|
+
to: `${AGENT_CONFIG_DIR}/agents/context_fetcher.md`
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'template',
|
|
44
|
+
from: 'agents/prompt_writer.md.template',
|
|
45
|
+
to: `${AGENT_CONFIG_DIR}/agents/prompt_writer.md`
|
|
46
|
+
},
|
|
32
47
|
{
|
|
33
48
|
type: 'template',
|
|
34
49
|
from: 'commands/plan_workflow.md.template',
|
|
@@ -65,6 +80,21 @@ export const AGENT_CONFIGS = {
|
|
|
65
80
|
from: `${AGENT_CONFIG_DIR}/agents/workflow_planner.md`,
|
|
66
81
|
to: '.claude/agents/workflow_planner.md'
|
|
67
82
|
},
|
|
83
|
+
{
|
|
84
|
+
type: 'symlink',
|
|
85
|
+
from: `${AGENT_CONFIG_DIR}/agents/workflow_quality.md`,
|
|
86
|
+
to: '.claude/agents/workflow_quality.md'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'symlink',
|
|
90
|
+
from: `${AGENT_CONFIG_DIR}/agents/context_fetcher.md`,
|
|
91
|
+
to: '.claude/agents/context_fetcher.md'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'symlink',
|
|
95
|
+
from: `${AGENT_CONFIG_DIR}/agents/prompt_writer.md`,
|
|
96
|
+
to: '.claude/agents/prompt_writer.md'
|
|
97
|
+
},
|
|
68
98
|
{
|
|
69
99
|
type: 'symlink',
|
|
70
100
|
from: `${AGENT_CONFIG_DIR}/commands/plan_workflow.md`,
|
|
@@ -39,11 +39,11 @@ describe('coding_agents service', () => {
|
|
|
39
39
|
const expectedCount = AGENT_CONFIGS.outputai.mappings.length +
|
|
40
40
|
AGENT_CONFIGS['claude-code'].mappings.length;
|
|
41
41
|
expect(files.length).toBe(expectedCount);
|
|
42
|
-
expect(files.length).toBe(
|
|
42
|
+
expect(files.length).toBe(16);
|
|
43
43
|
});
|
|
44
44
|
it('should have outputai files with .outputai prefix', () => {
|
|
45
45
|
const files = getRequiredFiles();
|
|
46
|
-
const outputaiFiles = files.slice(0,
|
|
46
|
+
const outputaiFiles = files.slice(0, 9);
|
|
47
47
|
outputaiFiles.forEach(file => {
|
|
48
48
|
expect(file).toMatch(/^\.outputai\//);
|
|
49
49
|
});
|
|
@@ -77,12 +77,18 @@ describe('coding_agents service', () => {
|
|
|
77
77
|
missingFiles: [
|
|
78
78
|
'.outputai/AGENTS.md',
|
|
79
79
|
'.outputai/agents/workflow_planner.md',
|
|
80
|
+
'.outputai/agents/workflow_quality.md',
|
|
81
|
+
'.outputai/agents/context_fetcher.md',
|
|
82
|
+
'.outputai/agents/prompt_writer.md',
|
|
80
83
|
'.outputai/commands/plan_workflow.md',
|
|
81
84
|
'.outputai/commands/build_workflow.md',
|
|
82
85
|
'.outputai/meta/pre_flight.md',
|
|
83
86
|
'.outputai/meta/post_flight.md',
|
|
84
87
|
'CLAUDE.md',
|
|
85
88
|
'.claude/agents/workflow_planner.md',
|
|
89
|
+
'.claude/agents/workflow_quality.md',
|
|
90
|
+
'.claude/agents/context_fetcher.md',
|
|
91
|
+
'.claude/agents/prompt_writer.md',
|
|
86
92
|
'.claude/commands/plan_workflow.md',
|
|
87
93
|
'.claude/commands/build_workflow.md'
|
|
88
94
|
],
|
|
@@ -98,54 +104,21 @@ describe('coding_agents service', () => {
|
|
|
98
104
|
missingFiles: [],
|
|
99
105
|
isComplete: true
|
|
100
106
|
});
|
|
101
|
-
expect(access).toHaveBeenCalledTimes(
|
|
107
|
+
expect(access).toHaveBeenCalledTimes(17); // dir + 9 outputai + 7 claude-code
|
|
102
108
|
});
|
|
103
109
|
it('should return missing files when some files do not exist', async () => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
// Call 3: .outputai/agents/workflow_planner.md missing
|
|
117
|
-
if (callNum === 3) {
|
|
118
|
-
throw { code: 'ENOENT' };
|
|
119
|
-
}
|
|
120
|
-
// Call 4: .outputai/commands/plan_workflow.md exists
|
|
121
|
-
if (callNum === 4) {
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
// Call 5: .outputai/commands/build_workflow.md exists
|
|
125
|
-
if (callNum === 5) {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
// Call 6: .outputai/meta/pre_flight.md exists
|
|
129
|
-
if (callNum === 6) {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
// Call 7: .outputai/meta/post_flight.md exists
|
|
133
|
-
if (callNum === 7) {
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
// Call 8: CLAUDE.md exists
|
|
137
|
-
if (callNum === 8) {
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
// Call 9: .claude/agents/workflow_planner.md missing
|
|
141
|
-
if (callNum === 9) {
|
|
142
|
-
throw { code: 'ENOENT' };
|
|
143
|
-
}
|
|
144
|
-
// Call 10: .claude/commands/plan_workflow.md exists
|
|
145
|
-
if (callNum === 10) {
|
|
146
|
-
return undefined;
|
|
110
|
+
const missingFiles = new Set([
|
|
111
|
+
'.outputai/agents/workflow_planner.md',
|
|
112
|
+
'.claude/agents/workflow_planner.md'
|
|
113
|
+
]);
|
|
114
|
+
vi.mocked(access).mockImplementation(async (path) => {
|
|
115
|
+
const pathStr = path.toString();
|
|
116
|
+
// Check if this path ends with any of the missing files
|
|
117
|
+
for (const missing of missingFiles) {
|
|
118
|
+
if (pathStr.endsWith(missing)) {
|
|
119
|
+
throw { code: 'ENOENT' };
|
|
120
|
+
}
|
|
147
121
|
}
|
|
148
|
-
// Call 11: .claude/commands/build_workflow.md exists
|
|
149
122
|
return undefined;
|
|
150
123
|
});
|
|
151
124
|
const result = await checkAgentStructure('/test/project');
|
|
@@ -159,18 +132,20 @@ describe('coding_agents service', () => {
|
|
|
159
132
|
});
|
|
160
133
|
});
|
|
161
134
|
it('should check all required files even when directory exists', async () => {
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
135
|
+
const missingFiles = new Set([
|
|
136
|
+
'.outputai/agents/workflow_planner.md',
|
|
137
|
+
'.outputai/commands/plan_workflow.md',
|
|
138
|
+
'.claude/commands/plan_workflow.md'
|
|
139
|
+
]);
|
|
140
|
+
vi.mocked(access).mockImplementation(async (path) => {
|
|
141
|
+
const pathStr = path.toString();
|
|
142
|
+
for (const missing of missingFiles) {
|
|
143
|
+
if (pathStr.endsWith(missing)) {
|
|
144
|
+
throw { code: 'ENOENT' };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
});
|
|
174
149
|
const result = await checkAgentStructure('/test/project');
|
|
175
150
|
expect(result.dirExists).toBe(true);
|
|
176
151
|
expect(result.missingFiles).toEqual([
|
|
@@ -207,8 +182,8 @@ describe('coding_agents service', () => {
|
|
|
207
182
|
});
|
|
208
183
|
// Should create outputai files (6 templates)
|
|
209
184
|
expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md'), expect.any(String), 'utf-8');
|
|
210
|
-
// Should create symlinks (
|
|
211
|
-
expect(fs.symlink).toHaveBeenCalledTimes(
|
|
185
|
+
// Should create symlinks (7 symlinks for claude-code)
|
|
186
|
+
expect(fs.symlink).toHaveBeenCalledTimes(7);
|
|
212
187
|
});
|
|
213
188
|
it('should skip existing files when force is false', async () => {
|
|
214
189
|
// Mock some files exist
|
|
@@ -3,3 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const getEjectSuccessMessage: (destPath: string, outputFile: string, binName: string) => string;
|
|
5
5
|
export declare const getProjectSuccessMessage: (folderName: string, installSuccess: boolean, envConfigured?: boolean) => string;
|
|
6
|
+
export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, targetDir: string, filesCreated: string[]) => string;
|
|
@@ -176,7 +176,7 @@ export const getProjectSuccessMessage = (folderName, installSuccess, envConfigur
|
|
|
176
176
|
note: 'Launches Temporal, Redis, PostgreSQL, API, Worker, and UI'
|
|
177
177
|
}, {
|
|
178
178
|
step: 'Run example workflow',
|
|
179
|
-
command: 'output workflow run simple --input
|
|
179
|
+
command: 'output workflow run simple --input src/simple/scenarios/question_ada_lovelace.json',
|
|
180
180
|
note: 'Execute in a new terminal after services are running'
|
|
181
181
|
}, {
|
|
182
182
|
step: 'Monitor workflows',
|
|
@@ -231,3 +231,67 @@ ${ux.colorize('dim', ' with AI assistance.')}
|
|
|
231
231
|
${ux.colorize('green', ux.colorize('bold', 'Happy building with Output SDK! 🚀'))}
|
|
232
232
|
`;
|
|
233
233
|
};
|
|
234
|
+
export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, filesCreated) => {
|
|
235
|
+
const divider = ux.colorize('dim', '─'.repeat(80));
|
|
236
|
+
const bulletPoint = ux.colorize('green', '▸');
|
|
237
|
+
const formattedFiles = filesCreated.map(file => {
|
|
238
|
+
return ` ${bulletPoint} ${formatPath(file)}`;
|
|
239
|
+
}).join('\n');
|
|
240
|
+
const steps = [
|
|
241
|
+
{
|
|
242
|
+
step: 'Navigate to workflow directory',
|
|
243
|
+
command: `cd ${targetDir}`
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
step: 'Edit workflow files',
|
|
247
|
+
note: 'Customize workflow.ts, steps.ts, and prompts to match your requirements'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
step: 'Configure environment',
|
|
251
|
+
command: 'Edit .env file',
|
|
252
|
+
note: 'Add your LLM provider credentials (ANTHROPIC_API_KEY or OPENAI_API_KEY)'
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
step: 'Test with example scenario',
|
|
256
|
+
command: `output workflow run ${workflowName} --input ${targetDir}/scenarios/test_input.json`,
|
|
257
|
+
note: 'Run after starting services with "output dev"'
|
|
258
|
+
}
|
|
259
|
+
];
|
|
260
|
+
const formattedSteps = steps.map((item, index) => {
|
|
261
|
+
const stepNumber = ux.colorize('dim', `${index + 1}.`);
|
|
262
|
+
const stepText = ux.colorize('white', item.step);
|
|
263
|
+
const command = item.command ? `\n ${bulletPoint} ${formatCommand(item.command)}` : '';
|
|
264
|
+
const note = item.note ? `\n ${ux.colorize('dim', ` ${item.note}`)}` : '';
|
|
265
|
+
return ` ${stepNumber} ${stepText}${command}${note}`;
|
|
266
|
+
}).join('\n\n');
|
|
267
|
+
return `
|
|
268
|
+
${divider}
|
|
269
|
+
|
|
270
|
+
${ux.colorize('bold', ux.colorize('green', '✅ SUCCESS!'))} ${ux.colorize('bold', `Workflow "${workflowName}" created`)}
|
|
271
|
+
|
|
272
|
+
${divider}
|
|
273
|
+
|
|
274
|
+
${createSectionHeader('WORKFLOW DETAILS', '📁')}
|
|
275
|
+
|
|
276
|
+
${bulletPoint} ${ux.colorize('white', 'Name:')} ${formatPath(workflowName)}
|
|
277
|
+
${bulletPoint} ${ux.colorize('white', 'Location:')} ${formatPath(targetDir)}
|
|
278
|
+
|
|
279
|
+
${divider}
|
|
280
|
+
|
|
281
|
+
${createSectionHeader('FILES CREATED', '📄')}
|
|
282
|
+
|
|
283
|
+
${formattedFiles}
|
|
284
|
+
|
|
285
|
+
${divider}
|
|
286
|
+
|
|
287
|
+
${createSectionHeader('NEXT STEPS', '🚀')}
|
|
288
|
+
|
|
289
|
+
${formattedSteps}
|
|
290
|
+
|
|
291
|
+
${divider}
|
|
292
|
+
|
|
293
|
+
${ux.colorize('dim', '💡 Tip: Check the README.md in your workflow directory for detailed documentation.')}
|
|
294
|
+
|
|
295
|
+
${ux.colorize('green', ux.colorize('bold', 'Happy building! 🛠️'))}
|
|
296
|
+
`;
|
|
297
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TraceData } from '#types/trace.js';
|
|
2
|
+
export type { TraceData };
|
|
3
|
+
/**
|
|
4
|
+
* Find trace file from workflow metadata
|
|
5
|
+
*/
|
|
6
|
+
export declare function findTraceFile(workflowId: string): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Read and parse trace file
|
|
9
|
+
*/
|
|
10
|
+
export declare function readTraceFile(path: string): Promise<TraceData>;
|
|
11
|
+
/**
|
|
12
|
+
* Get trace data from workflow ID
|
|
13
|
+
*/
|
|
14
|
+
export declare function getTrace(workflowId: string): Promise<TraceData>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { getWorkflowIdResult } from '#api/generated/api.js';
|
|
3
|
+
import { getErrorCode } from '#utils/error_utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Check if a file exists with detailed error information
|
|
6
|
+
*/
|
|
7
|
+
async function fileExists(path) {
|
|
8
|
+
try {
|
|
9
|
+
await stat(path);
|
|
10
|
+
return { exists: true };
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const code = getErrorCode(error);
|
|
14
|
+
if (code === 'ENOENT') {
|
|
15
|
+
return { exists: false };
|
|
16
|
+
}
|
|
17
|
+
if (code === 'EACCES') {
|
|
18
|
+
return { exists: false, error: `Permission denied: ${path}` };
|
|
19
|
+
}
|
|
20
|
+
return { exists: false, error: `Cannot access file: ${path}` };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find trace file from workflow metadata
|
|
25
|
+
*/
|
|
26
|
+
export async function findTraceFile(workflowId) {
|
|
27
|
+
const response = await getWorkflowIdResult(workflowId);
|
|
28
|
+
// Check if we got a successful response
|
|
29
|
+
if (response.status !== 200) {
|
|
30
|
+
throw new Error(`Failed to get workflow result for ${workflowId}`);
|
|
31
|
+
}
|
|
32
|
+
const tracePath = response.data.trace?.destinations?.local;
|
|
33
|
+
if (!tracePath) {
|
|
34
|
+
throw new Error(`No trace file path found for workflow ${workflowId}`);
|
|
35
|
+
}
|
|
36
|
+
const fileCheck = await fileExists(tracePath);
|
|
37
|
+
if (!fileCheck.exists) {
|
|
38
|
+
const errorDetail = fileCheck.error || `Trace file not found at path: ${tracePath}`;
|
|
39
|
+
throw new Error(errorDetail);
|
|
40
|
+
}
|
|
41
|
+
return tracePath;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read and parse trace file
|
|
45
|
+
*/
|
|
46
|
+
export async function readTraceFile(path) {
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(path, 'utf-8');
|
|
49
|
+
return JSON.parse(content);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
53
|
+
throw new Error(`Trace file not found at path: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
if (error instanceof SyntaxError) {
|
|
56
|
+
throw new Error(`Invalid JSON in trace file: ${path}`);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get trace data from workflow ID
|
|
63
|
+
*/
|
|
64
|
+
export async function getTrace(workflowId) {
|
|
65
|
+
const tracePath = await findTraceFile(workflowId);
|
|
66
|
+
return readTraceFile(tracePath);
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { findTraceFile, readTraceFile } from './trace_reader.js';
|
|
3
|
+
// Mock file system operations
|
|
4
|
+
vi.mock('node:fs/promises', () => ({
|
|
5
|
+
readFile: vi.fn(),
|
|
6
|
+
stat: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
// Mock API
|
|
9
|
+
vi.mock('../api/generated/api.js', () => ({
|
|
10
|
+
getWorkflowIdResult: vi.fn()
|
|
11
|
+
}));
|
|
12
|
+
describe('TraceReader', () => {
|
|
13
|
+
const getMocks = async () => {
|
|
14
|
+
const fsModule = await import('node:fs/promises');
|
|
15
|
+
const apiModule = await import('../api/generated/api.js');
|
|
16
|
+
return {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
mockReadFile: fsModule.readFile,
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
mockStat: fsModule.stat,
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
mockGetWorkflowIdResult: apiModule.getWorkflowIdResult
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
describe('findTraceFile', () => {
|
|
29
|
+
it('should find trace file from workflow output metadata', async () => {
|
|
30
|
+
const { mockGetWorkflowIdResult, mockStat } = await getMocks();
|
|
31
|
+
const workflowId = 'test-workflow-123';
|
|
32
|
+
const expectedPath = '/app/logs/runs/test/2024-01-01_test-workflow-123.json';
|
|
33
|
+
mockGetWorkflowIdResult.mockResolvedValue({
|
|
34
|
+
status: 200,
|
|
35
|
+
data: {
|
|
36
|
+
workflowId,
|
|
37
|
+
output: { result: 'test result' },
|
|
38
|
+
trace: {
|
|
39
|
+
destinations: {
|
|
40
|
+
local: expectedPath,
|
|
41
|
+
remote: null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
mockStat.mockResolvedValue({ isFile: () => true });
|
|
47
|
+
const result = await findTraceFile(workflowId);
|
|
48
|
+
expect(result).toBe(expectedPath);
|
|
49
|
+
expect(mockGetWorkflowIdResult).toHaveBeenCalledWith(workflowId);
|
|
50
|
+
expect(mockStat).toHaveBeenCalledWith(expectedPath);
|
|
51
|
+
});
|
|
52
|
+
it('should throw error when no trace path in metadata', async () => {
|
|
53
|
+
const { mockGetWorkflowIdResult } = await getMocks();
|
|
54
|
+
const workflowId = 'test-workflow-456';
|
|
55
|
+
mockGetWorkflowIdResult.mockResolvedValue({
|
|
56
|
+
status: 200,
|
|
57
|
+
data: {
|
|
58
|
+
workflowId,
|
|
59
|
+
output: { result: 'test result' },
|
|
60
|
+
trace: {
|
|
61
|
+
destinations: {
|
|
62
|
+
local: null,
|
|
63
|
+
remote: null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
await expect(findTraceFile(workflowId))
|
|
69
|
+
.rejects.toThrow(`No trace file path found for workflow ${workflowId}`);
|
|
70
|
+
});
|
|
71
|
+
it('should throw error when trace file not on disk', async () => {
|
|
72
|
+
const { mockGetWorkflowIdResult, mockStat } = await getMocks();
|
|
73
|
+
const workflowId = 'test-workflow-789';
|
|
74
|
+
const expectedPath = '/app/logs/runs/test/2024-01-01_test-workflow-789.json';
|
|
75
|
+
mockGetWorkflowIdResult.mockResolvedValue({
|
|
76
|
+
status: 200,
|
|
77
|
+
data: {
|
|
78
|
+
workflowId,
|
|
79
|
+
output: { result: 'test result' },
|
|
80
|
+
trace: {
|
|
81
|
+
destinations: {
|
|
82
|
+
local: expectedPath,
|
|
83
|
+
remote: null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const enoentError = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
|
|
89
|
+
mockStat.mockRejectedValue(enoentError);
|
|
90
|
+
await expect(findTraceFile(workflowId))
|
|
91
|
+
.rejects.toThrow(`Trace file not found at path: ${expectedPath}`);
|
|
92
|
+
});
|
|
93
|
+
it('should throw error when API call fails', async () => {
|
|
94
|
+
const { mockGetWorkflowIdResult } = await getMocks();
|
|
95
|
+
const workflowId = 'non-existent';
|
|
96
|
+
mockGetWorkflowIdResult.mockRejectedValue(new Error('Workflow not found'));
|
|
97
|
+
await expect(findTraceFile(workflowId))
|
|
98
|
+
.rejects.toThrow('Workflow not found');
|
|
99
|
+
});
|
|
100
|
+
it('should handle missing trace property gracefully', async () => {
|
|
101
|
+
const { mockGetWorkflowIdResult } = await getMocks();
|
|
102
|
+
const workflowId = 'test-workflow-no-trace';
|
|
103
|
+
mockGetWorkflowIdResult.mockResolvedValue({
|
|
104
|
+
status: 200,
|
|
105
|
+
data: {
|
|
106
|
+
workflowId,
|
|
107
|
+
output: { result: 'test result' }
|
|
108
|
+
// No trace property at all
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
await expect(findTraceFile(workflowId))
|
|
112
|
+
.rejects.toThrow(`No trace file path found for workflow ${workflowId}`);
|
|
113
|
+
});
|
|
114
|
+
it('should throw error when workflow not found (404)', async () => {
|
|
115
|
+
const { mockGetWorkflowIdResult } = await getMocks();
|
|
116
|
+
const workflowId = 'non-existent-workflow';
|
|
117
|
+
mockGetWorkflowIdResult.mockResolvedValue({
|
|
118
|
+
status: 404,
|
|
119
|
+
data: void 0
|
|
120
|
+
});
|
|
121
|
+
await expect(findTraceFile(workflowId))
|
|
122
|
+
.rejects.toThrow(`Failed to get workflow result for ${workflowId}`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('readTraceFile', () => {
|
|
126
|
+
it('should read and parse JSON file successfully', async () => {
|
|
127
|
+
const { mockReadFile } = await getMocks();
|
|
128
|
+
const path = '/logs/test.json';
|
|
129
|
+
const traceData = {
|
|
130
|
+
root: { workflowName: 'test' },
|
|
131
|
+
events: []
|
|
132
|
+
};
|
|
133
|
+
mockReadFile.mockResolvedValue(JSON.stringify(traceData));
|
|
134
|
+
const result = await readTraceFile(path);
|
|
135
|
+
expect(result).toEqual(traceData);
|
|
136
|
+
expect(mockReadFile).toHaveBeenCalledWith(path, 'utf-8');
|
|
137
|
+
});
|
|
138
|
+
it('should throw error for non-existent file', async () => {
|
|
139
|
+
const { mockReadFile } = await getMocks();
|
|
140
|
+
const path = '/logs/missing.json';
|
|
141
|
+
const error = new Error('ENOENT');
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
+
error.code = 'ENOENT';
|
|
144
|
+
mockReadFile.mockRejectedValue(error);
|
|
145
|
+
await expect(readTraceFile(path))
|
|
146
|
+
.rejects.toThrow(`Trace file not found at path: ${path}`);
|
|
147
|
+
});
|
|
148
|
+
it('should throw error for invalid JSON', async () => {
|
|
149
|
+
const { mockReadFile } = await getMocks();
|
|
150
|
+
const path = '/logs/invalid.json';
|
|
151
|
+
mockReadFile.mockResolvedValue('invalid json {');
|
|
152
|
+
await expect(readTraceFile(path))
|
|
153
|
+
.rejects.toThrow(`Invalid JSON in trace file: ${path}`);
|
|
154
|
+
});
|
|
155
|
+
it('should rethrow other errors', async () => {
|
|
156
|
+
const { mockReadFile } = await getMocks();
|
|
157
|
+
const path = '/logs/test.json';
|
|
158
|
+
const error = new Error('Permission denied');
|
|
159
|
+
mockReadFile.mockRejectedValue(error);
|
|
160
|
+
await expect(readTraceFile(path))
|
|
161
|
+
.rejects.toThrow('Permission denied');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax, init-declarations */
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import { generateWorkflow } from './workflow_generator.js';
|
|
7
|
+
describe('Workflow Generator', () => {
|
|
8
|
+
let tempDir;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'workflow-gen-test-'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
describe('skeleton generation', () => {
|
|
16
|
+
it('should create prompts folder with prompt template', async () => {
|
|
17
|
+
const result = await generateWorkflow({
|
|
18
|
+
name: 'testWorkflow',
|
|
19
|
+
description: 'Test workflow',
|
|
20
|
+
outputDir: tempDir,
|
|
21
|
+
skeleton: true,
|
|
22
|
+
force: false
|
|
23
|
+
});
|
|
24
|
+
const promptPath = path.join(result.targetDir, 'prompts', 'prompt@v1.prompt');
|
|
25
|
+
const promptExists = await fs.access(promptPath).then(() => true).catch(() => false);
|
|
26
|
+
expect(promptExists).toBe(true);
|
|
27
|
+
expect(result.filesCreated).toContain('prompts/prompt@v1.prompt');
|
|
28
|
+
});
|
|
29
|
+
it('should create scenarios folder with test_input.json', async () => {
|
|
30
|
+
const result = await generateWorkflow({
|
|
31
|
+
name: 'testWorkflow',
|
|
32
|
+
description: 'Test workflow',
|
|
33
|
+
outputDir: tempDir,
|
|
34
|
+
skeleton: true,
|
|
35
|
+
force: false
|
|
36
|
+
});
|
|
37
|
+
const scenarioPath = path.join(result.targetDir, 'scenarios', 'test_input.json');
|
|
38
|
+
const scenarioExists = await fs.access(scenarioPath).then(() => true).catch(() => false);
|
|
39
|
+
expect(scenarioExists).toBe(true);
|
|
40
|
+
expect(result.filesCreated).toContain('scenarios/test_input.json');
|
|
41
|
+
});
|
|
42
|
+
it('should create valid JSON in scenario file', async () => {
|
|
43
|
+
const result = await generateWorkflow({
|
|
44
|
+
name: 'testWorkflow',
|
|
45
|
+
description: 'Test workflow',
|
|
46
|
+
outputDir: tempDir,
|
|
47
|
+
skeleton: true,
|
|
48
|
+
force: false
|
|
49
|
+
});
|
|
50
|
+
const scenarioPath = path.join(result.targetDir, 'scenarios', 'test_input.json');
|
|
51
|
+
const content = await fs.readFile(scenarioPath, 'utf-8');
|
|
52
|
+
const parsed = JSON.parse(content);
|
|
53
|
+
expect(parsed).toHaveProperty('prompt');
|
|
54
|
+
expect(parsed).toHaveProperty('data');
|
|
55
|
+
});
|
|
56
|
+
it('should create all expected skeleton files', async () => {
|
|
57
|
+
const result = await generateWorkflow({
|
|
58
|
+
name: 'testWorkflow',
|
|
59
|
+
description: 'Test workflow',
|
|
60
|
+
outputDir: tempDir,
|
|
61
|
+
skeleton: true,
|
|
62
|
+
force: false
|
|
63
|
+
});
|
|
64
|
+
const expectedFiles = [
|
|
65
|
+
'workflow.ts',
|
|
66
|
+
'steps.ts',
|
|
67
|
+
'README.md',
|
|
68
|
+
'.env',
|
|
69
|
+
'prompts/prompt@v1.prompt',
|
|
70
|
+
'scenarios/test_input.json'
|
|
71
|
+
];
|
|
72
|
+
for (const file of expectedFiles) {
|
|
73
|
+
expect(result.filesCreated).toContain(file);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|