@output.ai/cli 0.3.0-dev.pr156.05c9aa2 → 0.3.0-dev.pr156.696d4dd
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.
|
@@ -42,11 +42,27 @@ export type PostWorkflowRunBody = {
|
|
|
42
42
|
/** The name of the task queue to send the workflow to */
|
|
43
43
|
taskQueue?: string;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* File destinations for trace data
|
|
47
|
+
*/
|
|
48
|
+
export type PostWorkflowRun200TraceDestinations = {
|
|
49
|
+
/**
|
|
50
|
+
* Absolute path to local trace file, or null if not saved locally
|
|
51
|
+
* @nullable
|
|
52
|
+
*/
|
|
53
|
+
local: string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Remote trace location (e.g., S3 URI), or null if not saved remotely
|
|
56
|
+
* @nullable
|
|
57
|
+
*/
|
|
58
|
+
remote: string | null;
|
|
59
|
+
};
|
|
45
60
|
/**
|
|
46
61
|
* An object with information about the trace generated by the execution
|
|
47
62
|
*/
|
|
48
63
|
export type PostWorkflowRun200Trace = {
|
|
49
|
-
|
|
64
|
+
/** File destinations for trace data */
|
|
65
|
+
destinations?: PostWorkflowRun200TraceDestinations;
|
|
50
66
|
};
|
|
51
67
|
export type PostWorkflowRun200 = {
|
|
52
68
|
/** The workflow execution id */
|
|
@@ -94,11 +110,27 @@ export type GetWorkflowIdStatus200 = {
|
|
|
94
110
|
/** An epoch timestamp representing when the workflow ended */
|
|
95
111
|
completedAt?: number;
|
|
96
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* File destinations for trace data
|
|
115
|
+
*/
|
|
116
|
+
export type GetWorkflowIdOutput200TraceDestinations = {
|
|
117
|
+
/**
|
|
118
|
+
* Absolute path to local trace file, or null if not saved locally
|
|
119
|
+
* @nullable
|
|
120
|
+
*/
|
|
121
|
+
local: string | null;
|
|
122
|
+
/**
|
|
123
|
+
* Remote trace location (e.g., S3 URI), or null if not saved remotely
|
|
124
|
+
* @nullable
|
|
125
|
+
*/
|
|
126
|
+
remote: string | null;
|
|
127
|
+
};
|
|
97
128
|
/**
|
|
98
129
|
* An object with information about the trace generated by the execution
|
|
99
130
|
*/
|
|
100
131
|
export type GetWorkflowIdOutput200Trace = {
|
|
101
|
-
|
|
132
|
+
/** File destinations for trace data */
|
|
133
|
+
destinations?: GetWorkflowIdOutput200TraceDestinations;
|
|
102
134
|
};
|
|
103
135
|
export type GetWorkflowIdOutput200 = {
|
|
104
136
|
/** The workflow execution id */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
3
3
|
import { traceFormatter } from '#utils/trace_formatter.js';
|
|
4
|
-
import {
|
|
4
|
+
import { findTraceFile, readTraceFile } from '#services/trace_reader.js';
|
|
5
5
|
export default class WorkflowDebug extends Command {
|
|
6
6
|
static description = 'Get and display workflow execution trace for debugging';
|
|
7
7
|
static examples = [
|
|
@@ -39,9 +39,8 @@ export default class WorkflowDebug extends Command {
|
|
|
39
39
|
this.displayTextTrace(traceData);
|
|
40
40
|
}
|
|
41
41
|
async getTrace(workflowId) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
return reader.readTraceFile(tracePath);
|
|
42
|
+
const tracePath = await findTraceFile(workflowId);
|
|
43
|
+
return readTraceFile(tracePath);
|
|
45
44
|
}
|
|
46
45
|
outputJson(data) {
|
|
47
46
|
this.log(JSON.stringify(data, null, 2));
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
readTraceFile(path: string): Promise<any>;
|
|
10
|
-
/**
|
|
11
|
-
* Check if a file exists
|
|
12
|
-
*/
|
|
13
|
-
private fileExists;
|
|
14
|
-
}
|
|
15
|
-
export declare const traceReader: TraceReader;
|
|
1
|
+
/**
|
|
2
|
+
* Find trace file from workflow metadata
|
|
3
|
+
*/
|
|
4
|
+
export declare function findTraceFile(workflowId: string): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Read and parse trace file
|
|
7
|
+
*/
|
|
8
|
+
export declare function readTraceFile(path: string): Promise<any>;
|
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const tracePath = status.data?.trace?.destinations?.local;
|
|
11
|
-
if (!tracePath) {
|
|
12
|
-
throw new Error(`No trace file path found for workflow ${workflowId}`);
|
|
13
|
-
}
|
|
14
|
-
if (!await this.fileExists(tracePath)) {
|
|
15
|
-
throw new Error(`Trace file not found at path: ${tracePath}`);
|
|
16
|
-
}
|
|
17
|
-
return tracePath;
|
|
2
|
+
import { getWorkflowIdOutput } from '#api/generated/api.js';
|
|
3
|
+
/**
|
|
4
|
+
* Check if a file exists
|
|
5
|
+
*/
|
|
6
|
+
async function fileExists(path) {
|
|
7
|
+
try {
|
|
8
|
+
await stat(path);
|
|
9
|
+
return true;
|
|
18
10
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Find trace file from workflow metadata
|
|
17
|
+
*/
|
|
18
|
+
export async function findTraceFile(workflowId) {
|
|
19
|
+
const response = await getWorkflowIdOutput(workflowId);
|
|
20
|
+
// Check if we got a successful response
|
|
21
|
+
if (response.status !== 200) {
|
|
22
|
+
throw new Error(`Failed to get workflow output for ${workflowId}`);
|
|
23
|
+
}
|
|
24
|
+
const tracePath = response.data.trace?.destinations?.local;
|
|
25
|
+
if (!tracePath) {
|
|
26
|
+
throw new Error(`No trace file path found for workflow ${workflowId}`);
|
|
27
|
+
}
|
|
28
|
+
if (!await fileExists(tracePath)) {
|
|
29
|
+
throw new Error(`Trace file not found at path: ${tracePath}`);
|
|
30
|
+
}
|
|
31
|
+
return tracePath;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read and parse trace file
|
|
35
|
+
*/
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
export async function readTraceFile(path) {
|
|
38
|
+
try {
|
|
39
|
+
const content = await readFile(path, 'utf-8');
|
|
40
|
+
return JSON.parse(content);
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
async fileExists(path) {
|
|
42
|
-
try {
|
|
43
|
-
await stat(path);
|
|
44
|
-
return true;
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
44
|
+
throw new Error(`Trace file not found at path: ${path}`);
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (error instanceof SyntaxError) {
|
|
47
|
+
throw new Error(`Invalid JSON in trace file: ${path}`);
|
|
48
48
|
}
|
|
49
|
+
throw error;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
|
-
// Export singleton instance for convenience
|
|
52
|
-
export const traceReader = new TraceReader();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { findTraceFile, readTraceFile } from './trace_reader.js';
|
|
3
3
|
// Mock file system operations
|
|
4
4
|
vi.mock('node:fs/promises', () => ({
|
|
5
5
|
readFile: vi.fn(),
|
|
@@ -7,7 +7,7 @@ vi.mock('node:fs/promises', () => ({
|
|
|
7
7
|
}));
|
|
8
8
|
// Mock API
|
|
9
9
|
vi.mock('../api/generated/api.js', () => ({
|
|
10
|
-
|
|
10
|
+
getWorkflowIdOutput: vi.fn()
|
|
11
11
|
}));
|
|
12
12
|
describe('TraceReader', () => {
|
|
13
13
|
const getMocks = async () => {
|
|
@@ -19,75 +19,110 @@ describe('TraceReader', () => {
|
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
20
|
mockStat: fsModule.stat,
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
-
|
|
22
|
+
mockGetWorkflowIdOutput: apiModule.getWorkflowIdOutput
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
afterEach(() => {
|
|
26
26
|
vi.clearAllMocks();
|
|
27
27
|
});
|
|
28
28
|
describe('findTraceFile', () => {
|
|
29
|
-
it('should find trace file from workflow metadata', async () => {
|
|
30
|
-
const
|
|
31
|
-
const { mockGetWorkflowIdStatus, mockStat } = await getMocks();
|
|
29
|
+
it('should find trace file from workflow output metadata', async () => {
|
|
30
|
+
const { mockGetWorkflowIdOutput, mockStat } = await getMocks();
|
|
32
31
|
const workflowId = 'test-workflow-123';
|
|
33
32
|
const expectedPath = '/app/logs/runs/test/2024-01-01_test-workflow-123.json';
|
|
34
|
-
|
|
33
|
+
mockGetWorkflowIdOutput.mockResolvedValue({
|
|
34
|
+
status: 200,
|
|
35
35
|
data: {
|
|
36
|
+
workflowId,
|
|
37
|
+
output: { result: 'test result' },
|
|
36
38
|
trace: {
|
|
37
39
|
destinations: {
|
|
38
|
-
local: expectedPath
|
|
40
|
+
local: expectedPath,
|
|
41
|
+
remote: null
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
});
|
|
43
46
|
mockStat.mockResolvedValue({ isFile: () => true });
|
|
44
|
-
const result = await
|
|
47
|
+
const result = await findTraceFile(workflowId);
|
|
45
48
|
expect(result).toBe(expectedPath);
|
|
46
|
-
expect(
|
|
49
|
+
expect(mockGetWorkflowIdOutput).toHaveBeenCalledWith(workflowId);
|
|
47
50
|
expect(mockStat).toHaveBeenCalledWith(expectedPath);
|
|
48
51
|
});
|
|
49
52
|
it('should throw error when no trace path in metadata', async () => {
|
|
50
|
-
const
|
|
51
|
-
const { mockGetWorkflowIdStatus } = await getMocks();
|
|
53
|
+
const { mockGetWorkflowIdOutput } = await getMocks();
|
|
52
54
|
const workflowId = 'test-workflow-456';
|
|
53
|
-
|
|
55
|
+
mockGetWorkflowIdOutput.mockResolvedValue({
|
|
56
|
+
status: 200,
|
|
54
57
|
data: {
|
|
55
|
-
|
|
58
|
+
workflowId,
|
|
59
|
+
output: { result: 'test result' },
|
|
60
|
+
trace: {
|
|
61
|
+
destinations: {
|
|
62
|
+
local: null,
|
|
63
|
+
remote: null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
});
|
|
58
|
-
await expect(
|
|
68
|
+
await expect(findTraceFile(workflowId))
|
|
59
69
|
.rejects.toThrow(`No trace file path found for workflow ${workflowId}`);
|
|
60
70
|
});
|
|
61
71
|
it('should throw error when trace file not on disk', async () => {
|
|
62
|
-
const
|
|
63
|
-
const { mockGetWorkflowIdStatus, mockStat } = await getMocks();
|
|
72
|
+
const { mockGetWorkflowIdOutput, mockStat } = await getMocks();
|
|
64
73
|
const workflowId = 'test-workflow-789';
|
|
65
74
|
const expectedPath = '/app/logs/runs/test/2024-01-01_test-workflow-789.json';
|
|
66
|
-
|
|
75
|
+
mockGetWorkflowIdOutput.mockResolvedValue({
|
|
76
|
+
status: 200,
|
|
67
77
|
data: {
|
|
78
|
+
workflowId,
|
|
79
|
+
output: { result: 'test result' },
|
|
68
80
|
trace: {
|
|
69
81
|
destinations: {
|
|
70
|
-
local: expectedPath
|
|
82
|
+
local: expectedPath,
|
|
83
|
+
remote: null
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
});
|
|
75
88
|
mockStat.mockRejectedValue(new Error('ENOENT'));
|
|
76
|
-
await expect(
|
|
89
|
+
await expect(findTraceFile(workflowId))
|
|
77
90
|
.rejects.toThrow(`Trace file not found at path: ${expectedPath}`);
|
|
78
91
|
});
|
|
79
92
|
it('should throw error when API call fails', async () => {
|
|
80
|
-
const
|
|
81
|
-
const { mockGetWorkflowIdStatus } = await getMocks();
|
|
93
|
+
const { mockGetWorkflowIdOutput } = await getMocks();
|
|
82
94
|
const workflowId = 'non-existent';
|
|
83
|
-
|
|
84
|
-
await expect(
|
|
95
|
+
mockGetWorkflowIdOutput.mockRejectedValue(new Error('Workflow not found'));
|
|
96
|
+
await expect(findTraceFile(workflowId))
|
|
85
97
|
.rejects.toThrow('Workflow not found');
|
|
86
98
|
});
|
|
99
|
+
it('should handle missing trace property gracefully', async () => {
|
|
100
|
+
const { mockGetWorkflowIdOutput } = await getMocks();
|
|
101
|
+
const workflowId = 'test-workflow-no-trace';
|
|
102
|
+
mockGetWorkflowIdOutput.mockResolvedValue({
|
|
103
|
+
status: 200,
|
|
104
|
+
data: {
|
|
105
|
+
workflowId,
|
|
106
|
+
output: { result: 'test result' }
|
|
107
|
+
// No trace property at all
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
await expect(findTraceFile(workflowId))
|
|
111
|
+
.rejects.toThrow(`No trace file path found for workflow ${workflowId}`);
|
|
112
|
+
});
|
|
113
|
+
it('should throw error when workflow not found (404)', async () => {
|
|
114
|
+
const { mockGetWorkflowIdOutput } = await getMocks();
|
|
115
|
+
const workflowId = 'non-existent-workflow';
|
|
116
|
+
mockGetWorkflowIdOutput.mockResolvedValue({
|
|
117
|
+
status: 404,
|
|
118
|
+
data: void 0
|
|
119
|
+
});
|
|
120
|
+
await expect(findTraceFile(workflowId))
|
|
121
|
+
.rejects.toThrow(`Failed to get workflow output for ${workflowId}`);
|
|
122
|
+
});
|
|
87
123
|
});
|
|
88
124
|
describe('readTraceFile', () => {
|
|
89
125
|
it('should read and parse JSON file successfully', async () => {
|
|
90
|
-
const traceReader = new TraceReader();
|
|
91
126
|
const { mockReadFile } = await getMocks();
|
|
92
127
|
const path = '/logs/test.json';
|
|
93
128
|
const traceData = {
|
|
@@ -95,36 +130,33 @@ describe('TraceReader', () => {
|
|
|
95
130
|
events: []
|
|
96
131
|
};
|
|
97
132
|
mockReadFile.mockResolvedValue(JSON.stringify(traceData));
|
|
98
|
-
const result = await
|
|
133
|
+
const result = await readTraceFile(path);
|
|
99
134
|
expect(result).toEqual(traceData);
|
|
100
135
|
expect(mockReadFile).toHaveBeenCalledWith(path, 'utf-8');
|
|
101
136
|
});
|
|
102
137
|
it('should throw error for non-existent file', async () => {
|
|
103
|
-
const traceReader = new TraceReader();
|
|
104
138
|
const { mockReadFile } = await getMocks();
|
|
105
139
|
const path = '/logs/missing.json';
|
|
106
140
|
const error = new Error('ENOENT');
|
|
107
141
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
142
|
error.code = 'ENOENT';
|
|
109
143
|
mockReadFile.mockRejectedValue(error);
|
|
110
|
-
await expect(
|
|
144
|
+
await expect(readTraceFile(path))
|
|
111
145
|
.rejects.toThrow(`Trace file not found at path: ${path}`);
|
|
112
146
|
});
|
|
113
147
|
it('should throw error for invalid JSON', async () => {
|
|
114
|
-
const traceReader = new TraceReader();
|
|
115
148
|
const { mockReadFile } = await getMocks();
|
|
116
149
|
const path = '/logs/invalid.json';
|
|
117
150
|
mockReadFile.mockResolvedValue('invalid json {');
|
|
118
|
-
await expect(
|
|
151
|
+
await expect(readTraceFile(path))
|
|
119
152
|
.rejects.toThrow(`Invalid JSON in trace file: ${path}`);
|
|
120
153
|
});
|
|
121
154
|
it('should rethrow other errors', async () => {
|
|
122
|
-
const traceReader = new TraceReader();
|
|
123
155
|
const { mockReadFile } = await getMocks();
|
|
124
156
|
const path = '/logs/test.json';
|
|
125
157
|
const error = new Error('Permission denied');
|
|
126
158
|
mockReadFile.mockRejectedValue(error);
|
|
127
|
-
await expect(
|
|
159
|
+
await expect(readTraceFile(path))
|
|
128
160
|
.rejects.toThrow('Permission denied');
|
|
129
161
|
});
|
|
130
162
|
});
|