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.
Files changed (211) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -1
  3. package/README.md +305 -184
  4. package/dist/barrier.d.ts +159 -0
  5. package/dist/barrier.d.ts.map +1 -0
  6. package/dist/barrier.js +377 -0
  7. package/dist/barrier.js.map +1 -0
  8. package/dist/cascade-context.d.ts +149 -0
  9. package/dist/cascade-context.d.ts.map +1 -0
  10. package/dist/cascade-context.js +324 -0
  11. package/dist/cascade-context.js.map +1 -0
  12. package/dist/cascade-executor.d.ts +196 -0
  13. package/dist/cascade-executor.d.ts.map +1 -0
  14. package/dist/cascade-executor.js +384 -0
  15. package/dist/cascade-executor.js.map +1 -0
  16. package/dist/context.d.ts.map +1 -1
  17. package/dist/context.js +27 -8
  18. package/dist/context.js.map +1 -1
  19. package/dist/cron-parser.d.ts +65 -0
  20. package/dist/cron-parser.d.ts.map +1 -0
  21. package/dist/cron-parser.js +294 -0
  22. package/dist/cron-parser.js.map +1 -0
  23. package/dist/cron-scheduler.d.ts +117 -0
  24. package/dist/cron-scheduler.d.ts.map +1 -0
  25. package/dist/cron-scheduler.js +176 -0
  26. package/dist/cron-scheduler.js.map +1 -0
  27. package/dist/database-context.d.ts +184 -0
  28. package/dist/database-context.d.ts.map +1 -0
  29. package/dist/database-context.js +428 -0
  30. package/dist/database-context.js.map +1 -0
  31. package/dist/dependency-graph.d.ts +157 -0
  32. package/dist/dependency-graph.d.ts.map +1 -0
  33. package/dist/dependency-graph.js +382 -0
  34. package/dist/dependency-graph.js.map +1 -0
  35. package/dist/digital-objects-adapter.d.ts +159 -0
  36. package/dist/digital-objects-adapter.d.ts.map +1 -0
  37. package/dist/digital-objects-adapter.js +229 -0
  38. package/dist/digital-objects-adapter.js.map +1 -0
  39. package/dist/durable-execution-cloudflare.d.ts +427 -0
  40. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  41. package/dist/durable-execution-cloudflare.js +510 -0
  42. package/dist/durable-execution-cloudflare.js.map +1 -0
  43. package/dist/durable-execution.d.ts +482 -0
  44. package/dist/durable-execution.d.ts.map +1 -0
  45. package/dist/durable-execution.js +594 -0
  46. package/dist/durable-execution.js.map +1 -0
  47. package/dist/durable-workflow.d.ts +176 -0
  48. package/dist/durable-workflow.d.ts.map +1 -0
  49. package/dist/durable-workflow.js +552 -0
  50. package/dist/durable-workflow.js.map +1 -0
  51. package/dist/every.d.ts +31 -2
  52. package/dist/every.d.ts.map +1 -1
  53. package/dist/every.js +63 -32
  54. package/dist/every.js.map +1 -1
  55. package/dist/graph/index.d.ts +8 -0
  56. package/dist/graph/index.d.ts.map +1 -0
  57. package/dist/graph/index.js +8 -0
  58. package/dist/graph/index.js.map +1 -0
  59. package/dist/graph/topological-sort.d.ts +121 -0
  60. package/dist/graph/topological-sort.d.ts.map +1 -0
  61. package/dist/graph/topological-sort.js +292 -0
  62. package/dist/graph/topological-sort.js.map +1 -0
  63. package/dist/index.d.ts +10 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +25 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/logger.d.ts +101 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +115 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/on.d.ts +35 -10
  72. package/dist/on.d.ts.map +1 -1
  73. package/dist/on.js +53 -19
  74. package/dist/on.js.map +1 -1
  75. package/dist/runtime.d.ts +169 -0
  76. package/dist/runtime.d.ts.map +1 -0
  77. package/dist/runtime.js +275 -0
  78. package/dist/runtime.js.map +1 -0
  79. package/dist/send.d.ts.map +1 -1
  80. package/dist/send.js +4 -3
  81. package/dist/send.js.map +1 -1
  82. package/dist/telemetry.d.ts +150 -0
  83. package/dist/telemetry.d.ts.map +1 -0
  84. package/dist/telemetry.js +388 -0
  85. package/dist/telemetry.js.map +1 -0
  86. package/dist/timer-registry.d.ts +77 -0
  87. package/dist/timer-registry.d.ts.map +1 -0
  88. package/dist/timer-registry.js +154 -0
  89. package/dist/timer-registry.js.map +1 -0
  90. package/dist/types.d.ts +105 -6
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/types.js +17 -1
  93. package/dist/types.js.map +1 -1
  94. package/dist/worker/durable-step.d.ts +481 -0
  95. package/dist/worker/durable-step.d.ts.map +1 -0
  96. package/dist/worker/durable-step.js +606 -0
  97. package/dist/worker/durable-step.js.map +1 -0
  98. package/dist/worker/index.d.ts +106 -0
  99. package/dist/worker/index.d.ts.map +1 -0
  100. package/dist/worker/index.js +124 -0
  101. package/dist/worker/index.js.map +1 -0
  102. package/dist/worker/state-adapter.d.ts +230 -0
  103. package/dist/worker/state-adapter.d.ts.map +1 -0
  104. package/dist/worker/state-adapter.js +409 -0
  105. package/dist/worker/state-adapter.js.map +1 -0
  106. package/dist/worker/topological-executor.d.ts +282 -0
  107. package/dist/worker/topological-executor.d.ts.map +1 -0
  108. package/dist/worker/topological-executor.js +396 -0
  109. package/dist/worker/topological-executor.js.map +1 -0
  110. package/dist/worker/workflow-builder.d.ts +286 -0
  111. package/dist/worker/workflow-builder.d.ts.map +1 -0
  112. package/dist/worker/workflow-builder.js +565 -0
  113. package/dist/worker/workflow-builder.js.map +1 -0
  114. package/dist/worker.d.ts +800 -0
  115. package/dist/worker.d.ts.map +1 -0
  116. package/dist/worker.js +2428 -0
  117. package/dist/worker.js.map +1 -0
  118. package/dist/workflow-builder.d.ts +287 -0
  119. package/dist/workflow-builder.d.ts.map +1 -0
  120. package/dist/workflow-builder.js +762 -0
  121. package/dist/workflow-builder.js.map +1 -0
  122. package/dist/workflow.d.ts +14 -30
  123. package/dist/workflow.d.ts.map +1 -1
  124. package/dist/workflow.js +136 -292
  125. package/dist/workflow.js.map +1 -1
  126. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  127. package/examples/02-content-moderation-cascade.ts +454 -0
  128. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  129. package/examples/04-database-persistence.ts +518 -0
  130. package/examples/README.md +173 -0
  131. package/package.json +21 -4
  132. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  133. package/src/__tests__/durable-workflow.test.ts +297 -0
  134. package/src/barrier.ts +507 -0
  135. package/src/cascade-context.ts +495 -0
  136. package/src/cascade-executor.ts +588 -0
  137. package/src/context.ts +51 -17
  138. package/src/cron-parser.ts +347 -0
  139. package/src/cron-scheduler.ts +239 -0
  140. package/src/database-context.ts +658 -0
  141. package/src/dependency-graph.ts +518 -0
  142. package/src/digital-objects-adapter.ts +351 -0
  143. package/src/durable-execution-cloudflare.ts +855 -0
  144. package/src/durable-execution.ts +1042 -0
  145. package/src/durable-workflow.ts +717 -0
  146. package/src/every.ts +104 -35
  147. package/src/graph/index.ts +19 -0
  148. package/src/graph/topological-sort.ts +412 -0
  149. package/src/index.ts +147 -0
  150. package/src/logger.ts +148 -0
  151. package/src/on.ts +81 -26
  152. package/src/runtime.ts +436 -0
  153. package/src/send.ts +4 -5
  154. package/src/telemetry.ts +577 -0
  155. package/src/timer-registry.ts +179 -0
  156. package/src/types.ts +146 -10
  157. package/src/worker/durable-step.ts +976 -0
  158. package/src/worker/index.ts +216 -0
  159. package/src/worker/state-adapter.ts +589 -0
  160. package/src/worker/topological-executor.ts +625 -0
  161. package/src/worker/workflow-builder.ts +871 -0
  162. package/src/worker.ts +2906 -0
  163. package/src/workflow-builder.ts +1068 -0
  164. package/src/workflow.ts +199 -355
  165. package/test/barrier-join.test.ts +442 -0
  166. package/test/barrier-unhandled-rejections.test.ts +359 -0
  167. package/test/cascade-context.test.ts +390 -0
  168. package/test/cascade-executor.test.ts +852 -0
  169. package/test/cron-parser.test.ts +314 -0
  170. package/test/cron-scheduler.test.ts +291 -0
  171. package/test/database-context.test.ts +770 -0
  172. package/test/db-provider-adapter.test.ts +862 -0
  173. package/test/dependency-graph.test.ts +512 -0
  174. package/test/durable-execution-cloudflare.test.ts +606 -0
  175. package/test/durable-execution-in-process.test.ts +286 -0
  176. package/test/durable-execution.test.ts +247 -0
  177. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  178. package/test/graph/topological-sort.test.ts +586 -0
  179. package/test/integration.test.ts +442 -0
  180. package/test/rpc-surface.test.ts +946 -0
  181. package/test/runtime.test.ts +262 -0
  182. package/test/schedule-timer-cleanup.test.ts +353 -0
  183. package/test/send-race-conditions.test.ts +400 -0
  184. package/test/type-safety-every.test.ts +303 -0
  185. package/test/worker/durable-cascade.test.ts +1117 -0
  186. package/test/worker/durable-step.test.ts +723 -0
  187. package/test/worker/topological-executor.test.ts +1240 -0
  188. package/test/worker/workflow-builder.test.ts +1067 -0
  189. package/test/worker.test.ts +608 -0
  190. package/test/workflow-builder.test.ts +1670 -0
  191. package/test/workflow-cron.test.ts +256 -0
  192. package/test/workflow-state-adapter.test.ts +923 -0
  193. package/test/workflow.test.ts +25 -22
  194. package/tsconfig.json +3 -1
  195. package/vitest.config.ts +38 -1
  196. package/vitest.workers.config.ts +44 -0
  197. package/wrangler.jsonc +22 -0
  198. package/.turbo/turbo-test.log +0 -7
  199. package/src/context.js +0 -83
  200. package/src/every.js +0 -267
  201. package/src/index.js +0 -71
  202. package/src/on.js +0 -79
  203. package/src/send.js +0 -111
  204. package/src/types.js +0 -4
  205. package/src/workflow.js +0 -455
  206. package/test/context.test.js +0 -116
  207. package/test/every.test.js +0 -282
  208. package/test/on.test.js +0 -80
  209. package/test/send.test.js +0 -89
  210. package/test/workflow.test.js +0 -224
  211. 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
+ })