@positronic/core 0.0.1
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/.swcrc +31 -0
- package/dist/src/adapters/types.js +16 -0
- package/dist/src/clients/types.js +1 -0
- package/dist/src/dsl/constants.js +15 -0
- package/dist/src/dsl/extensions.js +19 -0
- package/dist/src/dsl/json-patch.js +30 -0
- package/dist/src/dsl/types.js +1 -0
- package/dist/src/dsl/workflow-runner.js +93 -0
- package/dist/src/dsl/workflow.js +308 -0
- package/dist/src/file-stores/local-file-store.js +12 -0
- package/dist/src/file-stores/types.js +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/utils/temp-files.js +27 -0
- package/dist/types/adapters/types.d.ts +10 -0
- package/dist/types/adapters/types.d.ts.map +1 -0
- package/dist/types/clients/types.d.ts +10 -0
- package/dist/types/clients/types.d.ts.map +1 -0
- package/dist/types/dsl/constants.d.ts +16 -0
- package/dist/types/dsl/constants.d.ts.map +1 -0
- package/dist/types/dsl/extensions.d.ts +18 -0
- package/dist/types/dsl/extensions.d.ts.map +1 -0
- package/dist/types/dsl/json-patch.d.ts +11 -0
- package/dist/types/dsl/json-patch.d.ts.map +1 -0
- package/dist/types/dsl/types.d.ts +14 -0
- package/dist/types/dsl/types.d.ts.map +1 -0
- package/dist/types/dsl/workflow-runner.d.ts +28 -0
- package/dist/types/dsl/workflow-runner.d.ts.map +1 -0
- package/dist/types/dsl/workflow.d.ts +118 -0
- package/dist/types/dsl/workflow.d.ts.map +1 -0
- package/dist/types/file-stores/local-file-store.d.ts +7 -0
- package/dist/types/file-stores/local-file-store.d.ts.map +1 -0
- package/dist/types/file-stores/types.d.ts +4 -0
- package/dist/types/file-stores/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/utils/temp-files.d.ts +12 -0
- package/dist/types/utils/temp-files.d.ts.map +1 -0
- package/package.json +21 -0
- package/src/adapters/types.ts +24 -0
- package/src/clients/types.ts +14 -0
- package/src/dsl/constants.ts +16 -0
- package/src/dsl/extensions.ts +58 -0
- package/src/dsl/json-patch.ts +27 -0
- package/src/dsl/types.ts +13 -0
- package/src/dsl/workflow-runner.test.ts +203 -0
- package/src/dsl/workflow-runner.ts +146 -0
- package/src/dsl/workflow.test.ts +1435 -0
- package/src/dsl/workflow.ts +554 -0
- package/src/file-stores/local-file-store.ts +11 -0
- package/src/file-stores/types.ts +3 -0
- package/src/index.ts +22 -0
- package/src/utils/temp-files.ts +46 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,1435 @@
|
|
|
1
|
+
import { WORKFLOW_EVENTS, STATUS } from './constants';
|
|
2
|
+
import { applyPatches} from './json-patch';
|
|
3
|
+
import { State } from './types';
|
|
4
|
+
import { workflow, type WorkflowEvent, type WorkflowErrorEvent} from './workflow';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { nextStep } from '../../../../test-utils';
|
|
7
|
+
import { FileStore } from '../file-stores/types';
|
|
8
|
+
|
|
9
|
+
class TestFileStore implements FileStore {
|
|
10
|
+
private files: Map<string, string> = new Map();
|
|
11
|
+
|
|
12
|
+
setFile(path: string, content: string) {
|
|
13
|
+
this.files.set(path, content);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async readFile(path: string): Promise<string> {
|
|
17
|
+
const content = this.files.get(path);
|
|
18
|
+
if (content === undefined) {
|
|
19
|
+
throw new Error(`File not found: ${path}`);
|
|
20
|
+
}
|
|
21
|
+
return content;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const testFileStore = new TestFileStore();
|
|
26
|
+
|
|
27
|
+
type AssertEquals<T, U> =
|
|
28
|
+
0 extends (1 & T) ? false : // fails if T is any
|
|
29
|
+
0 extends (1 & U) ? false : // fails if U is any
|
|
30
|
+
[T] extends [U] ? [U] extends [T] ? true : false : false;
|
|
31
|
+
|
|
32
|
+
// Mock PromptClient for testing
|
|
33
|
+
const mockClient = {
|
|
34
|
+
execute: jest.fn()
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('workflow creation', () => {
|
|
38
|
+
it('should create a workflow with steps and run through them', async () => {
|
|
39
|
+
const testWorkflow = workflow('test workflow')
|
|
40
|
+
.step(
|
|
41
|
+
"First step",
|
|
42
|
+
() => {
|
|
43
|
+
return { count: 1 };
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
.step(
|
|
47
|
+
"Second step",
|
|
48
|
+
({ state }) => ({ ...state, doubled: state.count * 2 })
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
|
|
52
|
+
|
|
53
|
+
// Check start event
|
|
54
|
+
const startResult = await workflowRun.next();
|
|
55
|
+
expect(startResult.value).toEqual(expect.objectContaining({
|
|
56
|
+
type: WORKFLOW_EVENTS.START,
|
|
57
|
+
status: STATUS.RUNNING,
|
|
58
|
+
workflowTitle: 'test workflow',
|
|
59
|
+
workflowDescription: undefined
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Skip initial step status event
|
|
63
|
+
await nextStep(workflowRun);
|
|
64
|
+
|
|
65
|
+
// Check first step start
|
|
66
|
+
const firstStepStartResult = await nextStep(workflowRun);
|
|
67
|
+
expect(firstStepStartResult).toEqual(expect.objectContaining({
|
|
68
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
69
|
+
status: STATUS.RUNNING,
|
|
70
|
+
stepTitle: 'First step',
|
|
71
|
+
stepId: expect.any(String)
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// Check first step completion
|
|
75
|
+
const firstStepResult = await nextStep(workflowRun);
|
|
76
|
+
expect(firstStepResult).toEqual(expect.objectContaining({
|
|
77
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
78
|
+
status: STATUS.RUNNING,
|
|
79
|
+
stepTitle: 'First step',
|
|
80
|
+
stepId: expect.any(String),
|
|
81
|
+
patch: [{
|
|
82
|
+
op: 'add',
|
|
83
|
+
path: '/count',
|
|
84
|
+
value: 1
|
|
85
|
+
}]
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Step Status Event
|
|
89
|
+
await nextStep(workflowRun);
|
|
90
|
+
|
|
91
|
+
// Check second step start
|
|
92
|
+
const secondStepStartResult = await nextStep(workflowRun);
|
|
93
|
+
expect(secondStepStartResult).toEqual(expect.objectContaining({
|
|
94
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
95
|
+
status: STATUS.RUNNING,
|
|
96
|
+
stepTitle: 'Second step',
|
|
97
|
+
stepId: expect.any(String)
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Check second step completion
|
|
101
|
+
const secondStepResult = await nextStep(workflowRun);
|
|
102
|
+
expect(secondStepResult).toEqual(expect.objectContaining({
|
|
103
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
104
|
+
stepTitle: 'Second step',
|
|
105
|
+
stepId: expect.any(String),
|
|
106
|
+
patch: [{
|
|
107
|
+
op: 'add',
|
|
108
|
+
path: '/doubled',
|
|
109
|
+
value: 2
|
|
110
|
+
}]
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
// Step Status Event
|
|
114
|
+
const stepStatusResult = await nextStep(workflowRun);
|
|
115
|
+
expect(stepStatusResult).toEqual(expect.objectContaining({
|
|
116
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
117
|
+
steps: [
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
title: 'First step',
|
|
120
|
+
status: STATUS.COMPLETE,
|
|
121
|
+
id: expect.any(String)
|
|
122
|
+
}),
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
title: 'Second step',
|
|
125
|
+
status: STATUS.COMPLETE,
|
|
126
|
+
id: expect.any(String)
|
|
127
|
+
})
|
|
128
|
+
]
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
// Check workflow completion
|
|
132
|
+
const completeResult = await nextStep(workflowRun);
|
|
133
|
+
expect(completeResult).toEqual(expect.objectContaining({
|
|
134
|
+
type: WORKFLOW_EVENTS.COMPLETE,
|
|
135
|
+
status: STATUS.COMPLETE,
|
|
136
|
+
workflowTitle: 'test workflow',
|
|
137
|
+
workflowDescription: undefined,
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should create a workflow with a name and description when passed an object', async () => {
|
|
142
|
+
const testWorkflow = workflow({
|
|
143
|
+
title: 'my named workflow',
|
|
144
|
+
description: 'some description'
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
|
|
148
|
+
const startResult = await workflowRun.next();
|
|
149
|
+
expect(startResult.value).toEqual(expect.objectContaining({
|
|
150
|
+
type: WORKFLOW_EVENTS.START,
|
|
151
|
+
status: STATUS.RUNNING,
|
|
152
|
+
workflowTitle: 'my named workflow',
|
|
153
|
+
workflowDescription: 'some description',
|
|
154
|
+
options: {}
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should create a workflow with just a name when passed a string', async () => {
|
|
159
|
+
const testWorkflow = workflow('simple workflow');
|
|
160
|
+
const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
|
|
161
|
+
const startResult = await workflowRun.next();
|
|
162
|
+
expect(startResult.value).toEqual(expect.objectContaining({
|
|
163
|
+
type: WORKFLOW_EVENTS.START,
|
|
164
|
+
status: STATUS.RUNNING,
|
|
165
|
+
workflowTitle: 'simple workflow',
|
|
166
|
+
workflowDescription: undefined,
|
|
167
|
+
options: {}
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should allow overriding client per step', async () => {
|
|
172
|
+
const overrideClient = {
|
|
173
|
+
execute: jest.fn().mockResolvedValue({ override: true })
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Make sure that for the default prompt the default client returns a known value.
|
|
177
|
+
mockClient.execute.mockResolvedValueOnce({ override: false });
|
|
178
|
+
|
|
179
|
+
const testWorkflow = workflow('Client Override Test')
|
|
180
|
+
.prompt(
|
|
181
|
+
"Use default client",
|
|
182
|
+
{
|
|
183
|
+
template: () => "prompt1",
|
|
184
|
+
responseModel: {
|
|
185
|
+
schema: z.object({ override: z.boolean() }),
|
|
186
|
+
name: 'overrideResponse'
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
.prompt(
|
|
191
|
+
"Use override client",
|
|
192
|
+
{
|
|
193
|
+
template: () => "prompt2",
|
|
194
|
+
responseModel: {
|
|
195
|
+
schema: z.object({ override: z.boolean() }),
|
|
196
|
+
name: 'overrideResponse'
|
|
197
|
+
},
|
|
198
|
+
client: overrideClient
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Run the workflow and capture all events
|
|
203
|
+
const events = [];
|
|
204
|
+
let finalState = {};
|
|
205
|
+
for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
206
|
+
events.push(event);
|
|
207
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
208
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Final state should include both responses
|
|
213
|
+
expect(finalState).toEqual({
|
|
214
|
+
overrideResponse: { override: true }
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Verify that each client was used correctly based on the supplied prompt configuration.
|
|
218
|
+
expect(mockClient.execute).toHaveBeenCalledWith("prompt1", expect.any(Object));
|
|
219
|
+
expect(overrideClient.execute).toHaveBeenCalledWith("prompt2", expect.any(Object));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('error handling', () => {
|
|
224
|
+
it('should handle errors in actions and maintain correct status', async () => {
|
|
225
|
+
const errorWorkflow = workflow('Error Workflow')
|
|
226
|
+
// Step 1: Normal step
|
|
227
|
+
.step("First step", () => ({
|
|
228
|
+
value: 1
|
|
229
|
+
}))
|
|
230
|
+
// Step 2: Error step
|
|
231
|
+
.step("Error step", () => {
|
|
232
|
+
if (true) {
|
|
233
|
+
throw new Error('Test error');
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
value: 1
|
|
237
|
+
};
|
|
238
|
+
})
|
|
239
|
+
// Step 3: Should never execute
|
|
240
|
+
.step("Never reached", ({ state }) => ({
|
|
241
|
+
value: state.value + 1
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
let errorEvent, finalStepStatusEvent;
|
|
245
|
+
try {
|
|
246
|
+
for await (const event of errorWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
247
|
+
if (event.type === WORKFLOW_EVENTS.ERROR) {
|
|
248
|
+
errorEvent = event;
|
|
249
|
+
}
|
|
250
|
+
if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
|
|
251
|
+
finalStepStatusEvent = event;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// Error is expected to be thrown
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Verify final state
|
|
259
|
+
expect(errorEvent?.status).toBe(STATUS.ERROR);
|
|
260
|
+
expect(errorEvent?.error?.message).toBe('Test error');
|
|
261
|
+
|
|
262
|
+
// Verify steps status
|
|
263
|
+
if (!finalStepStatusEvent?.steps) {
|
|
264
|
+
throw new Error('Steps not found');
|
|
265
|
+
}
|
|
266
|
+
expect(finalStepStatusEvent.steps[0].status).toBe(STATUS.COMPLETE);
|
|
267
|
+
expect(finalStepStatusEvent.steps[1].status).toBe(STATUS.ERROR);
|
|
268
|
+
expect(finalStepStatusEvent.steps[2].status).toBe(STATUS.PENDING);
|
|
269
|
+
|
|
270
|
+
// Verify error event structure
|
|
271
|
+
expect(errorEvent).toEqual(expect.objectContaining({
|
|
272
|
+
type: WORKFLOW_EVENTS.ERROR,
|
|
273
|
+
status: STATUS.ERROR,
|
|
274
|
+
workflowTitle: 'Error Workflow',
|
|
275
|
+
error: expect.objectContaining({
|
|
276
|
+
name: expect.any(String),
|
|
277
|
+
message: expect.any(String)
|
|
278
|
+
}),
|
|
279
|
+
}));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle errors in nested workflows and propagate them up', async () => {
|
|
283
|
+
// Create an inner workflow that will throw an error
|
|
284
|
+
const innerWorkflow = workflow<{}, { inner?: boolean, value?: number }>('Failing Inner Workflow')
|
|
285
|
+
.step(
|
|
286
|
+
"Throw error",
|
|
287
|
+
(): { value: number } => {
|
|
288
|
+
throw new Error('Inner workflow error');
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Create outer workflow that uses the failing inner workflow
|
|
293
|
+
const outerWorkflow = workflow('Outer Workflow')
|
|
294
|
+
.step(
|
|
295
|
+
"First step",
|
|
296
|
+
() => ({ step: "first" })
|
|
297
|
+
)
|
|
298
|
+
.workflow(
|
|
299
|
+
"Run inner workflow",
|
|
300
|
+
innerWorkflow,
|
|
301
|
+
({ state, workflowState }) => ({
|
|
302
|
+
...state,
|
|
303
|
+
step: "second",
|
|
304
|
+
innerResult: workflowState.value
|
|
305
|
+
}),
|
|
306
|
+
() => ({ value: 5 })
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const events: WorkflowEvent<any>[] = [];
|
|
310
|
+
let error: Error | undefined;
|
|
311
|
+
let mainWorkflowId: string | undefined;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
315
|
+
events.push(event);
|
|
316
|
+
if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
|
|
317
|
+
mainWorkflowId = event.workflowRunId;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
error = e as Error;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Verify error was thrown
|
|
325
|
+
expect(error?.message).toBe('Inner workflow error');
|
|
326
|
+
|
|
327
|
+
// Verify event sequence including error
|
|
328
|
+
expect(events).toEqual([
|
|
329
|
+
expect.objectContaining({
|
|
330
|
+
type: WORKFLOW_EVENTS.START,
|
|
331
|
+
workflowTitle: 'Outer Workflow',
|
|
332
|
+
status: STATUS.RUNNING,
|
|
333
|
+
workflowRunId: mainWorkflowId
|
|
334
|
+
}),
|
|
335
|
+
expect.objectContaining({
|
|
336
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
337
|
+
steps: expect.any(Array)
|
|
338
|
+
}),
|
|
339
|
+
expect.objectContaining({
|
|
340
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
341
|
+
status: STATUS.RUNNING,
|
|
342
|
+
stepTitle: 'First step'
|
|
343
|
+
}),
|
|
344
|
+
expect.objectContaining({
|
|
345
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
346
|
+
status: STATUS.RUNNING,
|
|
347
|
+
stepTitle: 'First step'
|
|
348
|
+
}),
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
351
|
+
steps: expect.any(Array)
|
|
352
|
+
}),
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
355
|
+
status: STATUS.RUNNING,
|
|
356
|
+
stepTitle: 'Run inner workflow'
|
|
357
|
+
}),
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
type: WORKFLOW_EVENTS.START,
|
|
360
|
+
workflowTitle: 'Failing Inner Workflow',
|
|
361
|
+
status: STATUS.RUNNING,
|
|
362
|
+
}),
|
|
363
|
+
expect.objectContaining({
|
|
364
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
365
|
+
steps: expect.any(Array)
|
|
366
|
+
}),
|
|
367
|
+
expect.objectContaining({
|
|
368
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
369
|
+
status: STATUS.RUNNING,
|
|
370
|
+
stepTitle: 'Throw error'
|
|
371
|
+
}),
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
type: WORKFLOW_EVENTS.ERROR,
|
|
374
|
+
workflowTitle: 'Failing Inner Workflow',
|
|
375
|
+
status: STATUS.ERROR,
|
|
376
|
+
error: expect.objectContaining({
|
|
377
|
+
name: expect.any(String),
|
|
378
|
+
message: expect.any(String)
|
|
379
|
+
}),
|
|
380
|
+
}),
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
383
|
+
steps: expect.arrayContaining([
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
title: 'Throw error',
|
|
386
|
+
status: STATUS.ERROR
|
|
387
|
+
})
|
|
388
|
+
])
|
|
389
|
+
}),
|
|
390
|
+
expect.objectContaining({
|
|
391
|
+
type: WORKFLOW_EVENTS.ERROR,
|
|
392
|
+
workflowTitle: 'Outer Workflow',
|
|
393
|
+
status: STATUS.ERROR,
|
|
394
|
+
error: expect.objectContaining({
|
|
395
|
+
name: expect.any(String),
|
|
396
|
+
message: expect.any(String)
|
|
397
|
+
})
|
|
398
|
+
}),
|
|
399
|
+
expect.objectContaining({
|
|
400
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
401
|
+
steps: expect.arrayContaining([
|
|
402
|
+
expect.objectContaining({
|
|
403
|
+
title: 'Run inner workflow',
|
|
404
|
+
status: STATUS.ERROR
|
|
405
|
+
})
|
|
406
|
+
])
|
|
407
|
+
})
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
// Find inner and outer error events by workflowRunId
|
|
411
|
+
const innerErrorEvent = events.find(e =>
|
|
412
|
+
e.type === WORKFLOW_EVENTS.ERROR &&
|
|
413
|
+
e.workflowRunId !== mainWorkflowId
|
|
414
|
+
) as WorkflowErrorEvent<any>;
|
|
415
|
+
|
|
416
|
+
const outerErrorEvent = events.find(e =>
|
|
417
|
+
e.type === WORKFLOW_EVENTS.ERROR &&
|
|
418
|
+
e.workflowRunId === mainWorkflowId
|
|
419
|
+
) as WorkflowErrorEvent<any>;
|
|
420
|
+
|
|
421
|
+
expect(innerErrorEvent.error).toEqual(expect.objectContaining({
|
|
422
|
+
message: 'Inner workflow error'
|
|
423
|
+
}));
|
|
424
|
+
expect(outerErrorEvent.error).toEqual(expect.objectContaining({
|
|
425
|
+
message: 'Inner workflow error'
|
|
426
|
+
}));
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('step creation', () => {
|
|
431
|
+
it('should create a step that updates state', async () => {
|
|
432
|
+
const testWorkflow = workflow('Simple Workflow')
|
|
433
|
+
.step("Simple step", ({ state }) => ({
|
|
434
|
+
...state,
|
|
435
|
+
count: 1,
|
|
436
|
+
message: 'Count is now 1'
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
const events = [];
|
|
440
|
+
let finalState = {};
|
|
441
|
+
for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
442
|
+
events.push(event);
|
|
443
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
444
|
+
finalState = applyPatches(finalState, event.patch);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Skip checking events[0] (workflow:start)
|
|
449
|
+
// Skip checking events[1] (step:status)
|
|
450
|
+
|
|
451
|
+
// Verify the step start event
|
|
452
|
+
expect(events[2]).toEqual(expect.objectContaining({
|
|
453
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
454
|
+
status: STATUS.RUNNING,
|
|
455
|
+
stepTitle: 'Simple step',
|
|
456
|
+
stepId: expect.any(String),
|
|
457
|
+
options: {}
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
// Verify the step complete event
|
|
461
|
+
expect(events[3]).toEqual(expect.objectContaining({
|
|
462
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
463
|
+
status: STATUS.RUNNING,
|
|
464
|
+
stepTitle: 'Simple step',
|
|
465
|
+
stepId: expect.any(String),
|
|
466
|
+
patch: [{
|
|
467
|
+
op: 'add',
|
|
468
|
+
path: '/count',
|
|
469
|
+
value: 1
|
|
470
|
+
}, {
|
|
471
|
+
op: 'add',
|
|
472
|
+
path: '/message',
|
|
473
|
+
value: 'Count is now 1'
|
|
474
|
+
}],
|
|
475
|
+
options: {}
|
|
476
|
+
}));
|
|
477
|
+
|
|
478
|
+
expect(events[4]).toEqual(expect.objectContaining({
|
|
479
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
480
|
+
steps: [
|
|
481
|
+
expect.objectContaining({ title: 'Simple step', status: STATUS.COMPLETE, id: expect.any(String) })
|
|
482
|
+
],
|
|
483
|
+
options: {}
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
// Verify the workflow complete event
|
|
487
|
+
expect(events[5]).toEqual(expect.objectContaining({
|
|
488
|
+
type: WORKFLOW_EVENTS.COMPLETE,
|
|
489
|
+
status: STATUS.COMPLETE,
|
|
490
|
+
workflowTitle: 'Simple Workflow',
|
|
491
|
+
options: {}
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
// Verify the final state
|
|
495
|
+
expect(finalState).toEqual({
|
|
496
|
+
count: 1,
|
|
497
|
+
message: 'Count is now 1'
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should maintain immutable results between steps', async () => {
|
|
502
|
+
const testWorkflow = workflow('Immutable Steps Workflow')
|
|
503
|
+
.step("First step", () => ({
|
|
504
|
+
value: 1
|
|
505
|
+
}))
|
|
506
|
+
.step("Second step", ({ state }) => {
|
|
507
|
+
// Attempt to modify previous step's state
|
|
508
|
+
state.value = 99;
|
|
509
|
+
return {
|
|
510
|
+
value: 2
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
let finalState = {};
|
|
515
|
+
const patches = [];
|
|
516
|
+
for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
517
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
518
|
+
patches.push(...event.patch);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Apply all patches to the initial state
|
|
523
|
+
finalState = applyPatches(finalState, patches);
|
|
524
|
+
|
|
525
|
+
// Verify the final state
|
|
526
|
+
expect(finalState).toEqual({ value: 2 });
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe('workflow resumption', () => {
|
|
531
|
+
const mockClient = {
|
|
532
|
+
execute: jest.fn()
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
it('should resume workflow from the correct step when given initialCompletedSteps', async () => {
|
|
536
|
+
const executedSteps: string[] = [];
|
|
537
|
+
const threeStepWorkflow = workflow('Three Step Workflow')
|
|
538
|
+
.step("Step 1", ({ state }) => {
|
|
539
|
+
executedSteps.push("Step 1");
|
|
540
|
+
return { ...state, value: 2 };
|
|
541
|
+
})
|
|
542
|
+
.step("Step 2", ({ state }) => {
|
|
543
|
+
executedSteps.push("Step 2");
|
|
544
|
+
return { ...state, value: state.value + 10 };
|
|
545
|
+
})
|
|
546
|
+
.step("Step 3", ({ state }) => {
|
|
547
|
+
executedSteps.push("Step 3");
|
|
548
|
+
return { ...state, value: state.value * 3 };
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// First run to get the first step completed with initial state
|
|
552
|
+
let initialCompletedSteps;
|
|
553
|
+
const initialState = { initialValue: true };
|
|
554
|
+
let firstStepState: State = initialState;
|
|
555
|
+
|
|
556
|
+
// Run workflow until we get the first step completed
|
|
557
|
+
for await (const event of threeStepWorkflow.run({
|
|
558
|
+
client: mockClient,
|
|
559
|
+
initialState,
|
|
560
|
+
fileStore: testFileStore
|
|
561
|
+
})) {
|
|
562
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
563
|
+
firstStepState = applyPatches(firstStepState, [event.patch]);
|
|
564
|
+
}
|
|
565
|
+
if (event.type === WORKFLOW_EVENTS.STEP_STATUS && event.steps[0].status === STATUS.COMPLETE) {
|
|
566
|
+
initialCompletedSteps = event.steps;
|
|
567
|
+
break; // Stop after first step
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Clear executed steps array
|
|
572
|
+
executedSteps.length = 0;
|
|
573
|
+
|
|
574
|
+
// Resume workflow with first step completed
|
|
575
|
+
let resumedState: State | undefined;
|
|
576
|
+
if (!initialCompletedSteps) throw new Error('Expected initialCompletedSteps');
|
|
577
|
+
|
|
578
|
+
for await (const event of threeStepWorkflow.run({
|
|
579
|
+
client: mockClient,
|
|
580
|
+
initialState,
|
|
581
|
+
initialCompletedSteps,
|
|
582
|
+
workflowRunId: 'test-run-id',
|
|
583
|
+
fileStore: testFileStore
|
|
584
|
+
})) {
|
|
585
|
+
if (event.type === WORKFLOW_EVENTS.RESTART) {
|
|
586
|
+
resumedState = event.initialState;
|
|
587
|
+
} else if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
588
|
+
resumedState = applyPatches(resumedState!, [event.patch]);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Verify only steps 2 and 3 were executed
|
|
593
|
+
expect(executedSteps).toEqual(["Step 2", "Step 3"]);
|
|
594
|
+
expect(executedSteps).not.toContain("Step 1");
|
|
595
|
+
|
|
596
|
+
// Verify the final state after all steps complete
|
|
597
|
+
expect(resumedState).toEqual({
|
|
598
|
+
value: 36,
|
|
599
|
+
initialValue: true
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
describe('nested workflows', () => {
|
|
605
|
+
it('should execute nested workflows and yield all inner workflow events', async () => {
|
|
606
|
+
// Create an inner workflow that will be nested
|
|
607
|
+
const innerWorkflow = workflow<{}, { value: number }>('Inner Workflow')
|
|
608
|
+
.step(
|
|
609
|
+
"Double value",
|
|
610
|
+
({ state }) => ({
|
|
611
|
+
inner: true,
|
|
612
|
+
value: state.value * 2
|
|
613
|
+
})
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Create outer workflow that uses the inner workflow
|
|
617
|
+
const outerWorkflow = workflow('Outer Workflow')
|
|
618
|
+
.step(
|
|
619
|
+
"Set prefix",
|
|
620
|
+
() => ({ prefix: "test-" })
|
|
621
|
+
)
|
|
622
|
+
.workflow(
|
|
623
|
+
"Run inner workflow",
|
|
624
|
+
innerWorkflow,
|
|
625
|
+
({ state, workflowState }) => ({
|
|
626
|
+
...state,
|
|
627
|
+
innerResult: workflowState.value
|
|
628
|
+
}),
|
|
629
|
+
() => ({ value: 5 })
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const events: WorkflowEvent<any>[] = [];
|
|
633
|
+
for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
634
|
+
events.push(event);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Verify all events are yielded in correct order
|
|
638
|
+
expect(events.map(e => ({
|
|
639
|
+
type: e.type,
|
|
640
|
+
workflowTitle: 'workflowTitle' in e ? e.workflowTitle : undefined,
|
|
641
|
+
status: 'status' in e ? e.status : undefined,
|
|
642
|
+
stepTitle: 'stepTitle' in e ? e.stepTitle : undefined
|
|
643
|
+
}))).toEqual([
|
|
644
|
+
// Outer workflow start
|
|
645
|
+
{
|
|
646
|
+
type: WORKFLOW_EVENTS.START,
|
|
647
|
+
workflowTitle: 'Outer Workflow',
|
|
648
|
+
status: STATUS.RUNNING,
|
|
649
|
+
stepTitle: undefined
|
|
650
|
+
},
|
|
651
|
+
// Initial step status for outer workflow
|
|
652
|
+
{
|
|
653
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
654
|
+
workflowTitle: undefined,
|
|
655
|
+
status: undefined,
|
|
656
|
+
stepTitle: undefined
|
|
657
|
+
},
|
|
658
|
+
// First step of outer workflow
|
|
659
|
+
{
|
|
660
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
661
|
+
workflowTitle: undefined,
|
|
662
|
+
status: STATUS.RUNNING,
|
|
663
|
+
stepTitle: 'Set prefix'
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
667
|
+
workflowTitle: undefined,
|
|
668
|
+
status: STATUS.RUNNING,
|
|
669
|
+
stepTitle: 'Set prefix'
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
673
|
+
workflowTitle: undefined,
|
|
674
|
+
status: undefined,
|
|
675
|
+
stepTitle: undefined
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
679
|
+
workflowTitle: undefined,
|
|
680
|
+
status: STATUS.RUNNING,
|
|
681
|
+
stepTitle: 'Run inner workflow'
|
|
682
|
+
},
|
|
683
|
+
// Inner workflow start
|
|
684
|
+
{
|
|
685
|
+
type: WORKFLOW_EVENTS.START,
|
|
686
|
+
workflowTitle: 'Inner Workflow',
|
|
687
|
+
status: STATUS.RUNNING,
|
|
688
|
+
stepTitle: undefined
|
|
689
|
+
},
|
|
690
|
+
// Initial step status for inner workflow
|
|
691
|
+
{
|
|
692
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
693
|
+
workflowTitle: undefined,
|
|
694
|
+
status: undefined,
|
|
695
|
+
stepTitle: undefined
|
|
696
|
+
},
|
|
697
|
+
// Inner workflow step
|
|
698
|
+
{
|
|
699
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
700
|
+
workflowTitle: undefined,
|
|
701
|
+
status: STATUS.RUNNING,
|
|
702
|
+
stepTitle: 'Double value'
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
706
|
+
workflowTitle: undefined,
|
|
707
|
+
status: STATUS.RUNNING,
|
|
708
|
+
stepTitle: 'Double value'
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
712
|
+
workflowTitle: undefined,
|
|
713
|
+
status: undefined,
|
|
714
|
+
stepTitle: undefined
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
type: WORKFLOW_EVENTS.COMPLETE,
|
|
718
|
+
workflowTitle: 'Inner Workflow',
|
|
719
|
+
status: STATUS.COMPLETE,
|
|
720
|
+
stepTitle: undefined
|
|
721
|
+
},
|
|
722
|
+
// Outer workflow nested step completion
|
|
723
|
+
{
|
|
724
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
725
|
+
workflowTitle: undefined,
|
|
726
|
+
status: STATUS.RUNNING,
|
|
727
|
+
stepTitle: 'Run inner workflow'
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
731
|
+
workflowTitle: undefined,
|
|
732
|
+
status: undefined,
|
|
733
|
+
stepTitle: undefined
|
|
734
|
+
},
|
|
735
|
+
// Outer workflow completion
|
|
736
|
+
{
|
|
737
|
+
type: WORKFLOW_EVENTS.COMPLETE,
|
|
738
|
+
workflowTitle: 'Outer Workflow',
|
|
739
|
+
status: STATUS.COMPLETE,
|
|
740
|
+
stepTitle: undefined
|
|
741
|
+
}
|
|
742
|
+
]);
|
|
743
|
+
|
|
744
|
+
// Verify states are passed correctly
|
|
745
|
+
let innerState: State = { value: 5 }; // Match the initial state from the workflow
|
|
746
|
+
let outerState = {};
|
|
747
|
+
|
|
748
|
+
for (const event of events) {
|
|
749
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
750
|
+
if (event.stepTitle === 'Double value') {
|
|
751
|
+
innerState = applyPatches(innerState, [event.patch]);
|
|
752
|
+
} else {
|
|
753
|
+
outerState = applyPatches(outerState, [event.patch]);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Verify final states
|
|
759
|
+
expect(innerState).toEqual({
|
|
760
|
+
inner: true,
|
|
761
|
+
value: 10
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
expect(outerState).toEqual({
|
|
765
|
+
prefix: "test-",
|
|
766
|
+
innerResult: 10
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should handle errors in nested workflows and propagate them up', async () => {
|
|
771
|
+
// Create an inner workflow that will throw an error
|
|
772
|
+
const innerWorkflow = workflow<{}, { inner: boolean, value: number }>('Failing Inner Workflow')
|
|
773
|
+
.step(
|
|
774
|
+
"Throw error",
|
|
775
|
+
(): { value: number } => {
|
|
776
|
+
throw new Error('Inner workflow error');
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
// Create outer workflow that uses the failing inner workflow
|
|
781
|
+
const outerWorkflow = workflow('Outer Workflow')
|
|
782
|
+
.step(
|
|
783
|
+
"First step",
|
|
784
|
+
() => ({ step: "first" })
|
|
785
|
+
)
|
|
786
|
+
.workflow(
|
|
787
|
+
"Run inner workflow",
|
|
788
|
+
innerWorkflow,
|
|
789
|
+
({ state, workflowState }) => ({
|
|
790
|
+
...state,
|
|
791
|
+
step: "second",
|
|
792
|
+
innerResult: workflowState.value
|
|
793
|
+
}),
|
|
794
|
+
() => ({ value: 5 })
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
const events: WorkflowEvent<any>[] = [];
|
|
798
|
+
let error: Error | undefined;
|
|
799
|
+
let mainWorkflowId: string | undefined;
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
803
|
+
events.push(event);
|
|
804
|
+
if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
|
|
805
|
+
mainWorkflowId = event.workflowRunId;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} catch (e) {
|
|
809
|
+
error = e as Error;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Verify error was thrown
|
|
813
|
+
expect(error?.message).toBe('Inner workflow error');
|
|
814
|
+
|
|
815
|
+
// Verify event sequence including error
|
|
816
|
+
expect(events).toEqual([
|
|
817
|
+
expect.objectContaining({
|
|
818
|
+
type: WORKFLOW_EVENTS.START,
|
|
819
|
+
workflowTitle: 'Outer Workflow',
|
|
820
|
+
status: STATUS.RUNNING,
|
|
821
|
+
workflowRunId: mainWorkflowId
|
|
822
|
+
}),
|
|
823
|
+
expect.objectContaining({
|
|
824
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
825
|
+
steps: expect.any(Array)
|
|
826
|
+
}),
|
|
827
|
+
expect.objectContaining({
|
|
828
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
829
|
+
status: STATUS.RUNNING,
|
|
830
|
+
stepTitle: 'First step'
|
|
831
|
+
}),
|
|
832
|
+
expect.objectContaining({
|
|
833
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
834
|
+
status: STATUS.RUNNING,
|
|
835
|
+
stepTitle: 'First step'
|
|
836
|
+
}),
|
|
837
|
+
expect.objectContaining({
|
|
838
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
839
|
+
steps: expect.any(Array)
|
|
840
|
+
}),
|
|
841
|
+
expect.objectContaining({
|
|
842
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
843
|
+
status: STATUS.RUNNING,
|
|
844
|
+
stepTitle: 'Run inner workflow'
|
|
845
|
+
}),
|
|
846
|
+
expect.objectContaining({
|
|
847
|
+
type: WORKFLOW_EVENTS.START,
|
|
848
|
+
workflowTitle: 'Failing Inner Workflow',
|
|
849
|
+
status: STATUS.RUNNING,
|
|
850
|
+
}),
|
|
851
|
+
expect.objectContaining({
|
|
852
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
853
|
+
steps: expect.any(Array)
|
|
854
|
+
}),
|
|
855
|
+
expect.objectContaining({
|
|
856
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
857
|
+
status: STATUS.RUNNING,
|
|
858
|
+
stepTitle: 'Throw error'
|
|
859
|
+
}),
|
|
860
|
+
expect.objectContaining({
|
|
861
|
+
type: WORKFLOW_EVENTS.ERROR,
|
|
862
|
+
workflowTitle: 'Failing Inner Workflow',
|
|
863
|
+
status: STATUS.ERROR,
|
|
864
|
+
error: expect.objectContaining({
|
|
865
|
+
name: expect.any(String),
|
|
866
|
+
message: expect.any(String)
|
|
867
|
+
}),
|
|
868
|
+
}),
|
|
869
|
+
expect.objectContaining({
|
|
870
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
871
|
+
steps: expect.arrayContaining([
|
|
872
|
+
expect.objectContaining({
|
|
873
|
+
title: 'Throw error',
|
|
874
|
+
status: STATUS.ERROR
|
|
875
|
+
})
|
|
876
|
+
])
|
|
877
|
+
}),
|
|
878
|
+
expect.objectContaining({
|
|
879
|
+
type: WORKFLOW_EVENTS.ERROR,
|
|
880
|
+
workflowTitle: 'Outer Workflow',
|
|
881
|
+
status: STATUS.ERROR,
|
|
882
|
+
error: expect.objectContaining({
|
|
883
|
+
name: expect.any(String),
|
|
884
|
+
message: expect.any(String)
|
|
885
|
+
})
|
|
886
|
+
}),
|
|
887
|
+
expect.objectContaining({
|
|
888
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
889
|
+
steps: expect.arrayContaining([
|
|
890
|
+
expect.objectContaining({
|
|
891
|
+
title: 'Run inner workflow',
|
|
892
|
+
status: STATUS.ERROR
|
|
893
|
+
})
|
|
894
|
+
])
|
|
895
|
+
})
|
|
896
|
+
]);
|
|
897
|
+
|
|
898
|
+
// Find inner and outer error events by workflowRunId
|
|
899
|
+
const innerErrorEvent = events.find(e =>
|
|
900
|
+
e.type === WORKFLOW_EVENTS.ERROR &&
|
|
901
|
+
e.workflowRunId !== mainWorkflowId
|
|
902
|
+
) as WorkflowErrorEvent<any>;
|
|
903
|
+
|
|
904
|
+
const outerErrorEvent = events.find(e =>
|
|
905
|
+
e.type === WORKFLOW_EVENTS.ERROR &&
|
|
906
|
+
e.workflowRunId === mainWorkflowId
|
|
907
|
+
) as WorkflowErrorEvent<any>;
|
|
908
|
+
|
|
909
|
+
expect(innerErrorEvent.error).toEqual(expect.objectContaining({
|
|
910
|
+
message: 'Inner workflow error'
|
|
911
|
+
}));
|
|
912
|
+
expect(outerErrorEvent.error).toEqual(expect.objectContaining({
|
|
913
|
+
message: 'Inner workflow error'
|
|
914
|
+
}));
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('should include patches in step status events for inner workflow steps', async () => {
|
|
918
|
+
interface InnerState extends State {
|
|
919
|
+
value: number;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
interface OuterState extends State {
|
|
923
|
+
value: number;
|
|
924
|
+
result?: number;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Create an inner workflow that modifies state
|
|
928
|
+
const innerWorkflow = workflow<{}, InnerState>('Inner Workflow')
|
|
929
|
+
.step("Double value", ({ state }) => ({
|
|
930
|
+
...state,
|
|
931
|
+
value: state.value * 2
|
|
932
|
+
}));
|
|
933
|
+
|
|
934
|
+
// Create outer workflow that uses the inner workflow
|
|
935
|
+
const outerWorkflow = workflow<{}, OuterState>('Outer Workflow')
|
|
936
|
+
.step("Set initial", () => ({
|
|
937
|
+
value: 5
|
|
938
|
+
}))
|
|
939
|
+
.workflow(
|
|
940
|
+
"Run inner workflow",
|
|
941
|
+
innerWorkflow,
|
|
942
|
+
({ state, workflowState }) => ({
|
|
943
|
+
...state,
|
|
944
|
+
result: workflowState.value
|
|
945
|
+
}),
|
|
946
|
+
(state) => ({ value: state.value })
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
// Run workflow and collect step status events
|
|
950
|
+
let finalStepStatus;
|
|
951
|
+
for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
952
|
+
if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
|
|
953
|
+
finalStepStatus = event;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Verify step status contains patches for all steps including the inner workflow step
|
|
958
|
+
expect(finalStepStatus?.steps).toEqual([
|
|
959
|
+
expect.objectContaining({
|
|
960
|
+
title: 'Set initial',
|
|
961
|
+
status: STATUS.COMPLETE,
|
|
962
|
+
patch: [{
|
|
963
|
+
op: 'add',
|
|
964
|
+
path: '/value',
|
|
965
|
+
value: 5
|
|
966
|
+
}]
|
|
967
|
+
}),
|
|
968
|
+
expect.objectContaining({
|
|
969
|
+
title: 'Run inner workflow',
|
|
970
|
+
status: STATUS.COMPLETE,
|
|
971
|
+
patch: [{
|
|
972
|
+
op: 'add',
|
|
973
|
+
path: '/result',
|
|
974
|
+
value: 10
|
|
975
|
+
}]
|
|
976
|
+
})
|
|
977
|
+
]);
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
describe('workflow options', () => {
|
|
982
|
+
it('should pass options through to workflow events', async () => {
|
|
983
|
+
const testWorkflow = workflow<{ testOption: string }>('Options Workflow')
|
|
984
|
+
.step(
|
|
985
|
+
"Simple step",
|
|
986
|
+
({ state, options }) => ({
|
|
987
|
+
value: 1,
|
|
988
|
+
passedOption: options.testOption
|
|
989
|
+
})
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const workflowOptions = {
|
|
993
|
+
testOption: 'test-value'
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
let finalEvent, finalStepStatus;
|
|
997
|
+
for await (const event of testWorkflow.run({
|
|
998
|
+
client: mockClient,
|
|
999
|
+
options: workflowOptions,
|
|
1000
|
+
fileStore: testFileStore
|
|
1001
|
+
})) {
|
|
1002
|
+
if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
|
|
1003
|
+
finalStepStatus = event;
|
|
1004
|
+
} else {
|
|
1005
|
+
finalEvent = event;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
expect(finalEvent).toEqual(expect.objectContaining({
|
|
1010
|
+
type: WORKFLOW_EVENTS.COMPLETE,
|
|
1011
|
+
status: STATUS.COMPLETE,
|
|
1012
|
+
workflowTitle: 'Options Workflow',
|
|
1013
|
+
workflowDescription: undefined,
|
|
1014
|
+
options: workflowOptions,
|
|
1015
|
+
}))
|
|
1016
|
+
expect(finalStepStatus).toEqual(expect.objectContaining({
|
|
1017
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
1018
|
+
steps: [
|
|
1019
|
+
expect.objectContaining({
|
|
1020
|
+
title: 'Simple step',
|
|
1021
|
+
patch: expect.any(Object),
|
|
1022
|
+
status: STATUS.COMPLETE,
|
|
1023
|
+
})
|
|
1024
|
+
],
|
|
1025
|
+
options: workflowOptions,
|
|
1026
|
+
}));
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
it('should provide empty object as default options', async () => {
|
|
1030
|
+
const testWorkflow = workflow('Default Options Workflow')
|
|
1031
|
+
.step(
|
|
1032
|
+
"Simple step",
|
|
1033
|
+
({ options }) => ({
|
|
1034
|
+
hasOptions: Object.keys(options).length === 0
|
|
1035
|
+
})
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
const workflowRun = testWorkflow.run({ client: mockClient, fileStore: testFileStore });
|
|
1039
|
+
|
|
1040
|
+
// Skip start event
|
|
1041
|
+
await workflowRun.next();
|
|
1042
|
+
|
|
1043
|
+
// Skip initial step status event
|
|
1044
|
+
await workflowRun.next();
|
|
1045
|
+
|
|
1046
|
+
// Check step start
|
|
1047
|
+
const stepStartResult = await workflowRun.next();
|
|
1048
|
+
expect(stepStartResult.value).toEqual(expect.objectContaining({
|
|
1049
|
+
options: {},
|
|
1050
|
+
type: WORKFLOW_EVENTS.STEP_START
|
|
1051
|
+
}));
|
|
1052
|
+
|
|
1053
|
+
// Check step completion
|
|
1054
|
+
const stepResult = await workflowRun.next();
|
|
1055
|
+
expect(stepResult.value).toEqual(expect.objectContaining({
|
|
1056
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
1057
|
+
stepTitle: 'Simple step',
|
|
1058
|
+
options: {},
|
|
1059
|
+
}));
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
describe('type inference', () => {
|
|
1064
|
+
it('should correctly infer complex workflow state types', async () => {
|
|
1065
|
+
// Create an inner workflow that uses the shared options type
|
|
1066
|
+
const innerWorkflow = workflow<{ features: string[] }>('Inner Type Test')
|
|
1067
|
+
.step(
|
|
1068
|
+
"Process features",
|
|
1069
|
+
({ options }) => ({
|
|
1070
|
+
processedValue: options.features.includes('fast') ? 100 : 42,
|
|
1071
|
+
featureCount: options.features.length
|
|
1072
|
+
})
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
// Create a complex workflow using multiple features
|
|
1076
|
+
const complexWorkflow = workflow<{ features: string[] }>('Complex Type Test')
|
|
1077
|
+
.step(
|
|
1078
|
+
"First step",
|
|
1079
|
+
({ options }) => ({
|
|
1080
|
+
initialFeatures: options.features,
|
|
1081
|
+
value: 42
|
|
1082
|
+
})
|
|
1083
|
+
)
|
|
1084
|
+
.workflow(
|
|
1085
|
+
"Nested workflow",
|
|
1086
|
+
innerWorkflow,
|
|
1087
|
+
({ state, workflowState }) => ({
|
|
1088
|
+
...state,
|
|
1089
|
+
processedValue: workflowState.processedValue,
|
|
1090
|
+
totalFeatures: workflowState.featureCount
|
|
1091
|
+
}),
|
|
1092
|
+
() => ({
|
|
1093
|
+
processedValue: 0,
|
|
1094
|
+
featureCount: 0
|
|
1095
|
+
})
|
|
1096
|
+
)
|
|
1097
|
+
.step(
|
|
1098
|
+
"Final step",
|
|
1099
|
+
({ state }) => ({
|
|
1100
|
+
...state,
|
|
1101
|
+
completed: true
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
// Type test setup
|
|
1106
|
+
type ExpectedState = {
|
|
1107
|
+
initialFeatures: string[];
|
|
1108
|
+
value: number;
|
|
1109
|
+
processedValue: number;
|
|
1110
|
+
totalFeatures: number;
|
|
1111
|
+
completed: true;
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
type ActualState = Parameters<
|
|
1115
|
+
Parameters<(typeof complexWorkflow)['step']>[1]
|
|
1116
|
+
>[0]['state'];
|
|
1117
|
+
|
|
1118
|
+
type TypeTest = AssertEquals<ActualState, ExpectedState>;
|
|
1119
|
+
const _typeAssert: TypeTest = true;
|
|
1120
|
+
|
|
1121
|
+
// Collect all events
|
|
1122
|
+
const events = [];
|
|
1123
|
+
let finalStepStatus, finalState = {};
|
|
1124
|
+
let mainWorkflowId: string | undefined;
|
|
1125
|
+
|
|
1126
|
+
for await (const event of complexWorkflow.run({
|
|
1127
|
+
client: mockClient,
|
|
1128
|
+
options: { features: ['fast', 'secure'] },
|
|
1129
|
+
fileStore: testFileStore
|
|
1130
|
+
})) {
|
|
1131
|
+
events.push(event);
|
|
1132
|
+
|
|
1133
|
+
// Capture the main workflow's ID from its start event
|
|
1134
|
+
if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
|
|
1135
|
+
mainWorkflowId = event.workflowRunId;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (event.type === WORKFLOW_EVENTS.STEP_STATUS) {
|
|
1139
|
+
finalStepStatus = event;
|
|
1140
|
+
} else if (
|
|
1141
|
+
event.type === WORKFLOW_EVENTS.STEP_COMPLETE &&
|
|
1142
|
+
event.workflowRunId === mainWorkflowId // Only process events from main workflow
|
|
1143
|
+
) {
|
|
1144
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Verify workflow start event
|
|
1149
|
+
expect(events[0]).toEqual(expect.objectContaining({
|
|
1150
|
+
type: WORKFLOW_EVENTS.START,
|
|
1151
|
+
status: STATUS.RUNNING,
|
|
1152
|
+
workflowTitle: 'Complex Type Test',
|
|
1153
|
+
workflowDescription: undefined,
|
|
1154
|
+
options: { features: ['fast', 'secure'] },
|
|
1155
|
+
workflowRunId: mainWorkflowId
|
|
1156
|
+
}));
|
|
1157
|
+
|
|
1158
|
+
// Verify inner workflow events are included
|
|
1159
|
+
const innerStartEvent = events.find(e =>
|
|
1160
|
+
e.type === WORKFLOW_EVENTS.START &&
|
|
1161
|
+
'workflowRunId' in e &&
|
|
1162
|
+
e.workflowRunId !== mainWorkflowId
|
|
1163
|
+
);
|
|
1164
|
+
expect(innerStartEvent).toEqual(expect.objectContaining({
|
|
1165
|
+
type: WORKFLOW_EVENTS.START,
|
|
1166
|
+
status: STATUS.RUNNING,
|
|
1167
|
+
workflowTitle: 'Inner Type Test',
|
|
1168
|
+
options: { features: ['fast', 'secure'] }
|
|
1169
|
+
}));
|
|
1170
|
+
|
|
1171
|
+
// Verify the final step status
|
|
1172
|
+
if (!finalStepStatus) throw new Error('Expected final step status event');
|
|
1173
|
+
const lastStep = finalStepStatus.steps[finalStepStatus.steps.length - 1];
|
|
1174
|
+
expect(lastStep.status).toBe(STATUS.COMPLETE);
|
|
1175
|
+
expect(lastStep.title).toBe('Final step');
|
|
1176
|
+
|
|
1177
|
+
expect(finalState).toEqual({
|
|
1178
|
+
initialFeatures: ['fast', 'secure'],
|
|
1179
|
+
value: 42,
|
|
1180
|
+
processedValue: 100,
|
|
1181
|
+
totalFeatures: 2,
|
|
1182
|
+
completed: true
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it('should correctly infer workflow reducer state types', async () => {
|
|
1187
|
+
// Create an inner workflow with a specific state shape
|
|
1188
|
+
const innerWorkflow = workflow('Inner State Test')
|
|
1189
|
+
.step(
|
|
1190
|
+
"Inner step",
|
|
1191
|
+
() => ({
|
|
1192
|
+
innerValue: 42,
|
|
1193
|
+
metadata: { processed: true }
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
// Create outer workflow to test reducer type inference
|
|
1198
|
+
const outerWorkflow = workflow('Outer State Test')
|
|
1199
|
+
.step(
|
|
1200
|
+
"First step",
|
|
1201
|
+
() => ({
|
|
1202
|
+
outerValue: 100,
|
|
1203
|
+
status: 'ready'
|
|
1204
|
+
})
|
|
1205
|
+
)
|
|
1206
|
+
.workflow(
|
|
1207
|
+
"Nested workflow",
|
|
1208
|
+
innerWorkflow,
|
|
1209
|
+
({ state, workflowState }) => {
|
|
1210
|
+
// Type assertion for outer state
|
|
1211
|
+
type ExpectedOuterState = {
|
|
1212
|
+
outerValue: number;
|
|
1213
|
+
status: string;
|
|
1214
|
+
};
|
|
1215
|
+
type ActualOuterState = typeof state;
|
|
1216
|
+
type OuterStateTest = AssertEquals<
|
|
1217
|
+
ActualOuterState,
|
|
1218
|
+
ExpectedOuterState
|
|
1219
|
+
>;
|
|
1220
|
+
const _outerAssert: OuterStateTest = true;
|
|
1221
|
+
|
|
1222
|
+
// Type assertion for inner workflow state
|
|
1223
|
+
type ExpectedInnerState = {
|
|
1224
|
+
innerValue: number;
|
|
1225
|
+
metadata: { processed: true };
|
|
1226
|
+
};
|
|
1227
|
+
type ActualInnerState = typeof workflowState;
|
|
1228
|
+
type InnerStateTest = AssertEquals<
|
|
1229
|
+
ActualInnerState,
|
|
1230
|
+
ExpectedInnerState
|
|
1231
|
+
>;
|
|
1232
|
+
const _innerAssert: InnerStateTest = true;
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
...state,
|
|
1236
|
+
innerResult: workflowState.innerValue,
|
|
1237
|
+
processed: workflowState.metadata.processed
|
|
1238
|
+
};
|
|
1239
|
+
},
|
|
1240
|
+
() => ({} as { innerValue: number; metadata: { processed: boolean } })
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
// Run the workflow to verify runtime behavior
|
|
1244
|
+
let finalState = {};
|
|
1245
|
+
let mainWorkflowId: string | undefined;
|
|
1246
|
+
|
|
1247
|
+
for await (const event of outerWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
1248
|
+
if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
|
|
1249
|
+
mainWorkflowId = event.workflowRunId;
|
|
1250
|
+
}
|
|
1251
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE && event.workflowRunId === mainWorkflowId) {
|
|
1252
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
expect(finalState).toEqual({
|
|
1257
|
+
outerValue: 100,
|
|
1258
|
+
status: 'ready',
|
|
1259
|
+
innerResult: 42,
|
|
1260
|
+
processed: true
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('should correctly infer step action state types', async () => {
|
|
1265
|
+
const testWorkflow = workflow('Action State Test')
|
|
1266
|
+
.step(
|
|
1267
|
+
"First step",
|
|
1268
|
+
() => ({
|
|
1269
|
+
count: 1,
|
|
1270
|
+
metadata: { created: new Date().toISOString() }
|
|
1271
|
+
})
|
|
1272
|
+
)
|
|
1273
|
+
.step(
|
|
1274
|
+
"Second step",
|
|
1275
|
+
({ state }) => {
|
|
1276
|
+
// Type assertion for action state
|
|
1277
|
+
type ExpectedState = {
|
|
1278
|
+
count: number;
|
|
1279
|
+
metadata: { created: string };
|
|
1280
|
+
};
|
|
1281
|
+
type ActualState = typeof state;
|
|
1282
|
+
type StateTest = AssertEquals<
|
|
1283
|
+
ActualState,
|
|
1284
|
+
ExpectedState
|
|
1285
|
+
>;
|
|
1286
|
+
const _stateAssert: StateTest = true;
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
...state,
|
|
1290
|
+
count: state.count + 1,
|
|
1291
|
+
metadata: {
|
|
1292
|
+
...state.metadata,
|
|
1293
|
+
updated: new Date().toISOString()
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
);
|
|
1298
|
+
|
|
1299
|
+
// Run the workflow to verify runtime behavior
|
|
1300
|
+
let finalState = {};
|
|
1301
|
+
let mainWorkflowId: string | undefined;
|
|
1302
|
+
|
|
1303
|
+
for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
1304
|
+
if (event.type === WORKFLOW_EVENTS.START && !mainWorkflowId) {
|
|
1305
|
+
mainWorkflowId = event.workflowRunId;
|
|
1306
|
+
}
|
|
1307
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE && event.workflowRunId === mainWorkflowId) {
|
|
1308
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
expect(finalState).toMatchObject({
|
|
1313
|
+
count: 2,
|
|
1314
|
+
metadata: {
|
|
1315
|
+
created: expect.any(String),
|
|
1316
|
+
updated: expect.any(String)
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it('should correctly infer prompt response types in subsequent steps', async () => {
|
|
1322
|
+
const testWorkflow = workflow('Prompt Type Test')
|
|
1323
|
+
.prompt(
|
|
1324
|
+
"Get user info",
|
|
1325
|
+
{
|
|
1326
|
+
template: () => "What is the user's info?",
|
|
1327
|
+
responseModel: {
|
|
1328
|
+
schema: z.object({ name: z.string(), age: z.number() }),
|
|
1329
|
+
name: "userInfo" as const // Must be const or type inference breaks
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
)
|
|
1333
|
+
.step(
|
|
1334
|
+
"Use response",
|
|
1335
|
+
({ state }) => {
|
|
1336
|
+
// Type assertion to verify state includes userInfo
|
|
1337
|
+
type ExpectedState = {
|
|
1338
|
+
userInfo: {
|
|
1339
|
+
name: string;
|
|
1340
|
+
age: number;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
type ActualState = typeof state;
|
|
1344
|
+
type StateTest = AssertEquals<ActualState, ExpectedState>;
|
|
1345
|
+
const _stateAssert: StateTest = true;
|
|
1346
|
+
|
|
1347
|
+
return {
|
|
1348
|
+
...state,
|
|
1349
|
+
greeting: `Hello ${state.userInfo.name}, you are ${state.userInfo.age} years old`
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
// Mock the client response
|
|
1355
|
+
mockClient.execute.mockResolvedValueOnce({
|
|
1356
|
+
name: "Test User",
|
|
1357
|
+
age: 30
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// Run workflow and collect final state
|
|
1361
|
+
let finalState = {};
|
|
1362
|
+
for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
1363
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
1364
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Verify the workflow executed correctly
|
|
1369
|
+
expect(finalState).toEqual({
|
|
1370
|
+
userInfo: {
|
|
1371
|
+
name: "Test User",
|
|
1372
|
+
age: 30
|
|
1373
|
+
},
|
|
1374
|
+
greeting: "Hello Test User, you are 30 years old"
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
it('should correctly handle prompt reduce function', async () => {
|
|
1379
|
+
const testWorkflow = workflow('Prompt Reduce Test')
|
|
1380
|
+
.prompt(
|
|
1381
|
+
"Get numbers",
|
|
1382
|
+
{
|
|
1383
|
+
template: () => "Give me some numbers",
|
|
1384
|
+
responseModel: {
|
|
1385
|
+
schema: z.object({ numbers: z.array(z.number()) }),
|
|
1386
|
+
name: "numbersResponse" as const
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
({ state, response, options }) => ({
|
|
1390
|
+
...state,
|
|
1391
|
+
numbersResponse: response, // Include the response explicitly
|
|
1392
|
+
sum: response.numbers.reduce((a, b) => a + b, 0),
|
|
1393
|
+
count: response.numbers.length
|
|
1394
|
+
})
|
|
1395
|
+
);
|
|
1396
|
+
|
|
1397
|
+
// Mock the client response
|
|
1398
|
+
mockClient.execute.mockResolvedValueOnce({
|
|
1399
|
+
numbers: [1, 2, 3, 4, 5]
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// Run workflow and collect final state
|
|
1403
|
+
let finalState = {};
|
|
1404
|
+
for await (const event of testWorkflow.run({ client: mockClient, fileStore: testFileStore })) {
|
|
1405
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
1406
|
+
finalState = applyPatches(finalState, [event.patch]);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Verify the workflow executed correctly with reduced state
|
|
1411
|
+
expect(finalState).toEqual({
|
|
1412
|
+
numbersResponse: {
|
|
1413
|
+
numbers: [1, 2, 3, 4, 5]
|
|
1414
|
+
},
|
|
1415
|
+
sum: 15,
|
|
1416
|
+
count: 5
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
// Verify type inference works correctly
|
|
1420
|
+
type ExpectedState = {
|
|
1421
|
+
numbersResponse: {
|
|
1422
|
+
numbers: number[];
|
|
1423
|
+
};
|
|
1424
|
+
sum: number;
|
|
1425
|
+
count: number;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
type ActualState = Parameters<
|
|
1429
|
+
Parameters<(typeof testWorkflow)['step']>[1]
|
|
1430
|
+
>[0]['state'];
|
|
1431
|
+
|
|
1432
|
+
type TypeTest = AssertEquals<ActualState, ExpectedState>;
|
|
1433
|
+
const _typeAssert: TypeTest = true;
|
|
1434
|
+
});
|
|
1435
|
+
});
|