ai-workflows 2.1.1 → 2.3.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -1
- package/README.md +305 -184
- package/dist/barrier.d.ts +159 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +377 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -8
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +53 -19
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +77 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +154 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +105 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +136 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +21 -4
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +507 -0
- package/src/cascade-context.ts +495 -0
- package/src/cascade-executor.ts +588 -0
- package/src/context.ts +51 -17
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/dependency-graph.ts +518 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +412 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +81 -26
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +179 -0
- package/src/types.ts +146 -10
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +199 -355
- package/test/barrier-join.test.ts +442 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +852 -0
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +353 -0
- package/test/send-race-conditions.test.ts +400 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -7
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- package/vitest.config.js +0 -7
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TopologicalExecutor Tests (RED Phase)
|
|
3
|
+
*
|
|
4
|
+
* Tests for TopologicalExecutor - executes workflow steps in topological order
|
|
5
|
+
* with parallel execution for steps at the same dependency level.
|
|
6
|
+
*
|
|
7
|
+
* These tests define the expected behavior for TopologicalExecutor before implementation.
|
|
8
|
+
* All tests SHOULD FAIL because TopologicalExecutor does not exist yet.
|
|
9
|
+
*
|
|
10
|
+
* Uses @cloudflare/vitest-pool-workers - NO MOCKS.
|
|
11
|
+
* Tests run against real Cloudflare Workflows bindings.
|
|
12
|
+
*
|
|
13
|
+
* Bead: aip-erlm
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
19
|
+
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// These imports will FAIL because TopologicalExecutor does not exist yet.
|
|
23
|
+
// This is the RED phase of TDD.
|
|
24
|
+
// ============================================================================
|
|
25
|
+
import {
|
|
26
|
+
TopologicalExecutor,
|
|
27
|
+
DurableGraph,
|
|
28
|
+
type ExecutionPlan,
|
|
29
|
+
type ExecutionResult,
|
|
30
|
+
type ExecutionLevel,
|
|
31
|
+
type StepExecutionResult,
|
|
32
|
+
} from '../../src/worker/topological-executor.js'
|
|
33
|
+
|
|
34
|
+
// Import WorkflowBuilder for creating test graphs
|
|
35
|
+
import { WorkflowBuilder, type BuiltWorkflow } from '../../src/worker/workflow-builder.js'
|
|
36
|
+
|
|
37
|
+
// Import DurableStep for step definitions
|
|
38
|
+
import { DurableStep, type StepConfig } from '../../src/worker/durable-step.js'
|
|
39
|
+
|
|
40
|
+
// Import topological sort types for comparison
|
|
41
|
+
import {
|
|
42
|
+
type SortableNode,
|
|
43
|
+
type ExecutionLevel as SortLevel,
|
|
44
|
+
} from '../../src/graph/topological-sort.js'
|
|
45
|
+
|
|
46
|
+
// Import the TestWorkflow that should be defined in worker.ts
|
|
47
|
+
import { TestWorkflow } from '../../src/worker.js'
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Type Definitions for Test Environment
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
interface TestEnv {
|
|
54
|
+
WORKFLOW: Workflow
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Helper Functions
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a workflow instance from the binding.
|
|
63
|
+
*/
|
|
64
|
+
async function getWorkflowInstance(name?: string): Promise<WorkflowInstance> {
|
|
65
|
+
const instance = await env.WORKFLOW.create({
|
|
66
|
+
id: name ?? crypto.randomUUID(),
|
|
67
|
+
})
|
|
68
|
+
return instance
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run a workflow and wait for it to complete.
|
|
73
|
+
*/
|
|
74
|
+
async function runWorkflow<T>(instance: WorkflowInstance, params?: unknown): Promise<T> {
|
|
75
|
+
const status = await instance.status()
|
|
76
|
+
if (status.status === 'queued' || status.status === 'running') {
|
|
77
|
+
let current = status
|
|
78
|
+
while (current.status !== 'complete' && current.status !== 'errored') {
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
80
|
+
current = await instance.status()
|
|
81
|
+
}
|
|
82
|
+
if (current.status === 'errored') {
|
|
83
|
+
const error = current.error as unknown
|
|
84
|
+
let errorMessage: string
|
|
85
|
+
|
|
86
|
+
if (typeof error === 'string') {
|
|
87
|
+
errorMessage = error
|
|
88
|
+
} else if (error && typeof error === 'object') {
|
|
89
|
+
const err = error as Record<string, unknown>
|
|
90
|
+
errorMessage = (err.message ??
|
|
91
|
+
err.name ??
|
|
92
|
+
err.error ??
|
|
93
|
+
(err.cause &&
|
|
94
|
+
typeof err.cause === 'object' &&
|
|
95
|
+
(err.cause as Record<string, unknown>).message) ??
|
|
96
|
+
JSON.stringify(error)) as string
|
|
97
|
+
} else {
|
|
98
|
+
errorMessage = String(error ?? 'Unknown error')
|
|
99
|
+
}
|
|
100
|
+
throw new Error(errorMessage)
|
|
101
|
+
}
|
|
102
|
+
return current.output as T
|
|
103
|
+
}
|
|
104
|
+
return status.output as T
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Test Data - Sample Step Functions
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
const stepA = async (input: { value: number }) => {
|
|
112
|
+
return { a: input.value * 2 }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stepB = async (input: { value: number }) => {
|
|
116
|
+
return { b: input.value + 10 }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stepC = async (input: { value: number }) => {
|
|
120
|
+
return { c: input.value - 5 }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stepD = async (input: { a: number; b?: number; c?: number }) => {
|
|
124
|
+
return { d: input.a + (input.b ?? 0) + (input.c ?? 0) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// 1. DurableGraph.fromBuilder() - Creating Execution Graph
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
describe('DurableGraph.fromBuilder()', () => {
|
|
132
|
+
it('should create a DurableGraph from a WorkflowBuilder', () => {
|
|
133
|
+
const builder = WorkflowBuilder.create('test-workflow')
|
|
134
|
+
.step('A', stepA)
|
|
135
|
+
.step('B', stepB)
|
|
136
|
+
.dependsOn('A')
|
|
137
|
+
.step('C', stepC)
|
|
138
|
+
.dependsOn('A')
|
|
139
|
+
.step('D', stepD)
|
|
140
|
+
.dependsOn('B', 'C')
|
|
141
|
+
|
|
142
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
143
|
+
|
|
144
|
+
expect(graph).toBeDefined()
|
|
145
|
+
expect(graph).toBeInstanceOf(DurableGraph)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should preserve step definitions from builder', () => {
|
|
149
|
+
const builder = WorkflowBuilder.create('test-workflow')
|
|
150
|
+
.step('validate', async (input: { orderId: string }) => ({ valid: true }))
|
|
151
|
+
.step('charge', async (input: { valid: boolean }) => ({ charged: true }))
|
|
152
|
+
.dependsOn('validate')
|
|
153
|
+
|
|
154
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
155
|
+
|
|
156
|
+
expect(graph.getStep('validate')).toBeDefined()
|
|
157
|
+
expect(graph.getStep('charge')).toBeDefined()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should preserve dependency relationships', () => {
|
|
161
|
+
const builder = WorkflowBuilder.create('test-workflow')
|
|
162
|
+
.step('A', stepA)
|
|
163
|
+
.step('B', stepB)
|
|
164
|
+
.dependsOn('A')
|
|
165
|
+
.step('C', stepC)
|
|
166
|
+
.dependsOn('B')
|
|
167
|
+
|
|
168
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
169
|
+
|
|
170
|
+
expect(graph.getDependencies('B')).toContain('A')
|
|
171
|
+
expect(graph.getDependencies('C')).toContain('B')
|
|
172
|
+
expect(graph.getDependencies('A')).toEqual([])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should calculate execution levels', () => {
|
|
176
|
+
const builder = WorkflowBuilder.create('test-workflow')
|
|
177
|
+
.step('A', stepA)
|
|
178
|
+
.step('B', stepB)
|
|
179
|
+
.dependsOn('A')
|
|
180
|
+
.step('C', stepC)
|
|
181
|
+
.dependsOn('A')
|
|
182
|
+
.step('D', stepD)
|
|
183
|
+
.dependsOn('B', 'C')
|
|
184
|
+
|
|
185
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
186
|
+
const levels = graph.getExecutionLevels()
|
|
187
|
+
|
|
188
|
+
// Level 0: A
|
|
189
|
+
// Level 1: B, C (both depend on A)
|
|
190
|
+
// Level 2: D (depends on B and C)
|
|
191
|
+
expect(levels).toHaveLength(3)
|
|
192
|
+
expect(levels[0].nodes).toContain('A')
|
|
193
|
+
expect(levels[1].nodes).toContain('B')
|
|
194
|
+
expect(levels[1].nodes).toContain('C')
|
|
195
|
+
expect(levels[2].nodes).toContain('D')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should support fromBuilt() for pre-built workflows', () => {
|
|
199
|
+
const workflow = WorkflowBuilder.create('test-workflow')
|
|
200
|
+
.step('A', stepA)
|
|
201
|
+
.step('B', stepB)
|
|
202
|
+
.dependsOn('A')
|
|
203
|
+
.build()
|
|
204
|
+
|
|
205
|
+
const graph = DurableGraph.fromBuilt(workflow)
|
|
206
|
+
|
|
207
|
+
expect(graph).toBeDefined()
|
|
208
|
+
expect(graph.getStep('A')).toBeDefined()
|
|
209
|
+
expect(graph.getStep('B')).toBeDefined()
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// 2. DurableGraph.validate() - No Circular Dependencies
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
describe('DurableGraph.validate()', () => {
|
|
218
|
+
it('should validate a valid DAG without errors', () => {
|
|
219
|
+
const builder = WorkflowBuilder.create('valid-dag')
|
|
220
|
+
.step('A', stepA)
|
|
221
|
+
.step('B', stepB)
|
|
222
|
+
.dependsOn('A')
|
|
223
|
+
.step('C', stepC)
|
|
224
|
+
.dependsOn('B')
|
|
225
|
+
|
|
226
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
227
|
+
|
|
228
|
+
expect(() => graph.validate()).not.toThrow()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should throw on circular dependencies', () => {
|
|
232
|
+
// Note: This should be caught at build time, but validate() provides an explicit check
|
|
233
|
+
const graph = new DurableGraph()
|
|
234
|
+
graph.addStep('A', stepA, ['B'])
|
|
235
|
+
graph.addStep('B', stepB, ['A'])
|
|
236
|
+
|
|
237
|
+
expect(() => graph.validate()).toThrow(/circular/i)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should throw on self-referencing dependencies', () => {
|
|
241
|
+
const graph = new DurableGraph()
|
|
242
|
+
graph.addStep('A', stepA, ['A'])
|
|
243
|
+
|
|
244
|
+
expect(() => graph.validate()).toThrow(/circular|self-reference/i)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should throw on indirect circular dependencies', () => {
|
|
248
|
+
const graph = new DurableGraph()
|
|
249
|
+
graph.addStep('A', stepA, ['C'])
|
|
250
|
+
graph.addStep('B', stepB, ['A'])
|
|
251
|
+
graph.addStep('C', stepC, ['B'])
|
|
252
|
+
|
|
253
|
+
expect(() => graph.validate()).toThrow(/circular/i)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should report the cycle path in the error', () => {
|
|
257
|
+
const graph = new DurableGraph()
|
|
258
|
+
graph.addStep('A', stepA, ['C'])
|
|
259
|
+
graph.addStep('B', stepB, ['A'])
|
|
260
|
+
graph.addStep('C', stepC, ['B'])
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
graph.validate()
|
|
264
|
+
expect.fail('Should have thrown')
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const err = error as Error & { cyclePath?: string[] }
|
|
267
|
+
expect(err.cyclePath).toBeDefined()
|
|
268
|
+
expect(err.cyclePath?.length).toBeGreaterThanOrEqual(3)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should validate missing dependencies', () => {
|
|
273
|
+
const graph = new DurableGraph()
|
|
274
|
+
graph.addStep('A', stepA, ['MISSING'])
|
|
275
|
+
|
|
276
|
+
expect(() => graph.validate({ strict: true })).toThrow(/missing.*MISSING/i)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// 3. TopologicalExecutor.run() - Execution in Dependency Order
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
describe('TopologicalExecutor.run()', () => {
|
|
285
|
+
it('should create a TopologicalExecutor from a DurableGraph', () => {
|
|
286
|
+
const builder = WorkflowBuilder.create('exec-test')
|
|
287
|
+
.step('A', stepA)
|
|
288
|
+
.step('B', stepB)
|
|
289
|
+
.dependsOn('A')
|
|
290
|
+
|
|
291
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
292
|
+
const executor = new TopologicalExecutor(graph)
|
|
293
|
+
|
|
294
|
+
expect(executor).toBeDefined()
|
|
295
|
+
expect(executor).toBeInstanceOf(TopologicalExecutor)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should execute steps in dependency order', async () => {
|
|
299
|
+
const executionOrder: string[] = []
|
|
300
|
+
|
|
301
|
+
const builder = WorkflowBuilder.create('order-test')
|
|
302
|
+
.step('A', async () => {
|
|
303
|
+
executionOrder.push('A')
|
|
304
|
+
return { a: 1 }
|
|
305
|
+
})
|
|
306
|
+
.step('B', async () => {
|
|
307
|
+
executionOrder.push('B')
|
|
308
|
+
return { b: 2 }
|
|
309
|
+
})
|
|
310
|
+
.dependsOn('A')
|
|
311
|
+
.step('C', async () => {
|
|
312
|
+
executionOrder.push('C')
|
|
313
|
+
return { c: 3 }
|
|
314
|
+
})
|
|
315
|
+
.dependsOn('B')
|
|
316
|
+
|
|
317
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
318
|
+
const executor = new TopologicalExecutor(graph)
|
|
319
|
+
|
|
320
|
+
await executor.run({ value: 1 })
|
|
321
|
+
|
|
322
|
+
expect(executionOrder).toEqual(['A', 'B', 'C'])
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should pass initial input to root steps', async () => {
|
|
326
|
+
let receivedInput: unknown = null
|
|
327
|
+
|
|
328
|
+
const builder = WorkflowBuilder.create('input-test').step(
|
|
329
|
+
'root',
|
|
330
|
+
async (input: { value: number }) => {
|
|
331
|
+
receivedInput = input
|
|
332
|
+
return { doubled: input.value * 2 }
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
337
|
+
const executor = new TopologicalExecutor(graph)
|
|
338
|
+
|
|
339
|
+
await executor.run({ value: 42 })
|
|
340
|
+
|
|
341
|
+
expect(receivedInput).toEqual({ value: 42 })
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('should provide step outputs to dependent steps', async () => {
|
|
345
|
+
let stepBInput: unknown = null
|
|
346
|
+
|
|
347
|
+
const builder = WorkflowBuilder.create('output-test')
|
|
348
|
+
.step('A', async (input: { value: number }) => {
|
|
349
|
+
return { doubled: input.value * 2 }
|
|
350
|
+
})
|
|
351
|
+
.step('B', async (input: unknown, ctx) => {
|
|
352
|
+
stepBInput = ctx.getStepResult('A')
|
|
353
|
+
return { received: true }
|
|
354
|
+
})
|
|
355
|
+
.dependsOn('A')
|
|
356
|
+
|
|
357
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
358
|
+
const executor = new TopologicalExecutor(graph)
|
|
359
|
+
|
|
360
|
+
await executor.run({ value: 21 })
|
|
361
|
+
|
|
362
|
+
expect(stepBInput).toEqual({ doubled: 42 })
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should return all step results', async () => {
|
|
366
|
+
const builder = WorkflowBuilder.create('results-test')
|
|
367
|
+
.step('A', async () => ({ a: 1 }))
|
|
368
|
+
.step('B', async () => ({ b: 2 }))
|
|
369
|
+
.step('C', async () => ({ c: 3 }))
|
|
370
|
+
|
|
371
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
372
|
+
const executor = new TopologicalExecutor(graph)
|
|
373
|
+
|
|
374
|
+
const results = await executor.run({})
|
|
375
|
+
|
|
376
|
+
expect(results.A).toEqual({ a: 1 })
|
|
377
|
+
expect(results.B).toEqual({ b: 2 })
|
|
378
|
+
expect(results.C).toEqual({ c: 3 })
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('should work with real Cloudflare Workflows binding', async () => {
|
|
382
|
+
const instance = await getWorkflowInstance('topo-exec-test-1')
|
|
383
|
+
|
|
384
|
+
const result = await runWorkflow<{
|
|
385
|
+
executionOrder: string[]
|
|
386
|
+
stepResults: Record<string, unknown>
|
|
387
|
+
}>(instance)
|
|
388
|
+
|
|
389
|
+
// A runs first, then B and C in parallel, then D
|
|
390
|
+
expect(result.executionOrder[0]).toBe('A')
|
|
391
|
+
expect(result.executionOrder[result.executionOrder.length - 1]).toBe('D')
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// ============================================================================
|
|
396
|
+
// 4. TopologicalExecutor - Steps with No Dependencies Run in Parallel (Level 0)
|
|
397
|
+
// ============================================================================
|
|
398
|
+
|
|
399
|
+
describe('TopologicalExecutor - parallel root execution', () => {
|
|
400
|
+
it('should run steps with no dependencies in parallel', async () => {
|
|
401
|
+
const startTimes: Record<string, number> = {}
|
|
402
|
+
|
|
403
|
+
const builder = WorkflowBuilder.create('parallel-roots')
|
|
404
|
+
.step('A', async () => {
|
|
405
|
+
startTimes.A = Date.now()
|
|
406
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
407
|
+
return { a: 1 }
|
|
408
|
+
})
|
|
409
|
+
.step('B', async () => {
|
|
410
|
+
startTimes.B = Date.now()
|
|
411
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
412
|
+
return { b: 2 }
|
|
413
|
+
})
|
|
414
|
+
.step('C', async () => {
|
|
415
|
+
startTimes.C = Date.now()
|
|
416
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
417
|
+
return { c: 3 }
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
421
|
+
const executor = new TopologicalExecutor(graph)
|
|
422
|
+
|
|
423
|
+
await executor.run({})
|
|
424
|
+
|
|
425
|
+
// All should start within 20ms of each other (running in parallel)
|
|
426
|
+
const times = Object.values(startTimes)
|
|
427
|
+
const maxDiff = Math.max(...times) - Math.min(...times)
|
|
428
|
+
expect(maxDiff).toBeLessThan(20)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should complete level 0 before starting level 1', async () => {
|
|
432
|
+
const level0End = { time: 0 }
|
|
433
|
+
const level1Start = { time: Infinity }
|
|
434
|
+
|
|
435
|
+
const builder = WorkflowBuilder.create('level-order')
|
|
436
|
+
.step('A', async () => {
|
|
437
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
438
|
+
level0End.time = Math.max(level0End.time, Date.now())
|
|
439
|
+
return { a: 1 }
|
|
440
|
+
})
|
|
441
|
+
.step('B', async () => {
|
|
442
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
443
|
+
level0End.time = Math.max(level0End.time, Date.now())
|
|
444
|
+
return { b: 2 }
|
|
445
|
+
})
|
|
446
|
+
.step('C', async () => {
|
|
447
|
+
level1Start.time = Math.min(level1Start.time, Date.now())
|
|
448
|
+
return { c: 3 }
|
|
449
|
+
})
|
|
450
|
+
.dependsOn('A', 'B')
|
|
451
|
+
|
|
452
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
453
|
+
const executor = new TopologicalExecutor(graph)
|
|
454
|
+
|
|
455
|
+
await executor.run({})
|
|
456
|
+
|
|
457
|
+
// Level 1 should start after level 0 completes
|
|
458
|
+
expect(level1Start.time).toBeGreaterThanOrEqual(level0End.time)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('should execute all roots even if some are fast', async () => {
|
|
462
|
+
const executed: string[] = []
|
|
463
|
+
|
|
464
|
+
const builder = WorkflowBuilder.create('all-roots')
|
|
465
|
+
.step('fast', async () => {
|
|
466
|
+
executed.push('fast')
|
|
467
|
+
return { fast: true }
|
|
468
|
+
})
|
|
469
|
+
.step('slow', async () => {
|
|
470
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
471
|
+
executed.push('slow')
|
|
472
|
+
return { slow: true }
|
|
473
|
+
})
|
|
474
|
+
.step('medium', async () => {
|
|
475
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
476
|
+
executed.push('medium')
|
|
477
|
+
return { medium: true }
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
481
|
+
const executor = new TopologicalExecutor(graph)
|
|
482
|
+
|
|
483
|
+
await executor.run({})
|
|
484
|
+
|
|
485
|
+
expect(executed).toContain('fast')
|
|
486
|
+
expect(executed).toContain('slow')
|
|
487
|
+
expect(executed).toContain('medium')
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// 5. TopologicalExecutor - Steps at Same Level Run in Parallel
|
|
493
|
+
// ============================================================================
|
|
494
|
+
|
|
495
|
+
describe('TopologicalExecutor - parallel level execution', () => {
|
|
496
|
+
it('should run steps at the same level in parallel', async () => {
|
|
497
|
+
const startTimes: Record<string, number> = {}
|
|
498
|
+
|
|
499
|
+
const builder = WorkflowBuilder.create('parallel-level')
|
|
500
|
+
.step('root', async () => {
|
|
501
|
+
return { root: true }
|
|
502
|
+
})
|
|
503
|
+
.step('A', async () => {
|
|
504
|
+
startTimes.A = Date.now()
|
|
505
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
506
|
+
return { a: 1 }
|
|
507
|
+
})
|
|
508
|
+
.dependsOn('root')
|
|
509
|
+
.step('B', async () => {
|
|
510
|
+
startTimes.B = Date.now()
|
|
511
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
512
|
+
return { b: 2 }
|
|
513
|
+
})
|
|
514
|
+
.dependsOn('root')
|
|
515
|
+
.step('C', async () => {
|
|
516
|
+
startTimes.C = Date.now()
|
|
517
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
518
|
+
return { c: 3 }
|
|
519
|
+
})
|
|
520
|
+
.dependsOn('root')
|
|
521
|
+
|
|
522
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
523
|
+
const executor = new TopologicalExecutor(graph)
|
|
524
|
+
|
|
525
|
+
await executor.run({})
|
|
526
|
+
|
|
527
|
+
// A, B, C should all start at approximately the same time
|
|
528
|
+
const maxDiff =
|
|
529
|
+
Math.max(startTimes.A, startTimes.B, startTimes.C) -
|
|
530
|
+
Math.min(startTimes.A, startTimes.B, startTimes.C)
|
|
531
|
+
expect(maxDiff).toBeLessThan(20)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('should handle diamond dependency pattern correctly', async () => {
|
|
535
|
+
// A
|
|
536
|
+
// / \
|
|
537
|
+
// B C
|
|
538
|
+
// \ /
|
|
539
|
+
// D
|
|
540
|
+
const executionOrder: string[] = []
|
|
541
|
+
|
|
542
|
+
const builder = WorkflowBuilder.create('diamond')
|
|
543
|
+
.step('A', async () => {
|
|
544
|
+
executionOrder.push('A')
|
|
545
|
+
return { a: 1 }
|
|
546
|
+
})
|
|
547
|
+
.step('B', async () => {
|
|
548
|
+
executionOrder.push('B')
|
|
549
|
+
return { b: 2 }
|
|
550
|
+
})
|
|
551
|
+
.dependsOn('A')
|
|
552
|
+
.step('C', async () => {
|
|
553
|
+
executionOrder.push('C')
|
|
554
|
+
return { c: 3 }
|
|
555
|
+
})
|
|
556
|
+
.dependsOn('A')
|
|
557
|
+
.step('D', async () => {
|
|
558
|
+
executionOrder.push('D')
|
|
559
|
+
return { d: 4 }
|
|
560
|
+
})
|
|
561
|
+
.dependsOn('B', 'C')
|
|
562
|
+
|
|
563
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
564
|
+
const executor = new TopologicalExecutor(graph)
|
|
565
|
+
|
|
566
|
+
await executor.run({})
|
|
567
|
+
|
|
568
|
+
// A must be first, D must be last
|
|
569
|
+
expect(executionOrder[0]).toBe('A')
|
|
570
|
+
expect(executionOrder[executionOrder.length - 1]).toBe('D')
|
|
571
|
+
// B and C can be in any order (they run in parallel)
|
|
572
|
+
expect(executionOrder).toContain('B')
|
|
573
|
+
expect(executionOrder).toContain('C')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('should provide execution metrics per level', async () => {
|
|
577
|
+
const builder = WorkflowBuilder.create('metrics-test')
|
|
578
|
+
.step('A', async () => {
|
|
579
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
580
|
+
return { a: 1 }
|
|
581
|
+
})
|
|
582
|
+
.step('B', async () => {
|
|
583
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
584
|
+
return { b: 2 }
|
|
585
|
+
})
|
|
586
|
+
.dependsOn('A')
|
|
587
|
+
|
|
588
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
589
|
+
const executor = new TopologicalExecutor(graph)
|
|
590
|
+
|
|
591
|
+
const result = await executor.run({})
|
|
592
|
+
|
|
593
|
+
expect(result._meta).toBeDefined()
|
|
594
|
+
expect(result._meta.levels).toHaveLength(2)
|
|
595
|
+
expect(result._meta.levels[0].duration).toBeGreaterThanOrEqual(30)
|
|
596
|
+
expect(result._meta.levels[1].duration).toBeGreaterThanOrEqual(50)
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// ============================================================================
|
|
601
|
+
// 6. TopologicalExecutor - Step Waits for All Dependencies
|
|
602
|
+
// ============================================================================
|
|
603
|
+
|
|
604
|
+
describe('TopologicalExecutor - dependency waiting', () => {
|
|
605
|
+
it('should wait for all dependencies before executing a step', async () => {
|
|
606
|
+
const completions: string[] = []
|
|
607
|
+
let dStartTime = 0
|
|
608
|
+
let bEndTime = 0
|
|
609
|
+
let cEndTime = 0
|
|
610
|
+
|
|
611
|
+
const builder = WorkflowBuilder.create('wait-all')
|
|
612
|
+
.step('A', async () => {
|
|
613
|
+
return { a: 1 }
|
|
614
|
+
})
|
|
615
|
+
.step('B', async () => {
|
|
616
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
617
|
+
bEndTime = Date.now()
|
|
618
|
+
completions.push('B')
|
|
619
|
+
return { b: 2 }
|
|
620
|
+
})
|
|
621
|
+
.dependsOn('A')
|
|
622
|
+
.step('C', async () => {
|
|
623
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
624
|
+
cEndTime = Date.now()
|
|
625
|
+
completions.push('C')
|
|
626
|
+
return { c: 3 }
|
|
627
|
+
})
|
|
628
|
+
.dependsOn('A')
|
|
629
|
+
.step('D', async () => {
|
|
630
|
+
dStartTime = Date.now()
|
|
631
|
+
completions.push('D')
|
|
632
|
+
return { d: 4 }
|
|
633
|
+
})
|
|
634
|
+
.dependsOn('B', 'C')
|
|
635
|
+
|
|
636
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
637
|
+
const executor = new TopologicalExecutor(graph)
|
|
638
|
+
|
|
639
|
+
await executor.run({})
|
|
640
|
+
|
|
641
|
+
// D should start after both B and C complete
|
|
642
|
+
expect(dStartTime).toBeGreaterThanOrEqual(Math.max(bEndTime, cEndTime))
|
|
643
|
+
// D should be after B and C in completions
|
|
644
|
+
expect(completions.indexOf('D')).toBeGreaterThan(completions.indexOf('B'))
|
|
645
|
+
expect(completions.indexOf('D')).toBeGreaterThan(completions.indexOf('C'))
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
it('should have access to all dependency outputs', async () => {
|
|
649
|
+
let receivedFromB: unknown = null
|
|
650
|
+
let receivedFromC: unknown = null
|
|
651
|
+
|
|
652
|
+
const builder = WorkflowBuilder.create('access-outputs')
|
|
653
|
+
.step('B', async () => ({ fromB: 'B-value' }))
|
|
654
|
+
.step('C', async () => ({ fromC: 'C-value' }))
|
|
655
|
+
.step('D', async (input, ctx) => {
|
|
656
|
+
receivedFromB = ctx.getStepResult('B')
|
|
657
|
+
receivedFromC = ctx.getStepResult('C')
|
|
658
|
+
return { d: true }
|
|
659
|
+
})
|
|
660
|
+
.dependsOn('B', 'C')
|
|
661
|
+
|
|
662
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
663
|
+
const executor = new TopologicalExecutor(graph)
|
|
664
|
+
|
|
665
|
+
await executor.run({})
|
|
666
|
+
|
|
667
|
+
expect(receivedFromB).toEqual({ fromB: 'B-value' })
|
|
668
|
+
expect(receivedFromC).toEqual({ fromC: 'C-value' })
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should handle partial dependency completion gracefully', async () => {
|
|
672
|
+
const stepStartTimes: Record<string, number> = {}
|
|
673
|
+
|
|
674
|
+
const builder = WorkflowBuilder.create('partial-deps')
|
|
675
|
+
.step('fast', async () => {
|
|
676
|
+
stepStartTimes.fast = Date.now()
|
|
677
|
+
return { fast: true }
|
|
678
|
+
})
|
|
679
|
+
.step('slow', async () => {
|
|
680
|
+
stepStartTimes.slow = Date.now()
|
|
681
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
682
|
+
return { slow: true }
|
|
683
|
+
})
|
|
684
|
+
.step('waiter', async () => {
|
|
685
|
+
stepStartTimes.waiter = Date.now()
|
|
686
|
+
return { waiting: true }
|
|
687
|
+
})
|
|
688
|
+
.dependsOn('fast', 'slow')
|
|
689
|
+
|
|
690
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
691
|
+
const executor = new TopologicalExecutor(graph)
|
|
692
|
+
|
|
693
|
+
await executor.run({})
|
|
694
|
+
|
|
695
|
+
// waiter should not start until slow completes (~100ms after start)
|
|
696
|
+
expect(stepStartTimes.waiter - stepStartTimes.fast).toBeGreaterThanOrEqual(90)
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
// ============================================================================
|
|
701
|
+
// 7. TopologicalExecutor - Failed Step Blocks Dependent Steps
|
|
702
|
+
// ============================================================================
|
|
703
|
+
|
|
704
|
+
describe('TopologicalExecutor - failure handling', () => {
|
|
705
|
+
it('should not execute dependent steps when a step fails', async () => {
|
|
706
|
+
const executed: string[] = []
|
|
707
|
+
|
|
708
|
+
const builder = WorkflowBuilder.create('failure-blocks')
|
|
709
|
+
.step('A', async () => {
|
|
710
|
+
executed.push('A')
|
|
711
|
+
throw new Error('A failed')
|
|
712
|
+
})
|
|
713
|
+
.step('B', async () => {
|
|
714
|
+
executed.push('B')
|
|
715
|
+
return { b: 2 }
|
|
716
|
+
})
|
|
717
|
+
.dependsOn('A')
|
|
718
|
+
.step('C', async () => {
|
|
719
|
+
executed.push('C')
|
|
720
|
+
return { c: 3 }
|
|
721
|
+
})
|
|
722
|
+
.dependsOn('B')
|
|
723
|
+
|
|
724
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
725
|
+
const executor = new TopologicalExecutor(graph)
|
|
726
|
+
|
|
727
|
+
await expect(executor.run({})).rejects.toThrow('A failed')
|
|
728
|
+
|
|
729
|
+
expect(executed).toContain('A')
|
|
730
|
+
expect(executed).not.toContain('B')
|
|
731
|
+
expect(executed).not.toContain('C')
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it('should continue executing unrelated branches on failure', async () => {
|
|
735
|
+
const executed: string[] = []
|
|
736
|
+
|
|
737
|
+
const builder = WorkflowBuilder.create('independent-branches')
|
|
738
|
+
.step('root', async () => {
|
|
739
|
+
executed.push('root')
|
|
740
|
+
return { root: true }
|
|
741
|
+
})
|
|
742
|
+
.step('fail-branch', async () => {
|
|
743
|
+
executed.push('fail-branch')
|
|
744
|
+
throw new Error('Branch failed')
|
|
745
|
+
})
|
|
746
|
+
.dependsOn('root')
|
|
747
|
+
.step('success-branch', async () => {
|
|
748
|
+
executed.push('success-branch')
|
|
749
|
+
return { success: true }
|
|
750
|
+
})
|
|
751
|
+
.dependsOn('root')
|
|
752
|
+
|
|
753
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
754
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
755
|
+
|
|
756
|
+
const result = await executor.run({})
|
|
757
|
+
|
|
758
|
+
expect(executed).toContain('root')
|
|
759
|
+
expect(executed).toContain('success-branch')
|
|
760
|
+
expect(result.errors).toBeDefined()
|
|
761
|
+
expect(result.errors['fail-branch']).toBeDefined()
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it('should track which steps were blocked by failures', async () => {
|
|
765
|
+
const builder = WorkflowBuilder.create('blocked-tracking')
|
|
766
|
+
.step('A', async () => {
|
|
767
|
+
throw new Error('A failed')
|
|
768
|
+
})
|
|
769
|
+
.step('B', async () => ({ b: 2 }))
|
|
770
|
+
.dependsOn('A')
|
|
771
|
+
.step('C', async () => ({ c: 3 }))
|
|
772
|
+
.dependsOn('B')
|
|
773
|
+
|
|
774
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
775
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
776
|
+
|
|
777
|
+
const result = await executor.run({})
|
|
778
|
+
|
|
779
|
+
expect(result.blocked).toBeDefined()
|
|
780
|
+
expect(result.blocked).toContain('B')
|
|
781
|
+
expect(result.blocked).toContain('C')
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('should provide error details in execution result', async () => {
|
|
785
|
+
const builder = WorkflowBuilder.create('error-details').step('failing', async () => {
|
|
786
|
+
throw new Error('Detailed error message')
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
790
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
791
|
+
|
|
792
|
+
const result = await executor.run({})
|
|
793
|
+
|
|
794
|
+
expect(result.errors.failing).toBeDefined()
|
|
795
|
+
expect(result.errors.failing.message).toBe('Detailed error message')
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// ============================================================================
|
|
800
|
+
// 8. TopologicalExecutor - Parallel Execution Uses Promise.all
|
|
801
|
+
// ============================================================================
|
|
802
|
+
|
|
803
|
+
describe('TopologicalExecutor - Promise.all for concurrent steps', () => {
|
|
804
|
+
it('should execute same-level steps with Promise.all semantics', async () => {
|
|
805
|
+
const timings: { start: number; end: number }[] = []
|
|
806
|
+
const stepDuration = 50
|
|
807
|
+
|
|
808
|
+
const builder = WorkflowBuilder.create('promise-all-test')
|
|
809
|
+
.step('A', async () => {
|
|
810
|
+
const start = Date.now()
|
|
811
|
+
await new Promise((r) => setTimeout(r, stepDuration))
|
|
812
|
+
timings.push({ start, end: Date.now() })
|
|
813
|
+
return { a: 1 }
|
|
814
|
+
})
|
|
815
|
+
.step('B', async () => {
|
|
816
|
+
const start = Date.now()
|
|
817
|
+
await new Promise((r) => setTimeout(r, stepDuration))
|
|
818
|
+
timings.push({ start, end: Date.now() })
|
|
819
|
+
return { b: 2 }
|
|
820
|
+
})
|
|
821
|
+
.step('C', async () => {
|
|
822
|
+
const start = Date.now()
|
|
823
|
+
await new Promise((r) => setTimeout(r, stepDuration))
|
|
824
|
+
timings.push({ start, end: Date.now() })
|
|
825
|
+
return { c: 3 }
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
829
|
+
const executor = new TopologicalExecutor(graph)
|
|
830
|
+
|
|
831
|
+
const startTime = Date.now()
|
|
832
|
+
await executor.run({})
|
|
833
|
+
const totalTime = Date.now() - startTime
|
|
834
|
+
|
|
835
|
+
// If running sequentially, would take ~150ms
|
|
836
|
+
// With Promise.all, should take ~50ms (plus overhead)
|
|
837
|
+
expect(totalTime).toBeLessThan(100)
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
it('should fail fast when any step in Promise.all fails', async () => {
|
|
841
|
+
const executed: string[] = []
|
|
842
|
+
|
|
843
|
+
const builder = WorkflowBuilder.create('fail-fast')
|
|
844
|
+
.step('fast-fail', async () => {
|
|
845
|
+
executed.push('fast-fail-start')
|
|
846
|
+
throw new Error('Fast failure')
|
|
847
|
+
})
|
|
848
|
+
.step('slow', async () => {
|
|
849
|
+
executed.push('slow-start')
|
|
850
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
851
|
+
executed.push('slow-end')
|
|
852
|
+
return { slow: true }
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
856
|
+
const executor = new TopologicalExecutor(graph)
|
|
857
|
+
|
|
858
|
+
await expect(executor.run({})).rejects.toThrow('Fast failure')
|
|
859
|
+
|
|
860
|
+
// The slow step should have started but may not complete
|
|
861
|
+
expect(executed).toContain('fast-fail-start')
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
it('should collect all results from parallel steps', async () => {
|
|
865
|
+
const builder = WorkflowBuilder.create('collect-results')
|
|
866
|
+
.step('A', async () => ({ result: 'A' }))
|
|
867
|
+
.step('B', async () => ({ result: 'B' }))
|
|
868
|
+
.step('C', async () => ({ result: 'C' }))
|
|
869
|
+
|
|
870
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
871
|
+
const executor = new TopologicalExecutor(graph)
|
|
872
|
+
|
|
873
|
+
const results = await executor.run({})
|
|
874
|
+
|
|
875
|
+
expect(results.A).toEqual({ result: 'A' })
|
|
876
|
+
expect(results.B).toEqual({ result: 'B' })
|
|
877
|
+
expect(results.C).toEqual({ result: 'C' })
|
|
878
|
+
})
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
// ============================================================================
|
|
882
|
+
// 9. TopologicalExecutor - Execution State Persists Across Restarts
|
|
883
|
+
// ============================================================================
|
|
884
|
+
|
|
885
|
+
describe('TopologicalExecutor - state persistence', () => {
|
|
886
|
+
it('should persist completed step results', async () => {
|
|
887
|
+
const instance = await getWorkflowInstance('persist-test-1')
|
|
888
|
+
|
|
889
|
+
const result = await runWorkflow<{
|
|
890
|
+
checkpointedSteps: string[]
|
|
891
|
+
allCompleted: boolean
|
|
892
|
+
}>(instance)
|
|
893
|
+
|
|
894
|
+
expect(result.checkpointedSteps).toContain('A')
|
|
895
|
+
expect(result.checkpointedSteps).toContain('B')
|
|
896
|
+
expect(result.allCompleted).toBe(true)
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
it('should resume from last completed step on restart', async () => {
|
|
900
|
+
const instance = await getWorkflowInstance('resume-test-1')
|
|
901
|
+
|
|
902
|
+
const result = await runWorkflow<{
|
|
903
|
+
resumedFromStep: string
|
|
904
|
+
stepsSkipped: string[]
|
|
905
|
+
stepsExecuted: string[]
|
|
906
|
+
}>(instance)
|
|
907
|
+
|
|
908
|
+
// If workflow was interrupted after A completed, it should resume from B
|
|
909
|
+
expect(result.stepsSkipped.length).toBeGreaterThan(0)
|
|
910
|
+
expect(result.stepsExecuted).not.toContain(result.stepsSkipped[0])
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
it('should not re-execute completed steps on restart', async () => {
|
|
914
|
+
const executionCounts: Record<string, number> = {}
|
|
915
|
+
|
|
916
|
+
const builder = WorkflowBuilder.create('no-reexec')
|
|
917
|
+
.step('A', async () => {
|
|
918
|
+
executionCounts.A = (executionCounts.A || 0) + 1
|
|
919
|
+
return { a: 1 }
|
|
920
|
+
})
|
|
921
|
+
.step('B', async () => {
|
|
922
|
+
executionCounts.B = (executionCounts.B || 0) + 1
|
|
923
|
+
return { b: 2 }
|
|
924
|
+
})
|
|
925
|
+
.dependsOn('A')
|
|
926
|
+
|
|
927
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
928
|
+
const executor = new TopologicalExecutor(graph, { enableCheckpoints: true })
|
|
929
|
+
|
|
930
|
+
// Simulate execution and checkpoint
|
|
931
|
+
await executor.run({})
|
|
932
|
+
|
|
933
|
+
// If A was checkpointed and we restart, A should not run again
|
|
934
|
+
// This test verifies the checkpoint behavior
|
|
935
|
+
expect(executionCounts.A).toBe(1)
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
it('should provide state snapshot for inspection', async () => {
|
|
939
|
+
const builder = WorkflowBuilder.create('snapshot-test')
|
|
940
|
+
.step('A', async () => ({ a: 1 }))
|
|
941
|
+
.step('B', async () => ({ b: 2 }))
|
|
942
|
+
.dependsOn('A')
|
|
943
|
+
|
|
944
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
945
|
+
const executor = new TopologicalExecutor(graph)
|
|
946
|
+
|
|
947
|
+
await executor.run({})
|
|
948
|
+
|
|
949
|
+
const snapshot = executor.getStateSnapshot()
|
|
950
|
+
|
|
951
|
+
expect(snapshot).toBeDefined()
|
|
952
|
+
expect(snapshot.completedSteps).toContain('A')
|
|
953
|
+
expect(snapshot.completedSteps).toContain('B')
|
|
954
|
+
expect(snapshot.results.A).toEqual({ a: 1 })
|
|
955
|
+
expect(snapshot.results.B).toEqual({ b: 2 })
|
|
956
|
+
})
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
// ============================================================================
|
|
960
|
+
// 10. TopologicalExecutor - Partial Failures Allow Successful Branches
|
|
961
|
+
// ============================================================================
|
|
962
|
+
|
|
963
|
+
describe('TopologicalExecutor - partial failure handling', () => {
|
|
964
|
+
it('should continue successful branches when continueOnError is enabled', async () => {
|
|
965
|
+
const executed: string[] = []
|
|
966
|
+
|
|
967
|
+
const builder = WorkflowBuilder.create('partial-failure')
|
|
968
|
+
.step('root', async () => {
|
|
969
|
+
executed.push('root')
|
|
970
|
+
return { root: true }
|
|
971
|
+
})
|
|
972
|
+
.step('left-fail', async () => {
|
|
973
|
+
executed.push('left-fail')
|
|
974
|
+
throw new Error('Left failed')
|
|
975
|
+
})
|
|
976
|
+
.dependsOn('root')
|
|
977
|
+
.step('left-child', async () => {
|
|
978
|
+
executed.push('left-child')
|
|
979
|
+
return { leftChild: true }
|
|
980
|
+
})
|
|
981
|
+
.dependsOn('left-fail')
|
|
982
|
+
.step('right-success', async () => {
|
|
983
|
+
executed.push('right-success')
|
|
984
|
+
return { right: true }
|
|
985
|
+
})
|
|
986
|
+
.dependsOn('root')
|
|
987
|
+
.step('right-child', async () => {
|
|
988
|
+
executed.push('right-child')
|
|
989
|
+
return { rightChild: true }
|
|
990
|
+
})
|
|
991
|
+
.dependsOn('right-success')
|
|
992
|
+
|
|
993
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
994
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
995
|
+
|
|
996
|
+
const result = await executor.run({})
|
|
997
|
+
|
|
998
|
+
// Left branch should fail, right branch should succeed
|
|
999
|
+
expect(executed).toContain('root')
|
|
1000
|
+
expect(executed).toContain('left-fail')
|
|
1001
|
+
expect(executed).not.toContain('left-child')
|
|
1002
|
+
expect(executed).toContain('right-success')
|
|
1003
|
+
expect(executed).toContain('right-child')
|
|
1004
|
+
|
|
1005
|
+
expect(result.partialResults).toBeDefined()
|
|
1006
|
+
expect(result.partialResults.right_success).toBeDefined()
|
|
1007
|
+
expect(result.partialResults.right_child).toBeDefined()
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
it('should report partial success with failed branches', async () => {
|
|
1011
|
+
const builder = WorkflowBuilder.create('mixed-results')
|
|
1012
|
+
.step('success1', async () => ({ s1: true }))
|
|
1013
|
+
.step('fail1', async () => {
|
|
1014
|
+
throw new Error('Fail 1')
|
|
1015
|
+
})
|
|
1016
|
+
.step('success2', async () => ({ s2: true }))
|
|
1017
|
+
|
|
1018
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1019
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
1020
|
+
|
|
1021
|
+
const result = await executor.run({})
|
|
1022
|
+
|
|
1023
|
+
expect(result.status).toBe('partial')
|
|
1024
|
+
expect(result.succeeded).toContain('success1')
|
|
1025
|
+
expect(result.succeeded).toContain('success2')
|
|
1026
|
+
expect(result.failed).toContain('fail1')
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
it('should provide partial results even when some branches fail', async () => {
|
|
1030
|
+
const builder = WorkflowBuilder.create('partial-results')
|
|
1031
|
+
.step('A', async () => ({ a: 'success' }))
|
|
1032
|
+
.step('B', async () => {
|
|
1033
|
+
throw new Error('B failed')
|
|
1034
|
+
})
|
|
1035
|
+
.step('C', async () => ({ c: 'success' }))
|
|
1036
|
+
|
|
1037
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1038
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
1039
|
+
|
|
1040
|
+
const result = await executor.run({})
|
|
1041
|
+
|
|
1042
|
+
expect(result.A).toEqual({ a: 'success' })
|
|
1043
|
+
expect(result.C).toEqual({ c: 'success' })
|
|
1044
|
+
expect(result.B).toBeUndefined()
|
|
1045
|
+
expect(result.errors.B).toBeDefined()
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it('should track dependency chains affected by failures', async () => {
|
|
1049
|
+
const builder = WorkflowBuilder.create('chain-tracking')
|
|
1050
|
+
.step('root', async () => ({ root: true }))
|
|
1051
|
+
.step('middle', async () => {
|
|
1052
|
+
throw new Error('Middle failed')
|
|
1053
|
+
})
|
|
1054
|
+
.dependsOn('root')
|
|
1055
|
+
.step('leaf1', async () => ({ leaf1: true }))
|
|
1056
|
+
.dependsOn('middle')
|
|
1057
|
+
.step('leaf2', async () => ({ leaf2: true }))
|
|
1058
|
+
.dependsOn('middle')
|
|
1059
|
+
|
|
1060
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1061
|
+
const executor = new TopologicalExecutor(graph, { continueOnError: true })
|
|
1062
|
+
|
|
1063
|
+
const result = await executor.run({})
|
|
1064
|
+
|
|
1065
|
+
expect(result.affectedChains).toBeDefined()
|
|
1066
|
+
expect(result.affectedChains).toContainEqual(['root', 'middle', 'leaf1'])
|
|
1067
|
+
expect(result.affectedChains).toContainEqual(['root', 'middle', 'leaf2'])
|
|
1068
|
+
})
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
// ============================================================================
|
|
1072
|
+
// 11. TopologicalExecutor - Execution Plan and Introspection
|
|
1073
|
+
// ============================================================================
|
|
1074
|
+
|
|
1075
|
+
describe('TopologicalExecutor - execution plan', () => {
|
|
1076
|
+
it('should provide execution plan before running', () => {
|
|
1077
|
+
const builder = WorkflowBuilder.create('plan-test')
|
|
1078
|
+
.step('A', stepA)
|
|
1079
|
+
.step('B', stepB)
|
|
1080
|
+
.dependsOn('A')
|
|
1081
|
+
.step('C', stepC)
|
|
1082
|
+
.dependsOn('A')
|
|
1083
|
+
.step('D', stepD)
|
|
1084
|
+
.dependsOn('B', 'C')
|
|
1085
|
+
|
|
1086
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1087
|
+
const executor = new TopologicalExecutor(graph)
|
|
1088
|
+
|
|
1089
|
+
const plan = executor.getExecutionPlan()
|
|
1090
|
+
|
|
1091
|
+
expect(plan.levels).toHaveLength(3)
|
|
1092
|
+
expect(plan.levels[0].steps).toContain('A')
|
|
1093
|
+
expect(plan.levels[1].steps).toContain('B')
|
|
1094
|
+
expect(plan.levels[1].steps).toContain('C')
|
|
1095
|
+
expect(plan.levels[2].steps).toContain('D')
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
it('should provide total step count', () => {
|
|
1099
|
+
const builder = WorkflowBuilder.create('count-test')
|
|
1100
|
+
.step('A', stepA)
|
|
1101
|
+
.step('B', stepB)
|
|
1102
|
+
.step('C', stepC)
|
|
1103
|
+
|
|
1104
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1105
|
+
const executor = new TopologicalExecutor(graph)
|
|
1106
|
+
|
|
1107
|
+
expect(executor.getStepCount()).toBe(3)
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
it('should provide maximum parallelism level', () => {
|
|
1111
|
+
const builder = WorkflowBuilder.create('parallelism-test')
|
|
1112
|
+
.step('root', async () => ({ root: true }))
|
|
1113
|
+
.step('A', stepA)
|
|
1114
|
+
.dependsOn('root')
|
|
1115
|
+
.step('B', stepB)
|
|
1116
|
+
.dependsOn('root')
|
|
1117
|
+
.step('C', stepC)
|
|
1118
|
+
.dependsOn('root')
|
|
1119
|
+
.step('D', stepD)
|
|
1120
|
+
.dependsOn('A', 'B', 'C')
|
|
1121
|
+
|
|
1122
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1123
|
+
const executor = new TopologicalExecutor(graph)
|
|
1124
|
+
|
|
1125
|
+
// Level 1 has 3 parallel steps (A, B, C)
|
|
1126
|
+
expect(executor.getMaxParallelism()).toBe(3)
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
it('should provide critical path information', () => {
|
|
1130
|
+
const builder = WorkflowBuilder.create('critical-path')
|
|
1131
|
+
.step('A', stepA) // Level 0
|
|
1132
|
+
.step('B', stepB) // Level 0
|
|
1133
|
+
.step('C', stepC) // Level 1
|
|
1134
|
+
.dependsOn('A')
|
|
1135
|
+
.step('D', stepD) // Level 2
|
|
1136
|
+
.dependsOn('C')
|
|
1137
|
+
|
|
1138
|
+
const graph = DurableGraph.fromBuilder(builder)
|
|
1139
|
+
const executor = new TopologicalExecutor(graph)
|
|
1140
|
+
|
|
1141
|
+
const criticalPath = executor.getCriticalPath()
|
|
1142
|
+
|
|
1143
|
+
// Longest path: A -> C -> D (3 levels)
|
|
1144
|
+
expect(criticalPath).toEqual(['A', 'C', 'D'])
|
|
1145
|
+
})
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
// ============================================================================
|
|
1149
|
+
// 12. Integration with Real Cloudflare Workflows
|
|
1150
|
+
// ============================================================================
|
|
1151
|
+
|
|
1152
|
+
describe('TopologicalExecutor with Real Cloudflare Workflows', () => {
|
|
1153
|
+
it('should execute graph via real workflow binding', async () => {
|
|
1154
|
+
const instance = await getWorkflowInstance('real-workflow-exec-1')
|
|
1155
|
+
|
|
1156
|
+
const result = await runWorkflow<{
|
|
1157
|
+
finalResult: unknown
|
|
1158
|
+
executionComplete: boolean
|
|
1159
|
+
}>(instance)
|
|
1160
|
+
|
|
1161
|
+
expect(result.executionComplete).toBe(true)
|
|
1162
|
+
expect(result.finalResult).toBeDefined()
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
it('should maintain durability guarantees across restarts', async () => {
|
|
1166
|
+
const instance = await getWorkflowInstance('durability-test-1')
|
|
1167
|
+
|
|
1168
|
+
const result = await runWorkflow<{
|
|
1169
|
+
checkpoints: number
|
|
1170
|
+
recoveredState: boolean
|
|
1171
|
+
}>(instance)
|
|
1172
|
+
|
|
1173
|
+
expect(result.checkpoints).toBeGreaterThan(0)
|
|
1174
|
+
expect(result.recoveredState).toBe(true)
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
it('should integrate with DurableStep for individual step durability', async () => {
|
|
1178
|
+
const instance = await getWorkflowInstance('durable-step-integration-1')
|
|
1179
|
+
|
|
1180
|
+
const result = await runWorkflow<{
|
|
1181
|
+
stepsDurable: boolean
|
|
1182
|
+
allStepsCheckpointed: boolean
|
|
1183
|
+
}>(instance)
|
|
1184
|
+
|
|
1185
|
+
expect(result.stepsDurable).toBe(true)
|
|
1186
|
+
expect(result.allStepsCheckpointed).toBe(true)
|
|
1187
|
+
})
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
// ============================================================================
|
|
1191
|
+
// 13. Type Definitions (Compile-time)
|
|
1192
|
+
// ============================================================================
|
|
1193
|
+
|
|
1194
|
+
describe('TopologicalExecutor types', () => {
|
|
1195
|
+
it('should define ExecutionPlan type', () => {
|
|
1196
|
+
const plan: ExecutionPlan = {
|
|
1197
|
+
levels: [
|
|
1198
|
+
{ level: 0, steps: ['A', 'B'] },
|
|
1199
|
+
{ level: 1, steps: ['C'] },
|
|
1200
|
+
],
|
|
1201
|
+
totalSteps: 3,
|
|
1202
|
+
maxParallelism: 2,
|
|
1203
|
+
criticalPath: ['A', 'C'],
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
expect(plan.levels).toHaveLength(2)
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
it('should define ExecutionResult type', () => {
|
|
1210
|
+
const result: ExecutionResult<{
|
|
1211
|
+
A: { a: number }
|
|
1212
|
+
B: { b: number }
|
|
1213
|
+
}> = {
|
|
1214
|
+
A: { a: 1 },
|
|
1215
|
+
B: { b: 2 },
|
|
1216
|
+
_meta: {
|
|
1217
|
+
levels: [{ level: 0, duration: 50, steps: ['A', 'B'] }],
|
|
1218
|
+
totalDuration: 50,
|
|
1219
|
+
status: 'complete',
|
|
1220
|
+
},
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
expect(result.A.a).toBe(1)
|
|
1224
|
+
expect(result._meta.status).toBe('complete')
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
it('should define StepExecutionResult type', () => {
|
|
1228
|
+
const stepResult: StepExecutionResult = {
|
|
1229
|
+
stepId: 'A',
|
|
1230
|
+
status: 'complete',
|
|
1231
|
+
result: { value: 42 },
|
|
1232
|
+
startTime: Date.now() - 100,
|
|
1233
|
+
endTime: Date.now(),
|
|
1234
|
+
duration: 100,
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
expect(stepResult.status).toBe('complete')
|
|
1238
|
+
expect(stepResult.duration).toBe(100)
|
|
1239
|
+
})
|
|
1240
|
+
})
|