ai-workflows 2.1.3 → 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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
@@ -0,0 +1,723 @@
1
+ /**
2
+ * DurableStep Wrapper Tests (RED Phase)
3
+ *
4
+ * Tests for DurableStep - a wrapper around Cloudflare Workflows step semantics
5
+ * that provides durable execution, retries, sleep, and step metadata.
6
+ *
7
+ * These tests define the expected behavior for DurableStep before implementation.
8
+ * All tests SHOULD FAIL because DurableStep integration with real Cloudflare
9
+ * Workflows is not yet implemented.
10
+ *
11
+ * Uses @cloudflare/vitest-pool-workers - NO MOCKS.
12
+ * Tests run against real Cloudflare Workflows bindings.
13
+ *
14
+ * Bead: aip-p3m5
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 the Cloudflare Workflows integration is not
24
+ // yet implemented in DurableStep. This is the RED phase of TDD.
25
+ // ============================================================================
26
+ import {
27
+ DurableStep,
28
+ StepContext,
29
+ type StepMetadata,
30
+ type StepConfig,
31
+ type WorkflowStep,
32
+ } from '../../src/worker/durable-step.js'
33
+
34
+ // Import the TestWorkflow that should be defined in worker.ts
35
+ // This will FAIL because TestWorkflow doesn't exist yet
36
+ import { TestWorkflow } from '../../src/worker.js'
37
+
38
+ // ============================================================================
39
+ // Type Definitions for Test Environment
40
+ // ============================================================================
41
+
42
+ interface TestEnv {
43
+ WORKFLOW: Workflow
44
+ }
45
+
46
+ // ============================================================================
47
+ // Helper Functions
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Get a workflow instance from the binding.
52
+ * This creates a new workflow instance for testing.
53
+ *
54
+ * Note: env.WORKFLOW.create() returns a WorkflowInstance directly.
55
+ * The `id` option sets the instance ID for later retrieval via get().
56
+ */
57
+ async function getWorkflowInstance(name?: string): Promise<WorkflowInstance> {
58
+ // Create returns the WorkflowInstance directly
59
+ const instance = await env.WORKFLOW.create({
60
+ id: name ?? crypto.randomUUID(),
61
+ })
62
+ return instance
63
+ }
64
+
65
+ /**
66
+ * Run a workflow and wait for it to complete.
67
+ */
68
+ async function runWorkflow<T>(instance: WorkflowInstance, params?: unknown): Promise<T> {
69
+ const status = await instance.status()
70
+ if (status.status === 'queued' || status.status === 'running') {
71
+ // Wait for completion
72
+ let current = status
73
+ while (current.status !== 'complete' && current.status !== 'errored') {
74
+ await new Promise((resolve) => setTimeout(resolve, 100))
75
+ current = await instance.status()
76
+ }
77
+ if (current.status === 'errored') {
78
+ // Extract error message from the error object
79
+ // Note: miniflare's workflow status API doesn't expose error details in current.error
80
+ // The actual error message is logged by workerd but not accessible via the status API
81
+ const error = current.error as unknown
82
+ let errorMessage: string
83
+
84
+ if (typeof error === 'string') {
85
+ errorMessage = error
86
+ } else if (error && typeof error === 'object') {
87
+ const err = error as Record<string, unknown>
88
+ // Try common error property names
89
+ errorMessage = (err.message ??
90
+ err.name ??
91
+ err.error ??
92
+ (err.cause &&
93
+ typeof err.cause === 'object' &&
94
+ (err.cause as Record<string, unknown>).message) ??
95
+ JSON.stringify(error)) as string
96
+ } else {
97
+ errorMessage = String(error ?? 'Unknown error')
98
+ }
99
+ throw new Error(errorMessage)
100
+ }
101
+ return current.output as T
102
+ }
103
+ return status.output as T
104
+ }
105
+
106
+ // ============================================================================
107
+ // 1. DurableStep: Core Construction
108
+ // ============================================================================
109
+
110
+ describe('DurableStep', () => {
111
+ describe('construction', () => {
112
+ it('creates a DurableStep with a name and function', () => {
113
+ const step = new DurableStep('fetch-data', async (input: { url: string }) => {
114
+ return { status: 200 }
115
+ })
116
+
117
+ expect(step).toBeInstanceOf(DurableStep)
118
+ expect(step.name).toBe('fetch-data')
119
+ })
120
+
121
+ it('creates a DurableStep with a name, config, and function', () => {
122
+ const step = new DurableStep(
123
+ 'process-payment',
124
+ {
125
+ retries: { limit: 3, delay: '1 second', backoff: 'exponential' },
126
+ timeout: '30 seconds',
127
+ },
128
+ async (input: { amount: number }) => {
129
+ return { success: true }
130
+ }
131
+ )
132
+
133
+ expect(step).toBeInstanceOf(DurableStep)
134
+ expect(step.name).toBe('process-payment')
135
+ expect(step.config).toBeDefined()
136
+ expect(step.config?.retries?.limit).toBe(3)
137
+ expect(step.config?.timeout).toBe('30 seconds')
138
+ })
139
+
140
+ it('preserves the function reference', () => {
141
+ const fn = async (input: { id: string }) => ({ found: true })
142
+ const step = new DurableStep('lookup', fn)
143
+
144
+ expect(step.fn).toBe(fn)
145
+ })
146
+ })
147
+
148
+ // ============================================================================
149
+ // 2. DurableStep.run() with Real Workflows Binding
150
+ // ============================================================================
151
+
152
+ describe('run() with real Workflows binding', () => {
153
+ it('executes the wrapped function with input via real workflow step', async () => {
154
+ // This test requires the TestWorkflow to be properly configured
155
+ // and the DurableStep to integrate with real Workflows step.do()
156
+ const instance = await getWorkflowInstance('exec-test-1')
157
+
158
+ // The workflow should execute a DurableStep internally
159
+ const result = await runWorkflow<{ value: number }>(instance)
160
+
161
+ expect(result).toBeDefined()
162
+ expect(result.value).toBe(42)
163
+ })
164
+
165
+ it('wraps execution in step.do() for durability', async () => {
166
+ // This test verifies that step.do() is called with the step name
167
+ const instance = await getWorkflowInstance('durability-test-1')
168
+
169
+ const result = await runWorkflow<{ stepName: string }>(instance)
170
+
171
+ expect(result.stepName).toBe('durable-action')
172
+ })
173
+
174
+ it('passes config to step.do() when provided', async () => {
175
+ // Verify that retry config is passed through to the Workflows runtime
176
+ const instance = await getWorkflowInstance('config-test-1')
177
+
178
+ const result = await runWorkflow<{ configApplied: boolean }>(instance)
179
+
180
+ expect(result.configApplied).toBe(true)
181
+ })
182
+
183
+ it('returns the result from the wrapped function', async () => {
184
+ const instance = await getWorkflowInstance('result-test-1')
185
+
186
+ const result = await runWorkflow<{ sum: number; product: number }>(instance)
187
+
188
+ expect(result).toEqual({ sum: 10, product: 21 })
189
+ })
190
+
191
+ it('propagates errors from the wrapped function', async () => {
192
+ const instance = await getWorkflowInstance('error-test-1')
193
+
194
+ // Note: miniflare's workflow status API doesn't expose error details,
195
+ // so we check that an error is thrown (the actual error message "Step execution failed"
196
+ // is visible in workerd logs but not in the status object)
197
+ await expect(runWorkflow(instance)).rejects.toThrow()
198
+ })
199
+
200
+ it('supports generic input and output types', async () => {
201
+ interface OrderInput {
202
+ orderId: string
203
+ items: string[]
204
+ }
205
+
206
+ interface OrderResult {
207
+ confirmed: boolean
208
+ total: number
209
+ }
210
+
211
+ const instance = await getWorkflowInstance('typed-test-1')
212
+
213
+ const result = await runWorkflow<OrderResult>(instance)
214
+
215
+ expect(result.confirmed).toBe(true)
216
+ expect(result.total).toBe(20)
217
+ })
218
+
219
+ it('supports void input', async () => {
220
+ const instance = await getWorkflowInstance('void-input-test-1')
221
+
222
+ const result = await runWorkflow<string>(instance)
223
+
224
+ expect(result).toBe('hello')
225
+ })
226
+ })
227
+
228
+ // ============================================================================
229
+ // 3. DurableStep with StepContext
230
+ // ============================================================================
231
+
232
+ describe('run() with StepContext', () => {
233
+ it('provides a StepContext to the function when requested', async () => {
234
+ const instance = await getWorkflowInstance('ctx-test-1')
235
+
236
+ const result = await runWorkflow<{ hasContext: boolean }>(instance)
237
+
238
+ expect(result.hasContext).toBe(true)
239
+ })
240
+
241
+ it('StepContext provides step metadata', async () => {
242
+ const instance = await getWorkflowInstance('metadata-test-1')
243
+
244
+ const result = await runWorkflow<StepMetadata>(instance)
245
+
246
+ expect(result).toBeDefined()
247
+ expect(result.id).toBe('meta-step')
248
+ expect(typeof result.attempt).toBe('number')
249
+ })
250
+ })
251
+ })
252
+
253
+ // ============================================================================
254
+ // 4. StepContext: step.do() for side effects
255
+ // ============================================================================
256
+
257
+ describe('StepContext', () => {
258
+ describe('do()', () => {
259
+ it('executes a named side effect durably via real Workflows', async () => {
260
+ const instance = await getWorkflowInstance('side-effect-test-1')
261
+
262
+ const result = await runWorkflow<{ sent: boolean }>(instance)
263
+
264
+ expect(result).toEqual({ sent: true })
265
+ })
266
+
267
+ it('executes do() with config for retries', async () => {
268
+ const instance = await getWorkflowInstance('retry-config-test-1')
269
+
270
+ const result = await runWorkflow<{ data: string }>(instance)
271
+
272
+ expect(result).toEqual({ data: 'response' })
273
+ })
274
+
275
+ it('propagates errors from do() side effects', async () => {
276
+ const instance = await getWorkflowInstance('side-effect-error-test-1')
277
+
278
+ // Note: miniflare's workflow status API doesn't expose error details,
279
+ // so we check that an error is thrown (the actual error message "Side effect failed"
280
+ // is visible in workerd logs but not in the status object)
281
+ await expect(runWorkflow(instance)).rejects.toThrow()
282
+ })
283
+
284
+ it('supports multiple sequential do() calls', async () => {
285
+ const instance = await getWorkflowInstance('sequential-test-1')
286
+
287
+ const result = await runWorkflow<string[]>(instance)
288
+
289
+ expect(result).toEqual(['step-1', 'step-2', 'step-3'])
290
+ })
291
+ })
292
+
293
+ // ============================================================================
294
+ // 5. StepContext: sleep() and sleepUntil()
295
+ // ============================================================================
296
+
297
+ describe('sleep()', () => {
298
+ it('sleeps for a specified duration string via real Workflows', async () => {
299
+ const instance = await getWorkflowInstance('sleep-test-1')
300
+
301
+ // Note: In real tests, this would actually wait. For testing purposes,
302
+ // we verify the workflow completes successfully after the sleep.
303
+ const startTime = Date.now()
304
+ const result = await runWorkflow<{ waited: boolean }>(instance)
305
+ const elapsed = Date.now() - startTime
306
+
307
+ expect(result).toEqual({ waited: true })
308
+ // Sleep should have occurred (at least partially in miniflare)
309
+ // The actual duration may be simulated in test environment
310
+ })
311
+
312
+ it('sleeps for a duration with various units', async () => {
313
+ const instance = await getWorkflowInstance('multi-sleep-test-1')
314
+
315
+ const result = await runWorkflow<{ sleepCount: number }>(instance)
316
+
317
+ expect(result.sleepCount).toBe(3)
318
+ })
319
+ })
320
+
321
+ describe('sleepUntil()', () => {
322
+ it('sleeps until a specified Date via real Workflows', async () => {
323
+ const instance = await getWorkflowInstance('sleep-until-test-1')
324
+
325
+ const result = await runWorkflow<{ resumed: boolean }>(instance)
326
+
327
+ expect(result).toEqual({ resumed: true })
328
+ })
329
+
330
+ it('sleeps until a specified unix timestamp (number)', async () => {
331
+ const instance = await getWorkflowInstance('timestamp-sleep-test-1')
332
+
333
+ const result = await runWorkflow<{ completed: boolean }>(instance)
334
+
335
+ expect(result.completed).toBe(true)
336
+ })
337
+ })
338
+
339
+ // ============================================================================
340
+ // 6. StepContext: Metadata
341
+ // ============================================================================
342
+
343
+ describe('metadata', () => {
344
+ it('exposes the step id', async () => {
345
+ const instance = await getWorkflowInstance('step-id-test-1')
346
+
347
+ const result = await runWorkflow<{ stepId: string }>(instance)
348
+
349
+ expect(result.stepId).toBe('named-step')
350
+ })
351
+
352
+ it('exposes the current attempt number', async () => {
353
+ const instance = await getWorkflowInstance('attempt-test-1')
354
+
355
+ const result = await runWorkflow<{ attempt: number }>(instance)
356
+
357
+ expect(result.attempt).toBeDefined()
358
+ expect(typeof result.attempt).toBe('number')
359
+ expect(result.attempt).toBeGreaterThanOrEqual(1)
360
+ })
361
+
362
+ it('exposes the configured retries limit', async () => {
363
+ const instance = await getWorkflowInstance('retries-limit-test-1')
364
+
365
+ const result = await runWorkflow<{ retriesLimit: number }>(instance)
366
+
367
+ expect(result.retriesLimit).toBe(5)
368
+ })
369
+
370
+ it('exposes retries as 0 when no retry config provided', async () => {
371
+ const instance = await getWorkflowInstance('no-retries-test-1')
372
+
373
+ const result = await runWorkflow<{ retriesLimit: number }>(instance)
374
+
375
+ expect(result.retriesLimit).toBe(0)
376
+ })
377
+ })
378
+
379
+ // ============================================================================
380
+ // 7. Error Handling and Retries
381
+ // ============================================================================
382
+
383
+ describe('error handling', () => {
384
+ it('retries on failure when retries are configured (real Workflows)', async () => {
385
+ // This test verifies that the Workflows runtime handles retries
386
+ const instance = await getWorkflowInstance('retry-behavior-test-1')
387
+
388
+ const result = await runWorkflow<{ attempts: number; success: boolean }>(instance)
389
+
390
+ // The workflow should retry and eventually succeed
391
+ expect(result.attempts).toBeGreaterThan(1)
392
+ expect(result.success).toBe(true)
393
+ })
394
+
395
+ it('respects timeout configuration', async () => {
396
+ const instance = await getWorkflowInstance('timeout-test-1')
397
+
398
+ // A step that exceeds its timeout should error
399
+ await expect(runWorkflow(instance)).rejects.toThrow()
400
+ })
401
+
402
+ it('supports exponential backoff configuration', async () => {
403
+ const instance = await getWorkflowInstance('exp-backoff-test-1')
404
+
405
+ const result = await runWorkflow<{ backoffApplied: boolean }>(instance)
406
+
407
+ expect(result.backoffApplied).toBe(true)
408
+ })
409
+
410
+ it('supports linear backoff configuration', async () => {
411
+ const instance = await getWorkflowInstance('linear-backoff-test-1')
412
+
413
+ const result = await runWorkflow<{ backoffType: string }>(instance)
414
+
415
+ expect(result.backoffType).toBe('linear')
416
+ })
417
+
418
+ it('supports constant backoff configuration', async () => {
419
+ const instance = await getWorkflowInstance('constant-backoff-test-1')
420
+
421
+ const result = await runWorkflow<{ backoffType: string }>(instance)
422
+
423
+ expect(result.backoffType).toBe('constant')
424
+ })
425
+
426
+ it('throws immediately without retries when no config', async () => {
427
+ const instance = await getWorkflowInstance('no-retry-error-test-1')
428
+
429
+ // Note: miniflare's workflow status API doesn't expose error details,
430
+ // so we check that an error is thrown (the actual error message "Immediate failure"
431
+ // is visible in workerd logs but not in the status object)
432
+ await expect(runWorkflow(instance)).rejects.toThrow()
433
+ })
434
+ })
435
+
436
+ // ============================================================================
437
+ // 8. Composability: DurableStep chains
438
+ // ============================================================================
439
+
440
+ describe('composability', () => {
441
+ it('multiple DurableSteps can be run sequentially in a workflow', async () => {
442
+ const instance = await getWorkflowInstance('sequential-steps-test-1')
443
+
444
+ const result = await runWorkflow<{
445
+ fetchData: string
446
+ processed: boolean
447
+ }>(instance)
448
+
449
+ expect(result.fetchData).toBe('response from https://api.example.com')
450
+ expect(result.processed).toBe(true)
451
+ })
452
+
453
+ it('DurableStep can be used as a factory function', async () => {
454
+ const instance = await getWorkflowInstance('factory-test-1')
455
+
456
+ const result = await runWorkflow<{
457
+ usersEndpoint: string
458
+ ordersEndpoint: string
459
+ }>(instance)
460
+
461
+ expect(result.usersEndpoint).toBe('api-users')
462
+ expect(result.ordersEndpoint).toBe('api-orders')
463
+ })
464
+
465
+ it('supports parallel DurableStep execution', async () => {
466
+ const instance = await getWorkflowInstance('parallel-test-1')
467
+
468
+ const result = await runWorkflow<{
469
+ results: string[]
470
+ executedInParallel: boolean
471
+ }>(instance)
472
+
473
+ expect(result.results).toHaveLength(3)
474
+ expect(result.executedInParallel).toBe(true)
475
+ })
476
+ })
477
+ })
478
+
479
+ // ============================================================================
480
+ // 9. State Persistence Across Workflow Restarts
481
+ // ============================================================================
482
+
483
+ describe('DurableStep: State Persistence', () => {
484
+ it('persists step state before execution', async () => {
485
+ const instance = await getWorkflowInstance('persist-before-test-1')
486
+
487
+ const result = await runWorkflow<{ statePersistedBefore: boolean }>(instance)
488
+
489
+ expect(result.statePersistedBefore).toBe(true)
490
+ })
491
+
492
+ it('persists step state after execution', async () => {
493
+ const instance = await getWorkflowInstance('persist-after-test-1')
494
+
495
+ const result = await runWorkflow<{ statePersistedAfter: boolean }>(instance)
496
+
497
+ expect(result.statePersistedAfter).toBe(true)
498
+ })
499
+
500
+ it('resumes from last successful step on workflow restart', async () => {
501
+ // This tests the durability guarantee - if a workflow restarts,
502
+ // it should not re-execute already completed steps
503
+ const instance = await getWorkflowInstance('resume-test-1')
504
+
505
+ const result = await runWorkflow<{
506
+ step1ExecutedOnce: boolean
507
+ step2Completed: boolean
508
+ }>(instance)
509
+
510
+ expect(result.step1ExecutedOnce).toBe(true)
511
+ expect(result.step2Completed).toBe(true)
512
+ })
513
+
514
+ it('tracks execution history', async () => {
515
+ const instance = await getWorkflowInstance('history-test-1')
516
+
517
+ const result = await runWorkflow<{
518
+ history: Array<{ step: string; timestamp: string }>
519
+ }>(instance)
520
+
521
+ expect(result.history).toBeDefined()
522
+ expect(Array.isArray(result.history)).toBe(true)
523
+ expect(result.history.length).toBeGreaterThan(0)
524
+ })
525
+ })
526
+
527
+ // ============================================================================
528
+ // 10. Timeout Handling
529
+ // ============================================================================
530
+
531
+ describe('DurableStep: Timeout Handling', () => {
532
+ it('handles step timeout gracefully', async () => {
533
+ const instance = await getWorkflowInstance('graceful-timeout-test-1')
534
+
535
+ // The workflow should handle the timeout and return a timeout error
536
+ const result = await runWorkflow<{ timedOut: boolean; error: string }>(instance)
537
+
538
+ expect(result.timedOut).toBe(true)
539
+ expect(result.error).toContain('timeout')
540
+ })
541
+
542
+ it('allows subsequent steps after timeout handling', async () => {
543
+ const instance = await getWorkflowInstance('after-timeout-test-1')
544
+
545
+ const result = await runWorkflow<{
546
+ step1TimedOut: boolean
547
+ step2Completed: boolean
548
+ }>(instance)
549
+
550
+ expect(result.step1TimedOut).toBe(true)
551
+ expect(result.step2Completed).toBe(true)
552
+ })
553
+
554
+ it('respects per-step timeout configuration', async () => {
555
+ const instance = await getWorkflowInstance('per-step-timeout-test-1')
556
+
557
+ const result = await runWorkflow<{
558
+ fastStepCompleted: boolean
559
+ slowStepTimedOut: boolean
560
+ }>(instance)
561
+
562
+ expect(result.fastStepCompleted).toBe(true)
563
+ expect(result.slowStepTimedOut).toBe(true)
564
+ })
565
+ })
566
+
567
+ // ============================================================================
568
+ // 11. Integration with WorkflowService
569
+ // ============================================================================
570
+
571
+ describe('DurableStep: WorkflowService Integration', () => {
572
+ it('DurableStep works within WorkflowService context', async () => {
573
+ // This verifies that DurableStep integrates properly with WorkflowService
574
+ const instance = await getWorkflowInstance('service-integration-test-1')
575
+
576
+ const result = await runWorkflow<{ serviceIntegrated: boolean }>(instance)
577
+
578
+ expect(result.serviceIntegrated).toBe(true)
579
+ })
580
+
581
+ it('DurableStep can access workflow context', async () => {
582
+ const instance = await getWorkflowInstance('context-access-test-1')
583
+
584
+ const result = await runWorkflow<{ contextAvailable: boolean }>(instance)
585
+
586
+ expect(result.contextAvailable).toBe(true)
587
+ })
588
+
589
+ it('DurableStep supports workflow-level state sharing', async () => {
590
+ const instance = await getWorkflowInstance('state-sharing-test-1')
591
+
592
+ const result = await runWorkflow<{
593
+ step1SetValue: string
594
+ step2ReadValue: string
595
+ }>(instance)
596
+
597
+ expect(result.step1SetValue).toBe('shared-data')
598
+ expect(result.step2ReadValue).toBe('shared-data')
599
+ })
600
+ })
601
+
602
+ // ============================================================================
603
+ // 12. StepMetadata Type Tests
604
+ // ============================================================================
605
+
606
+ describe('StepMetadata', () => {
607
+ it('has required fields: id, attempt, retries', () => {
608
+ // This is a type-level test - if the import works and the type has the
609
+ // required fields, the DurableStep constructor and metadata access should work.
610
+ const metadata: StepMetadata = {
611
+ id: 'test-step',
612
+ attempt: 1,
613
+ retries: 3,
614
+ }
615
+
616
+ expect(metadata.id).toBe('test-step')
617
+ expect(metadata.attempt).toBe(1)
618
+ expect(metadata.retries).toBe(3)
619
+ })
620
+ })
621
+
622
+ // ============================================================================
623
+ // 13. StepConfig Type Tests
624
+ // ============================================================================
625
+
626
+ describe('StepConfig', () => {
627
+ it('matches Cloudflare WorkflowStepConfig shape', () => {
628
+ const config: StepConfig = {
629
+ retries: {
630
+ limit: 5,
631
+ delay: '1 second',
632
+ backoff: 'exponential',
633
+ },
634
+ timeout: '30 seconds',
635
+ }
636
+
637
+ expect(config.retries?.limit).toBe(5)
638
+ expect(config.retries?.delay).toBe('1 second')
639
+ expect(config.retries?.backoff).toBe('exponential')
640
+ expect(config.timeout).toBe('30 seconds')
641
+ })
642
+
643
+ it('supports numeric delay values', () => {
644
+ const config: StepConfig = {
645
+ retries: {
646
+ limit: 3,
647
+ delay: 1000,
648
+ },
649
+ }
650
+
651
+ expect(config.retries?.delay).toBe(1000)
652
+ })
653
+
654
+ it('supports numeric timeout values', () => {
655
+ const config: StepConfig = {
656
+ timeout: 30000,
657
+ }
658
+
659
+ expect(config.timeout).toBe(30000)
660
+ })
661
+
662
+ it('allows omitting optional fields', () => {
663
+ const minimalConfig: StepConfig = {}
664
+
665
+ expect(minimalConfig.retries).toBeUndefined()
666
+ expect(minimalConfig.timeout).toBeUndefined()
667
+ })
668
+ })
669
+
670
+ // ============================================================================
671
+ // 14. Edge Cases
672
+ // ============================================================================
673
+
674
+ describe('DurableStep: Edge Cases', () => {
675
+ it('handles empty input', async () => {
676
+ const instance = await getWorkflowInstance('empty-input-test-1')
677
+
678
+ const result = await runWorkflow<{ processed: boolean }>(instance)
679
+
680
+ expect(result.processed).toBe(true)
681
+ })
682
+
683
+ it('handles large input data', async () => {
684
+ const instance = await getWorkflowInstance('large-input-test-1')
685
+
686
+ const result = await runWorkflow<{ dataSize: number }>(instance)
687
+
688
+ expect(result.dataSize).toBeGreaterThan(1000)
689
+ })
690
+
691
+ it('handles large output data', async () => {
692
+ const instance = await getWorkflowInstance('large-output-test-1')
693
+
694
+ const result = await runWorkflow<{ items: unknown[] }>(instance)
695
+
696
+ expect(result.items.length).toBeGreaterThan(1000)
697
+ })
698
+
699
+ it('handles nested step.do() calls', async () => {
700
+ const instance = await getWorkflowInstance('nested-do-test-1')
701
+
702
+ const result = await runWorkflow<{ nestedResult: string }>(instance)
703
+
704
+ expect(result.nestedResult).toBe('nested-success')
705
+ })
706
+
707
+ it('handles concurrent workflow instances', async () => {
708
+ // Create multiple workflow instances concurrently
709
+ const instances = await Promise.all([
710
+ getWorkflowInstance('concurrent-test-1'),
711
+ getWorkflowInstance('concurrent-test-2'),
712
+ getWorkflowInstance('concurrent-test-3'),
713
+ ])
714
+
715
+ const results = await Promise.all(
716
+ instances.map((instance) => runWorkflow<{ instanceId: string }>(instance))
717
+ )
718
+
719
+ // Each instance should have completed with its own ID
720
+ const ids = results.map((r) => r.instanceId)
721
+ expect(new Set(ids).size).toBe(3) // All unique
722
+ })
723
+ })