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,1067 @@
1
+ /**
2
+ * WorkflowBuilder DSL Tests (RED Phase)
3
+ *
4
+ * Tests for WorkflowBuilder - a declarative DSL for building durable workflows
5
+ * using a fluent builder pattern with step dependencies, event triggers,
6
+ * and scheduled execution.
7
+ *
8
+ * These tests define the expected behavior for WorkflowBuilder DSL before implementation.
9
+ * All tests SHOULD FAIL because WorkflowBuilder does not exist yet.
10
+ *
11
+ * Uses @cloudflare/vitest-pool-workers - NO MOCKS.
12
+ * Tests run against real Cloudflare Workflows bindings.
13
+ *
14
+ * Bead: aip-llm1
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ import { describe, it, expect, beforeEach } from 'vitest'
20
+ import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'
21
+
22
+ // ============================================================================
23
+ // These imports will FAIL because WorkflowBuilder does not exist yet.
24
+ // This is the RED phase of TDD.
25
+ // ============================================================================
26
+ import {
27
+ WorkflowBuilder,
28
+ type WorkflowBuilderConfig,
29
+ type StepDefinition,
30
+ type StepChain,
31
+ type TriggerConfig,
32
+ type ScheduleConfig,
33
+ type BuiltWorkflow,
34
+ } from '../../src/worker/workflow-builder.js'
35
+
36
+ // Import DurableStep for use in step definitions
37
+ import { DurableStep, type StepConfig } from '../../src/worker/durable-step.js'
38
+
39
+ // Import WorkflowService for integration tests
40
+ import { WorkflowServiceCore } from '../../src/worker.js'
41
+
42
+ // ============================================================================
43
+ // Type Definitions for Test Environment
44
+ // ============================================================================
45
+
46
+ interface TestEnv {
47
+ WORKFLOW: Workflow
48
+ }
49
+
50
+ // ============================================================================
51
+ // Test Data - Sample Step Functions
52
+ // ============================================================================
53
+
54
+ const validateOrder = async (input: { orderId: string }) => {
55
+ return { valid: true, orderId: input.orderId }
56
+ }
57
+
58
+ const chargePayment = async (input: { orderId: string; amount: number }) => {
59
+ return { charged: true, transactionId: `txn_${input.orderId}` }
60
+ }
61
+
62
+ const fulfillOrder = async (input: { orderId: string; transactionId: string }) => {
63
+ return { fulfilled: true, trackingNumber: `track_${input.orderId}` }
64
+ }
65
+
66
+ const sendNotification = async (input: { to: string; message: string }) => {
67
+ return { sent: true }
68
+ }
69
+
70
+ const cleanupData = async () => {
71
+ return { cleaned: true }
72
+ }
73
+
74
+ // ============================================================================
75
+ // 1. WorkflowBuilder.create() - Creating Workflow Definitions
76
+ // ============================================================================
77
+
78
+ describe('WorkflowBuilder.create()', () => {
79
+ it('creates a new workflow builder with a name', () => {
80
+ const builder = WorkflowBuilder.create('order-process')
81
+
82
+ expect(builder).toBeDefined()
83
+ expect(builder.name).toBe('order-process')
84
+ })
85
+
86
+ it('creates a new workflow builder with name and config', () => {
87
+ const builder = WorkflowBuilder.create('payment-flow', {
88
+ description: 'Handles payment processing',
89
+ version: '1.0.0',
90
+ })
91
+
92
+ expect(builder).toBeDefined()
93
+ expect(builder.name).toBe('payment-flow')
94
+ expect(builder.config?.description).toBe('Handles payment processing')
95
+ expect(builder.config?.version).toBe('1.0.0')
96
+ })
97
+
98
+ it('returns a WorkflowBuilder instance', () => {
99
+ const builder = WorkflowBuilder.create('test-workflow')
100
+
101
+ // Should be an instance of WorkflowBuilder
102
+ expect(builder).toBeInstanceOf(WorkflowBuilder)
103
+ })
104
+
105
+ it('allows method chaining', () => {
106
+ // The builder pattern should support fluent method chaining
107
+ const builder = WorkflowBuilder.create('chainable-workflow')
108
+
109
+ // step() should return something chainable
110
+ expect(typeof builder.step).toBe('function')
111
+ })
112
+
113
+ it('validates workflow name is provided', () => {
114
+ // Empty name should throw
115
+ expect(() => WorkflowBuilder.create('')).toThrow()
116
+ })
117
+
118
+ it('accepts optional timeout configuration', () => {
119
+ const builder = WorkflowBuilder.create('timed-workflow', {
120
+ timeout: '5 minutes',
121
+ })
122
+
123
+ expect(builder.config?.timeout).toBe('5 minutes')
124
+ })
125
+
126
+ it('accepts optional retry configuration', () => {
127
+ const builder = WorkflowBuilder.create('retry-workflow', {
128
+ retries: {
129
+ limit: 3,
130
+ delay: '1 second',
131
+ backoff: 'exponential',
132
+ },
133
+ })
134
+
135
+ expect(builder.config?.retries?.limit).toBe(3)
136
+ })
137
+ })
138
+
139
+ // ============================================================================
140
+ // 2. WorkflowBuilder.step() - Adding Durable Steps
141
+ // ============================================================================
142
+
143
+ describe('WorkflowBuilder.step()', () => {
144
+ let builder: ReturnType<typeof WorkflowBuilder.create>
145
+
146
+ beforeEach(() => {
147
+ builder = WorkflowBuilder.create('test-workflow')
148
+ })
149
+
150
+ it('adds a step with name and function', () => {
151
+ const result = builder.step('validate', validateOrder)
152
+
153
+ expect(result).toBeDefined()
154
+ // Should return something with dependsOn for chaining
155
+ expect(typeof result.dependsOn).toBe('function')
156
+ })
157
+
158
+ it('adds a step with name, config, and function', () => {
159
+ const result = builder.step(
160
+ 'charge',
161
+ {
162
+ retries: { limit: 3, delay: '1 second' },
163
+ timeout: '30 seconds',
164
+ },
165
+ chargePayment
166
+ )
167
+
168
+ expect(result).toBeDefined()
169
+ })
170
+
171
+ it('adds a step from a DurableStep instance', () => {
172
+ const durableStep = new DurableStep('fulfill', fulfillOrder)
173
+
174
+ const result = builder.step(durableStep)
175
+
176
+ expect(result).toBeDefined()
177
+ })
178
+
179
+ it('allows multiple steps to be added', () => {
180
+ builder.step('step1', validateOrder)
181
+ builder.step('step2', chargePayment)
182
+ const result = builder.step('step3', fulfillOrder)
183
+
184
+ expect(result).toBeDefined()
185
+ // After build, should have all 3 steps
186
+ })
187
+
188
+ it('preserves step order', () => {
189
+ builder.step('first', validateOrder)
190
+ builder.step('second', chargePayment)
191
+ builder.step('third', fulfillOrder)
192
+
193
+ const workflow = builder.build()
194
+
195
+ expect(workflow.steps).toHaveLength(3)
196
+ expect(workflow.steps[0]?.name).toBe('first')
197
+ expect(workflow.steps[1]?.name).toBe('second')
198
+ expect(workflow.steps[2]?.name).toBe('third')
199
+ })
200
+
201
+ it('rejects duplicate step names', () => {
202
+ builder.step('unique', validateOrder)
203
+
204
+ expect(() => builder.step('unique', chargePayment)).toThrow()
205
+ })
206
+
207
+ it('returns a StepChain for further configuration', () => {
208
+ const stepChain = builder.step('configurable', validateOrder)
209
+
210
+ // StepChain should have dependsOn, timeout, retries methods
211
+ expect(typeof stepChain.dependsOn).toBe('function')
212
+ expect(typeof stepChain.timeout).toBe('function')
213
+ expect(typeof stepChain.retries).toBe('function')
214
+ })
215
+
216
+ it('step chain allows setting timeout', () => {
217
+ const stepChain = builder.step('timed-step', validateOrder).timeout('30 seconds')
218
+
219
+ expect(stepChain).toBeDefined()
220
+ })
221
+
222
+ it('step chain allows setting retries', () => {
223
+ const stepChain = builder
224
+ .step('retry-step', validateOrder)
225
+ .retries({ limit: 3, delay: '1 second', backoff: 'exponential' })
226
+
227
+ expect(stepChain).toBeDefined()
228
+ })
229
+ })
230
+
231
+ // ============================================================================
232
+ // 3. WorkflowBuilder.step().dependsOn() - Declaring Dependencies
233
+ // ============================================================================
234
+
235
+ describe('WorkflowBuilder.step().dependsOn()', () => {
236
+ let builder: ReturnType<typeof WorkflowBuilder.create>
237
+
238
+ beforeEach(() => {
239
+ builder = WorkflowBuilder.create('dependency-workflow')
240
+ })
241
+
242
+ it('declares a single dependency', () => {
243
+ builder.step('validate', validateOrder)
244
+ const result = builder.step('charge', chargePayment).dependsOn('validate')
245
+
246
+ expect(result).toBeDefined()
247
+ })
248
+
249
+ it('declares multiple dependencies', () => {
250
+ builder.step('validate', validateOrder)
251
+ builder.step('check-inventory', async () => ({ available: true }))
252
+ const result = builder.step('charge', chargePayment).dependsOn('validate', 'check-inventory')
253
+
254
+ expect(result).toBeDefined()
255
+ })
256
+
257
+ it('declares dependencies with array syntax', () => {
258
+ builder.step('validate', validateOrder)
259
+ builder.step('check-inventory', async () => ({ available: true }))
260
+ const result = builder.step('charge', chargePayment).dependsOn(['validate', 'check-inventory'])
261
+
262
+ expect(result).toBeDefined()
263
+ })
264
+
265
+ it('chains multiple dependsOn calls', () => {
266
+ builder.step('step1', validateOrder)
267
+ builder.step('step2', async () => ({ done: true }))
268
+ const result = builder.step('step3', chargePayment).dependsOn('step1').dependsOn('step2')
269
+
270
+ expect(result).toBeDefined()
271
+ })
272
+
273
+ it('validates that dependencies exist', () => {
274
+ // Referencing non-existent step should throw on build
275
+ builder.step('charge', chargePayment).dependsOn('non-existent')
276
+
277
+ expect(() => builder.build()).toThrow(/dependency.*non-existent.*not found/i)
278
+ })
279
+
280
+ it('detects circular dependencies on build', () => {
281
+ // This creates a cycle: A -> B -> A
282
+ builder.step('stepA', validateOrder).dependsOn('stepB')
283
+ builder.step('stepB', chargePayment).dependsOn('stepA')
284
+
285
+ expect(() => builder.build()).toThrow(/circular dependency/i)
286
+ })
287
+
288
+ it('allows soft dependencies (can proceed on failure)', () => {
289
+ builder.step('validate', validateOrder)
290
+ const result = builder.step('charge', chargePayment).dependsOn('validate', { type: 'soft' })
291
+
292
+ expect(result).toBeDefined()
293
+ })
294
+
295
+ it('dependency options include wait timeout', () => {
296
+ builder.step('validate', validateOrder)
297
+ const result = builder
298
+ .step('charge', chargePayment)
299
+ .dependsOn('validate', { timeout: '5 minutes' })
300
+
301
+ expect(result).toBeDefined()
302
+ })
303
+
304
+ it('supports step chain continuation after dependsOn', () => {
305
+ builder.step('validate', validateOrder)
306
+
307
+ // dependsOn should return builder for continuation
308
+ const result = builder
309
+ .step('charge', chargePayment)
310
+ .dependsOn('validate')
311
+ .step('fulfill', fulfillOrder)
312
+ .dependsOn('charge')
313
+
314
+ expect(result).toBeDefined()
315
+ })
316
+ })
317
+
318
+ // ============================================================================
319
+ // 4. WorkflowBuilder.on() - Event-Triggered Steps
320
+ // ============================================================================
321
+
322
+ describe('WorkflowBuilder.on()', () => {
323
+ let builder: ReturnType<typeof WorkflowBuilder.create>
324
+
325
+ beforeEach(() => {
326
+ builder = WorkflowBuilder.create('event-workflow')
327
+ })
328
+
329
+ it('registers an event trigger with .do()', () => {
330
+ builder.step('validate', validateOrder)
331
+ const result = builder.on('Order.placed').do('validate')
332
+
333
+ expect(result).toBeDefined()
334
+ })
335
+
336
+ it('registers event trigger for step with inline function', () => {
337
+ const result = builder.on('Order.placed').do(async (event: { orderId: string }) => {
338
+ return { processed: true, orderId: event.orderId }
339
+ })
340
+
341
+ expect(result).toBeDefined()
342
+ })
343
+
344
+ it('event names follow Noun.event format', () => {
345
+ builder.step('handle', validateOrder)
346
+ const result = builder.on('Customer.created').do('handle')
347
+
348
+ expect(result).toBeDefined()
349
+ })
350
+
351
+ it('validates event name format', () => {
352
+ builder.step('handle', validateOrder)
353
+
354
+ // Invalid event name should throw
355
+ expect(() => builder.on('invalid-event-name').do('handle')).toThrow()
356
+ })
357
+
358
+ it('multiple events can trigger the same step', () => {
359
+ builder.step('notify', sendNotification)
360
+ builder.on('Order.placed').do('notify')
361
+ const result = builder.on('Order.shipped').do('notify')
362
+
363
+ expect(result).toBeDefined()
364
+ })
365
+
366
+ it('same event can trigger multiple steps', () => {
367
+ builder.step('validate', validateOrder)
368
+ builder.step('notify', sendNotification)
369
+ builder.on('Order.placed').do('validate')
370
+ const result = builder.on('Order.placed').do('notify')
371
+
372
+ expect(result).toBeDefined()
373
+ })
374
+
375
+ it('returns trigger chain for configuration', () => {
376
+ builder.step('validate', validateOrder)
377
+ const chain = builder.on('Order.placed')
378
+
379
+ expect(typeof chain.do).toBe('function')
380
+ expect(typeof chain.filter).toBe('function')
381
+ })
382
+
383
+ it('supports event filtering', () => {
384
+ builder.step('validate', validateOrder)
385
+ const result = builder
386
+ .on('Order.placed')
387
+ .filter((event) => event.amount > 100)
388
+ .do('validate')
389
+
390
+ expect(result).toBeDefined()
391
+ })
392
+
393
+ it('validates that step exists when using string reference', () => {
394
+ // Reference to non-existent step should throw on build
395
+ builder.on('Order.placed').do('non-existent-step')
396
+
397
+ expect(() => builder.build()).toThrow(/step.*non-existent-step.*not found/i)
398
+ })
399
+
400
+ it('on() creates implicit step when given inline function', () => {
401
+ builder.on('Order.placed').do(async (event: { orderId: string }) => {
402
+ return { processed: true }
403
+ })
404
+
405
+ const workflow = builder.build()
406
+
407
+ // Should have created an implicit step
408
+ expect(workflow.steps.length).toBeGreaterThanOrEqual(1)
409
+ })
410
+
411
+ it('supports typed event payloads', () => {
412
+ interface OrderPlacedEvent {
413
+ orderId: string
414
+ amount: number
415
+ customerId: string
416
+ }
417
+
418
+ const result = builder.on<OrderPlacedEvent>('Order.placed').do(async (event) => {
419
+ // TypeScript should know event has orderId, amount, customerId
420
+ return { processed: true, orderId: event.orderId }
421
+ })
422
+
423
+ expect(result).toBeDefined()
424
+ })
425
+ })
426
+
427
+ // ============================================================================
428
+ // 5. WorkflowBuilder.every() - Scheduled Steps
429
+ // ============================================================================
430
+
431
+ describe('WorkflowBuilder.every()', () => {
432
+ let builder: ReturnType<typeof WorkflowBuilder.create>
433
+
434
+ beforeEach(() => {
435
+ builder = WorkflowBuilder.create('scheduled-workflow')
436
+ })
437
+
438
+ it('registers a scheduled trigger with .do()', () => {
439
+ builder.step('cleanup', cleanupData)
440
+ const result = builder.every('hour').do('cleanup')
441
+
442
+ expect(result).toBeDefined()
443
+ })
444
+
445
+ it('supports common schedule intervals', () => {
446
+ builder.step('task', cleanupData)
447
+
448
+ // All these should work
449
+ expect(() => builder.every('minute').do('task')).not.toThrow()
450
+ expect(() => builder.every('hour').do('task')).not.toThrow()
451
+ expect(() => builder.every('day').do('task')).not.toThrow()
452
+ expect(() => builder.every('week').do('task')).not.toThrow()
453
+ })
454
+
455
+ it('supports day-of-week schedules', () => {
456
+ builder.step('report', cleanupData)
457
+ const result = builder.every('Monday').do('report')
458
+
459
+ expect(result).toBeDefined()
460
+ })
461
+
462
+ it('supports day-of-week with time', () => {
463
+ builder.step('report', cleanupData)
464
+ const result = builder.every('Monday').at('9am').do('report')
465
+
466
+ expect(result).toBeDefined()
467
+ })
468
+
469
+ it('supports interval with value', () => {
470
+ builder.step('check', cleanupData)
471
+ const result = builder.every(5).minutes().do('check')
472
+
473
+ expect(result).toBeDefined()
474
+ })
475
+
476
+ it('supports natural language schedules', () => {
477
+ builder.step('report', cleanupData)
478
+ const result = builder.every('first Monday of the month').do('report')
479
+
480
+ expect(result).toBeDefined()
481
+ })
482
+
483
+ it('supports cron expressions', () => {
484
+ builder.step('task', cleanupData)
485
+ const result = builder.every('0 9 * * 1').do('task') // Every Monday at 9am
486
+
487
+ expect(result).toBeDefined()
488
+ })
489
+
490
+ it('supports inline functions', () => {
491
+ const result = builder.every('hour').do(async () => {
492
+ return { completed: true }
493
+ })
494
+
495
+ expect(result).toBeDefined()
496
+ })
497
+
498
+ it('validates that step exists when using string reference', () => {
499
+ builder.every('hour').do('non-existent-step')
500
+
501
+ expect(() => builder.build()).toThrow(/step.*non-existent-step.*not found/i)
502
+ })
503
+
504
+ it('returns schedule chain for configuration', () => {
505
+ builder.step('task', cleanupData)
506
+ const chain = builder.every('hour')
507
+
508
+ expect(typeof chain.do).toBe('function')
509
+ expect(typeof chain.at).toBe('function')
510
+ })
511
+
512
+ it('supports timezone configuration', () => {
513
+ builder.step('report', cleanupData)
514
+ const result = builder.every('day').at('9am').timezone('America/New_York').do('report')
515
+
516
+ expect(result).toBeDefined()
517
+ })
518
+ })
519
+
520
+ // ============================================================================
521
+ // 6. WorkflowBuilder.build() - Building the Workflow
522
+ // ============================================================================
523
+
524
+ describe('WorkflowBuilder.build()', () => {
525
+ let builder: ReturnType<typeof WorkflowBuilder.create>
526
+
527
+ beforeEach(() => {
528
+ builder = WorkflowBuilder.create('buildable-workflow')
529
+ })
530
+
531
+ it('returns a BuiltWorkflow object', () => {
532
+ builder.step('validate', validateOrder)
533
+ const workflow = builder.build()
534
+
535
+ expect(workflow).toBeDefined()
536
+ expect(workflow.name).toBe('buildable-workflow')
537
+ })
538
+
539
+ it('built workflow contains all registered steps', () => {
540
+ builder.step('step1', validateOrder)
541
+ builder.step('step2', chargePayment)
542
+ builder.step('step3', fulfillOrder)
543
+
544
+ const workflow = builder.build()
545
+
546
+ expect(workflow.steps).toHaveLength(3)
547
+ })
548
+
549
+ it('built workflow contains event triggers', () => {
550
+ builder.step('validate', validateOrder)
551
+ builder.on('Order.placed').do('validate')
552
+
553
+ const workflow = builder.build()
554
+
555
+ expect(workflow.triggers).toBeDefined()
556
+ expect(workflow.triggers.events).toHaveLength(1)
557
+ expect(workflow.triggers.events[0]?.event).toBe('Order.placed')
558
+ })
559
+
560
+ it('built workflow contains schedule triggers', () => {
561
+ builder.step('cleanup', cleanupData)
562
+ builder.every('hour').do('cleanup')
563
+
564
+ const workflow = builder.build()
565
+
566
+ expect(workflow.triggers).toBeDefined()
567
+ expect(workflow.triggers.schedules).toHaveLength(1)
568
+ })
569
+
570
+ it('built workflow includes dependency graph', () => {
571
+ builder.step('validate', validateOrder)
572
+ builder.step('charge', chargePayment).dependsOn('validate')
573
+ builder.step('fulfill', fulfillOrder).dependsOn('charge')
574
+
575
+ const workflow = builder.build()
576
+
577
+ expect(workflow.dependencyGraph).toBeDefined()
578
+ expect(workflow.dependencyGraph.get('charge')).toContain('validate')
579
+ expect(workflow.dependencyGraph.get('fulfill')).toContain('charge')
580
+ })
581
+
582
+ it('built workflow provides execution order', () => {
583
+ builder.step('validate', validateOrder)
584
+ builder.step('charge', chargePayment).dependsOn('validate')
585
+ builder.step('fulfill', fulfillOrder).dependsOn('charge')
586
+
587
+ const workflow = builder.build()
588
+
589
+ // Should provide topologically sorted execution order
590
+ expect(workflow.executionOrder).toEqual(['validate', 'charge', 'fulfill'])
591
+ })
592
+
593
+ it('built workflow is executable', () => {
594
+ builder.step('validate', validateOrder)
595
+ const workflow = builder.build()
596
+
597
+ // Should have an execute method
598
+ expect(typeof workflow.execute).toBe('function')
599
+ })
600
+
601
+ it('build validates the workflow definition', () => {
602
+ // Empty workflow with no steps should be valid (entry point can be event)
603
+ builder.on('Order.placed').do(async () => ({ done: true }))
604
+
605
+ expect(() => builder.build()).not.toThrow()
606
+ })
607
+
608
+ it('build throws on invalid workflow', () => {
609
+ // Referencing non-existent step in dependency
610
+ builder.step('validate', validateOrder).dependsOn('missing')
611
+
612
+ expect(() => builder.build()).toThrow()
613
+ })
614
+
615
+ it('returns immutable workflow definition', () => {
616
+ builder.step('validate', validateOrder)
617
+ const workflow = builder.build()
618
+
619
+ // Modifying the builder after build should not affect built workflow
620
+ builder.step('extra', chargePayment)
621
+ const newWorkflow = builder.build()
622
+
623
+ expect(workflow.steps).toHaveLength(1)
624
+ expect(newWorkflow.steps).toHaveLength(2)
625
+ })
626
+
627
+ it('build includes workflow metadata', () => {
628
+ const builder = WorkflowBuilder.create('metadata-workflow', {
629
+ description: 'Test workflow',
630
+ version: '1.0.0',
631
+ })
632
+ builder.step('task', validateOrder)
633
+
634
+ const workflow = builder.build()
635
+
636
+ expect(workflow.metadata?.description).toBe('Test workflow')
637
+ expect(workflow.metadata?.version).toBe('1.0.0')
638
+ })
639
+ })
640
+
641
+ // ============================================================================
642
+ // 7. BuiltWorkflow.execute() - Executing the Workflow
643
+ // ============================================================================
644
+
645
+ describe('BuiltWorkflow.execute()', () => {
646
+ it('executes a simple workflow', async () => {
647
+ const workflow = WorkflowBuilder.create('simple-workflow')
648
+ .step('validate', validateOrder)
649
+ .build()
650
+
651
+ const result = await workflow.execute({ orderId: 'order-123' })
652
+
653
+ expect(result).toBeDefined()
654
+ expect(result.validate.valid).toBe(true)
655
+ })
656
+
657
+ it('executes steps in dependency order', async () => {
658
+ const executionLog: string[] = []
659
+
660
+ const workflow = WorkflowBuilder.create('ordered-workflow')
661
+ .step('first', async () => {
662
+ executionLog.push('first')
663
+ return { done: true }
664
+ })
665
+ .step('second', async () => {
666
+ executionLog.push('second')
667
+ return { done: true }
668
+ })
669
+ .dependsOn('first')
670
+ .step('third', async () => {
671
+ executionLog.push('third')
672
+ return { done: true }
673
+ })
674
+ .dependsOn('second')
675
+ .build()
676
+
677
+ await workflow.execute()
678
+
679
+ expect(executionLog).toEqual(['first', 'second', 'third'])
680
+ })
681
+
682
+ it('passes step output to dependent steps', async () => {
683
+ const workflow = WorkflowBuilder.create('data-flow')
684
+ .step('validate', async (input: { orderId: string }) => {
685
+ return { valid: true, orderId: input.orderId }
686
+ })
687
+ .step('charge', async (input: { orderId: string }, ctx) => {
688
+ // Should be able to access validate's output
689
+ const validateResult = ctx.getStepResult('validate')
690
+ return { charged: true, wasValid: validateResult.valid }
691
+ })
692
+ .dependsOn('validate')
693
+ .build()
694
+
695
+ const result = await workflow.execute({ orderId: 'order-123' })
696
+
697
+ expect(result.charge.wasValid).toBe(true)
698
+ })
699
+
700
+ it('executes parallel steps concurrently', async () => {
701
+ const startTimes: Record<string, number> = {}
702
+
703
+ const workflow = WorkflowBuilder.create('parallel-workflow')
704
+ .step('stepA', async () => {
705
+ startTimes.stepA = Date.now()
706
+ await new Promise((r) => setTimeout(r, 100))
707
+ return { a: true }
708
+ })
709
+ .step('stepB', async () => {
710
+ startTimes.stepB = Date.now()
711
+ await new Promise((r) => setTimeout(r, 100))
712
+ return { b: true }
713
+ })
714
+ .step('stepC', async () => {
715
+ startTimes.stepC = Date.now()
716
+ return { c: true }
717
+ })
718
+ .dependsOn('stepA', 'stepB')
719
+ .build()
720
+
721
+ await workflow.execute()
722
+
723
+ // stepA and stepB should start at approximately the same time
724
+ const timeDiff = Math.abs(startTimes.stepA - startTimes.stepB)
725
+ expect(timeDiff).toBeLessThan(50) // Within 50ms of each other
726
+ })
727
+
728
+ it('returns results from all steps', async () => {
729
+ const workflow = WorkflowBuilder.create('result-workflow')
730
+ .step('validate', async () => ({ valid: true }))
731
+ .step('charge', async () => ({ charged: true }))
732
+ .step('fulfill', async () => ({ fulfilled: true }))
733
+ .build()
734
+
735
+ const result = await workflow.execute()
736
+
737
+ expect(result.validate.valid).toBe(true)
738
+ expect(result.charge.charged).toBe(true)
739
+ expect(result.fulfill.fulfilled).toBe(true)
740
+ })
741
+
742
+ it('throws on step failure', async () => {
743
+ const workflow = WorkflowBuilder.create('failing-workflow')
744
+ .step('fail', async () => {
745
+ throw new Error('Step failed')
746
+ })
747
+ .build()
748
+
749
+ await expect(workflow.execute()).rejects.toThrow('Step failed')
750
+ })
751
+
752
+ it('supports step error handlers', async () => {
753
+ const workflow = WorkflowBuilder.create('error-handling-workflow')
754
+ .step('risky', async () => {
755
+ throw new Error('Oops')
756
+ })
757
+ .onError((error, ctx) => {
758
+ return { recovered: true, error: error.message }
759
+ })
760
+ .build()
761
+
762
+ const result = await workflow.execute()
763
+
764
+ expect(result.risky.recovered).toBe(true)
765
+ })
766
+ })
767
+
768
+ // ============================================================================
769
+ // 8. WorkflowService Integration
770
+ // ============================================================================
771
+
772
+ describe('WorkflowBuilder with WorkflowService', () => {
773
+ it('built workflow can be registered with WorkflowService', () => {
774
+ const workflow = WorkflowBuilder.create('registered-workflow')
775
+ .step('validate', validateOrder)
776
+ .on('Order.placed')
777
+ .do('validate')
778
+ .build()
779
+
780
+ const service = new WorkflowServiceCore()
781
+
782
+ // Should be able to register the built workflow
783
+ const result = service.registerWorkflow(workflow)
784
+
785
+ expect(result).toBeDefined()
786
+ })
787
+
788
+ it('registered workflow receives events', async () => {
789
+ let receivedEvent: unknown = null
790
+
791
+ const workflow = WorkflowBuilder.create('event-receiver')
792
+ .step('handle', async (event: { orderId: string }) => {
793
+ receivedEvent = event
794
+ return { handled: true }
795
+ })
796
+ .on('Order.placed')
797
+ .do('handle')
798
+ .build()
799
+
800
+ const service = new WorkflowServiceCore()
801
+ const registration = service.registerWorkflow(workflow)
802
+
803
+ // Emit an event
804
+ await service.emit(registration.id, 'Order.placed', { orderId: 'order-123' })
805
+
806
+ // Wait for processing
807
+ await new Promise((r) => setTimeout(r, 100))
808
+
809
+ expect(receivedEvent).toEqual({ orderId: 'order-123' })
810
+ })
811
+
812
+ it('registered workflow executes on schedule', async () => {
813
+ let executionCount = 0
814
+
815
+ const workflow = WorkflowBuilder.create('scheduled-runner')
816
+ .step('run', async () => {
817
+ executionCount++
818
+ return { count: executionCount }
819
+ })
820
+ .every('100ms')
821
+ .do('run') // Very short interval for testing
822
+ .build()
823
+
824
+ const service = new WorkflowServiceCore()
825
+ const registration = service.registerWorkflow(workflow)
826
+
827
+ await service.start(registration.id)
828
+
829
+ // Wait for a few executions
830
+ await new Promise((r) => setTimeout(r, 350))
831
+
832
+ await service.stop(registration.id)
833
+
834
+ expect(executionCount).toBeGreaterThanOrEqual(2)
835
+ })
836
+
837
+ it('multiple workflows can be registered', () => {
838
+ const workflow1 = WorkflowBuilder.create('workflow-1').step('step1', validateOrder).build()
839
+
840
+ const workflow2 = WorkflowBuilder.create('workflow-2').step('step2', chargePayment).build()
841
+
842
+ const service = new WorkflowServiceCore()
843
+
844
+ const reg1 = service.registerWorkflow(workflow1)
845
+ const reg2 = service.registerWorkflow(workflow2)
846
+
847
+ expect(reg1.id).not.toBe(reg2.id)
848
+ expect(service.list()).toContain(reg1.id)
849
+ expect(service.list()).toContain(reg2.id)
850
+ })
851
+ })
852
+
853
+ // ============================================================================
854
+ // 9. Complete DSL Example (from Issue)
855
+ // ============================================================================
856
+
857
+ describe('WorkflowBuilder DSL Complete Example', () => {
858
+ it('builds the order-process workflow from the issue example', () => {
859
+ const workflow = WorkflowBuilder.create('order-process')
860
+ .step('validate', validateOrder)
861
+ .step('charge', chargePayment)
862
+ .dependsOn('validate')
863
+ .step('fulfill', fulfillOrder)
864
+ .dependsOn('charge')
865
+ .on('Order.placed')
866
+ .do('validate')
867
+ .build()
868
+
869
+ expect(workflow).toBeDefined()
870
+ expect(workflow.name).toBe('order-process')
871
+ expect(workflow.steps).toHaveLength(3)
872
+ expect(workflow.triggers.events).toHaveLength(1)
873
+ expect(workflow.executionOrder).toEqual(['validate', 'charge', 'fulfill'])
874
+ })
875
+
876
+ it('executes the order-process workflow end-to-end', async () => {
877
+ const workflow = WorkflowBuilder.create('order-process')
878
+ .step('validate', async (input: { orderId: string }) => {
879
+ return { valid: true, orderId: input.orderId }
880
+ })
881
+ .step('charge', async (input: { orderId: string; amount: number }, ctx) => {
882
+ const validation = ctx.getStepResult('validate')
883
+ if (!validation.valid) throw new Error('Invalid order')
884
+ return { charged: true, transactionId: `txn_${input.orderId}` }
885
+ })
886
+ .dependsOn('validate')
887
+ .step('fulfill', async (input, ctx) => {
888
+ const charge = ctx.getStepResult('charge')
889
+ return { fulfilled: true, transactionId: charge.transactionId }
890
+ })
891
+ .dependsOn('charge')
892
+ .build()
893
+
894
+ const result = await workflow.execute({ orderId: 'order-456', amount: 99.99 })
895
+
896
+ expect(result.validate.valid).toBe(true)
897
+ expect(result.charge.charged).toBe(true)
898
+ expect(result.fulfill.fulfilled).toBe(true)
899
+ expect(result.fulfill.transactionId).toBe('txn_order-456')
900
+ })
901
+ })
902
+
903
+ // ============================================================================
904
+ // 10. Edge Cases and Error Handling
905
+ // ============================================================================
906
+
907
+ describe('WorkflowBuilder Edge Cases', () => {
908
+ it('handles empty workflow (no steps, only event triggers)', () => {
909
+ const workflow = WorkflowBuilder.create('event-only')
910
+ .on('Order.placed')
911
+ .do(async () => ({ done: true }))
912
+ .build()
913
+
914
+ expect(workflow).toBeDefined()
915
+ expect(workflow.steps.length).toBeGreaterThanOrEqual(1) // Implicit step from inline fn
916
+ })
917
+
918
+ it('handles workflow with only scheduled triggers', () => {
919
+ const workflow = WorkflowBuilder.create('schedule-only')
920
+ .every('hour')
921
+ .do(async () => ({ done: true }))
922
+ .build()
923
+
924
+ expect(workflow).toBeDefined()
925
+ })
926
+
927
+ it('handles complex dependency graph', () => {
928
+ // A
929
+ // / \
930
+ // B C
931
+ // \ /
932
+ // D
933
+ const workflow = WorkflowBuilder.create('diamond')
934
+ .step('A', async () => ({ a: true }))
935
+ .step('B', async () => ({ b: true }))
936
+ .dependsOn('A')
937
+ .step('C', async () => ({ c: true }))
938
+ .dependsOn('A')
939
+ .step('D', async () => ({ d: true }))
940
+ .dependsOn('B', 'C')
941
+ .build()
942
+
943
+ expect(workflow).toBeDefined()
944
+ expect(workflow.dependencyGraph.get('D')).toContain('B')
945
+ expect(workflow.dependencyGraph.get('D')).toContain('C')
946
+ })
947
+
948
+ it('handles self-referential step (should throw)', () => {
949
+ const builder = WorkflowBuilder.create('self-ref')
950
+ .step('loop', async () => ({ done: true }))
951
+ .dependsOn('loop')
952
+
953
+ expect(() => builder.build()).toThrow(/circular|self-referential/i)
954
+ })
955
+
956
+ it('handles very long step chains', () => {
957
+ const builder = WorkflowBuilder.create('long-chain')
958
+
959
+ // Create a chain of 100 steps
960
+ for (let i = 0; i < 100; i++) {
961
+ builder.step(`step${i}`, async () => ({ step: i }))
962
+ if (i > 0) {
963
+ builder.dependsOn(`step${i - 1}`)
964
+ }
965
+ }
966
+
967
+ const workflow = builder.build()
968
+
969
+ expect(workflow.steps).toHaveLength(100)
970
+ })
971
+
972
+ it('builder is reusable (can build multiple times)', () => {
973
+ const builder = WorkflowBuilder.create('reusable').step('step1', validateOrder)
974
+
975
+ const workflow1 = builder.build()
976
+
977
+ builder.step('step2', chargePayment)
978
+
979
+ const workflow2 = builder.build()
980
+
981
+ expect(workflow1.steps).toHaveLength(1)
982
+ expect(workflow2.steps).toHaveLength(2)
983
+ })
984
+ })
985
+
986
+ // ============================================================================
987
+ // 11. Type Safety Tests
988
+ // ============================================================================
989
+
990
+ describe('WorkflowBuilder Type Safety', () => {
991
+ it('preserves input/output types through step chain', () => {
992
+ interface OrderInput {
993
+ orderId: string
994
+ amount: number
995
+ }
996
+
997
+ interface ValidationResult {
998
+ valid: boolean
999
+ orderId: string
1000
+ }
1001
+
1002
+ const workflow = WorkflowBuilder.create('typed-workflow')
1003
+ .step<OrderInput, ValidationResult>('validate', async (input) => {
1004
+ // TypeScript should know input has orderId and amount
1005
+ return { valid: true, orderId: input.orderId }
1006
+ })
1007
+ .build()
1008
+
1009
+ expect(workflow).toBeDefined()
1010
+ })
1011
+
1012
+ it('step context provides typed access to previous results', async () => {
1013
+ interface Step1Result {
1014
+ value: number
1015
+ }
1016
+
1017
+ interface Step2Result {
1018
+ doubled: number
1019
+ }
1020
+
1021
+ const workflow = WorkflowBuilder.create('typed-context')
1022
+ .step<void, Step1Result>('step1', async () => ({ value: 21 }))
1023
+ .step<void, Step2Result>('step2', async (_, ctx) => {
1024
+ const step1Result = ctx.getStepResult<Step1Result>('step1')
1025
+ return { doubled: step1Result.value * 2 }
1026
+ })
1027
+ .dependsOn('step1')
1028
+ .build()
1029
+
1030
+ const result = await workflow.execute()
1031
+
1032
+ expect(result.step2.doubled).toBe(42)
1033
+ })
1034
+ })
1035
+
1036
+ // ============================================================================
1037
+ // 12. Real Cloudflare Workflows Integration
1038
+ // ============================================================================
1039
+
1040
+ describe('WorkflowBuilder with Real Cloudflare Workflows', () => {
1041
+ it('built workflow integrates with Cloudflare Workflows runtime', async () => {
1042
+ // This test requires the TestWorkflow to be configured in wrangler.jsonc
1043
+ // and the workflow to use real step.do() calls
1044
+
1045
+ const workflow = WorkflowBuilder.create('cf-integrated')
1046
+ .step('durable-step', async (input: { value: number }) => {
1047
+ // This should be wrapped in step.do() for durability
1048
+ return { result: input.value * 2 }
1049
+ })
1050
+ .build()
1051
+
1052
+ // The built workflow should be compatible with Cloudflare Workflows
1053
+ expect(workflow.isCloudflareCompatible).toBe(true)
1054
+ })
1055
+
1056
+ it('steps are wrapped with DurableStep for durability', () => {
1057
+ const workflow = WorkflowBuilder.create('durable-wrapped')
1058
+ .step('my-step', validateOrder, {
1059
+ retries: { limit: 3 },
1060
+ })
1061
+ .build()
1062
+
1063
+ // Each step in the built workflow should be a DurableStep
1064
+ const step = workflow.steps.find((s) => s.name === 'my-step')
1065
+ expect(step?.durableStep).toBeInstanceOf(DurableStep)
1066
+ })
1067
+ })