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,852 @@
1
+ /**
2
+ * TDD Tests for CascadeExecutor
3
+ *
4
+ * Follows the code -> generative -> agentic -> human escalation pattern.
5
+ * Tests written first (RED phase) to define expected behavior.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
9
+ import {
10
+ CascadeExecutor,
11
+ type TierHandler,
12
+ type CascadeConfig,
13
+ type CascadeResult,
14
+ type TierResult,
15
+ CascadeTimeoutError,
16
+ TierSkippedError,
17
+ } from '../src/cascade-executor.js'
18
+ import type { FiveWHEvent } from '../src/cascade-context.js'
19
+
20
+ // ============================================================================
21
+ // Test Helpers
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Create a mock tier handler that succeeds
26
+ */
27
+ function createSuccessHandler<T>(result: T, delay = 0): TierHandler<T> {
28
+ return {
29
+ name: 'test-handler',
30
+ execute: vi.fn(async () => {
31
+ if (delay > 0) {
32
+ await new Promise((r) => setTimeout(r, delay))
33
+ }
34
+ return result
35
+ }),
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Create a mock tier handler that fails
41
+ */
42
+ function createFailureHandler(error: Error | string, delay = 0): TierHandler<never> {
43
+ const err = typeof error === 'string' ? new Error(error) : error
44
+ return {
45
+ name: 'test-handler',
46
+ execute: vi.fn(async () => {
47
+ if (delay > 0) {
48
+ await new Promise((r) => setTimeout(r, delay))
49
+ }
50
+ throw err
51
+ }),
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Create a mock tier handler that tracks calls
57
+ */
58
+ function createTrackingHandler<T>(result: T, tracker: { calls: number[] }): TierHandler<T> {
59
+ const startTime = Date.now()
60
+ return {
61
+ name: 'tracking-handler',
62
+ execute: vi.fn(async () => {
63
+ tracker.calls.push(Date.now() - startTime)
64
+ return result
65
+ }),
66
+ }
67
+ }
68
+
69
+ // ============================================================================
70
+ // Tier Escalation Tests
71
+ // ============================================================================
72
+
73
+ describe('CascadeExecutor', () => {
74
+ beforeEach(() => {
75
+ vi.useFakeTimers()
76
+ })
77
+
78
+ afterEach(() => {
79
+ vi.useRealTimers()
80
+ })
81
+
82
+ describe('tier escalation', () => {
83
+ it('should execute code tier first', async () => {
84
+ const codeHandler = createSuccessHandler('code-result')
85
+
86
+ const executor = new CascadeExecutor({
87
+ tiers: {
88
+ code: codeHandler,
89
+ },
90
+ })
91
+
92
+ const resultPromise = executor.execute({ input: 'test' })
93
+ await vi.runAllTimersAsync()
94
+ const result = await resultPromise
95
+
96
+ expect(codeHandler.execute).toHaveBeenCalledTimes(1)
97
+ expect(result.value).toBe('code-result')
98
+ expect(result.tier).toBe('code')
99
+ })
100
+
101
+ it('should escalate to generative tier when code tier fails', async () => {
102
+ const codeHandler = createFailureHandler('Code tier failed')
103
+ const generativeHandler = createSuccessHandler('generative-result')
104
+
105
+ const executor = new CascadeExecutor({
106
+ tiers: {
107
+ code: codeHandler,
108
+ generative: generativeHandler,
109
+ },
110
+ })
111
+
112
+ const resultPromise = executor.execute({ input: 'test' })
113
+ await vi.runAllTimersAsync()
114
+ const result = await resultPromise
115
+
116
+ expect(codeHandler.execute).toHaveBeenCalledTimes(1)
117
+ expect(generativeHandler.execute).toHaveBeenCalledTimes(1)
118
+ expect(result.value).toBe('generative-result')
119
+ expect(result.tier).toBe('generative')
120
+ })
121
+
122
+ it('should escalate to agentic tier when generative tier fails', async () => {
123
+ const codeHandler = createFailureHandler('Code tier failed')
124
+ const generativeHandler = createFailureHandler('Generative tier failed')
125
+ const agenticHandler = createSuccessHandler('agentic-result')
126
+
127
+ const executor = new CascadeExecutor({
128
+ tiers: {
129
+ code: codeHandler,
130
+ generative: generativeHandler,
131
+ agentic: agenticHandler,
132
+ },
133
+ })
134
+
135
+ const resultPromise = executor.execute({ input: 'test' })
136
+ await vi.runAllTimersAsync()
137
+ const result = await resultPromise
138
+
139
+ expect(codeHandler.execute).toHaveBeenCalledTimes(1)
140
+ expect(generativeHandler.execute).toHaveBeenCalledTimes(1)
141
+ expect(agenticHandler.execute).toHaveBeenCalledTimes(1)
142
+ expect(result.value).toBe('agentic-result')
143
+ expect(result.tier).toBe('agentic')
144
+ })
145
+
146
+ it('should escalate to human tier as last resort', async () => {
147
+ const codeHandler = createFailureHandler('Code tier failed')
148
+ const generativeHandler = createFailureHandler('Generative tier failed')
149
+ const agenticHandler = createFailureHandler('Agentic tier failed')
150
+ const humanHandler = createSuccessHandler('human-result')
151
+
152
+ const executor = new CascadeExecutor({
153
+ tiers: {
154
+ code: codeHandler,
155
+ generative: generativeHandler,
156
+ agentic: agenticHandler,
157
+ human: humanHandler,
158
+ },
159
+ })
160
+
161
+ const resultPromise = executor.execute({ input: 'test' })
162
+ await vi.runAllTimersAsync()
163
+ const result = await resultPromise
164
+
165
+ expect(codeHandler.execute).toHaveBeenCalledTimes(1)
166
+ expect(generativeHandler.execute).toHaveBeenCalledTimes(1)
167
+ expect(agenticHandler.execute).toHaveBeenCalledTimes(1)
168
+ expect(humanHandler.execute).toHaveBeenCalledTimes(1)
169
+ expect(result.value).toBe('human-result')
170
+ expect(result.tier).toBe('human')
171
+ })
172
+
173
+ it('should short-circuit on successful tier (not execute remaining tiers)', async () => {
174
+ const codeHandler = createSuccessHandler('code-result')
175
+ const generativeHandler = createSuccessHandler('generative-result')
176
+ const agenticHandler = createSuccessHandler('agentic-result')
177
+ const humanHandler = createSuccessHandler('human-result')
178
+
179
+ const executor = new CascadeExecutor({
180
+ tiers: {
181
+ code: codeHandler,
182
+ generative: generativeHandler,
183
+ agentic: agenticHandler,
184
+ human: humanHandler,
185
+ },
186
+ })
187
+
188
+ const resultPromise = executor.execute({ input: 'test' })
189
+ await vi.runAllTimersAsync()
190
+ const result = await resultPromise
191
+
192
+ expect(codeHandler.execute).toHaveBeenCalledTimes(1)
193
+ expect(generativeHandler.execute).not.toHaveBeenCalled()
194
+ expect(agenticHandler.execute).not.toHaveBeenCalled()
195
+ expect(humanHandler.execute).not.toHaveBeenCalled()
196
+ expect(result.value).toBe('code-result')
197
+ expect(result.tier).toBe('code')
198
+ })
199
+
200
+ it('should throw when all tiers fail', async () => {
201
+ const codeHandler = createFailureHandler('Code failed')
202
+ const generativeHandler = createFailureHandler('Generative failed')
203
+ const agenticHandler = createFailureHandler('Agentic failed')
204
+ const humanHandler = createFailureHandler('Human failed')
205
+
206
+ const executor = new CascadeExecutor({
207
+ tiers: {
208
+ code: codeHandler,
209
+ generative: generativeHandler,
210
+ agentic: agenticHandler,
211
+ human: humanHandler,
212
+ },
213
+ })
214
+
215
+ const resultPromise = executor.execute({ input: 'test' })
216
+ await vi.runAllTimersAsync()
217
+
218
+ await expect(resultPromise).rejects.toThrow('All cascade tiers failed')
219
+ })
220
+
221
+ it('should execute tiers in correct order: code -> generative -> agentic -> human', async () => {
222
+ const executionOrder: string[] = []
223
+
224
+ const createOrderTrackingHandler = (tier: string): TierHandler<never> => ({
225
+ name: tier,
226
+ execute: vi.fn(async () => {
227
+ executionOrder.push(tier)
228
+ throw new Error(`${tier} failed`)
229
+ }),
230
+ })
231
+
232
+ const humanHandler: TierHandler<string> = {
233
+ name: 'human',
234
+ execute: vi.fn(async () => {
235
+ executionOrder.push('human')
236
+ return 'human-result'
237
+ }),
238
+ }
239
+
240
+ const executor = new CascadeExecutor({
241
+ tiers: {
242
+ code: createOrderTrackingHandler('code'),
243
+ generative: createOrderTrackingHandler('generative'),
244
+ agentic: createOrderTrackingHandler('agentic'),
245
+ human: humanHandler,
246
+ },
247
+ })
248
+
249
+ const resultPromise = executor.execute({ input: 'test' })
250
+ await vi.runAllTimersAsync()
251
+ await resultPromise
252
+
253
+ expect(executionOrder).toEqual(['code', 'generative', 'agentic', 'human'])
254
+ })
255
+
256
+ it('should skip unconfigured tiers gracefully', async () => {
257
+ // Only configure code and human tiers
258
+ const codeHandler = createFailureHandler('Code failed')
259
+ const humanHandler = createSuccessHandler('human-result')
260
+
261
+ const executor = new CascadeExecutor({
262
+ tiers: {
263
+ code: codeHandler,
264
+ human: humanHandler,
265
+ },
266
+ })
267
+
268
+ const resultPromise = executor.execute({ input: 'test' })
269
+ await vi.runAllTimersAsync()
270
+ const result = await resultPromise
271
+
272
+ expect(result.value).toBe('human-result')
273
+ expect(result.tier).toBe('human')
274
+ expect(result.skippedTiers).toContain('generative')
275
+ expect(result.skippedTiers).toContain('agentic')
276
+ })
277
+
278
+ it('should record tier results in cascade history', async () => {
279
+ const codeHandler = createFailureHandler('Code failed')
280
+ const generativeHandler = createSuccessHandler('generative-result')
281
+
282
+ const executor = new CascadeExecutor({
283
+ tiers: {
284
+ code: codeHandler,
285
+ generative: generativeHandler,
286
+ },
287
+ })
288
+
289
+ const resultPromise = executor.execute({ input: 'test' })
290
+ await vi.runAllTimersAsync()
291
+ const result = await resultPromise
292
+
293
+ expect(result.history).toHaveLength(2)
294
+ expect(result.history[0]).toMatchObject({
295
+ tier: 'code',
296
+ success: false,
297
+ })
298
+ expect(result.history[1]).toMatchObject({
299
+ tier: 'generative',
300
+ success: true,
301
+ })
302
+ })
303
+ })
304
+
305
+ // ============================================================================
306
+ // Timeout Handling Tests
307
+ // ============================================================================
308
+
309
+ describe('timeout handling', () => {
310
+ it('should support per-tier timeout configuration', async () => {
311
+ const slowCodeHandler: TierHandler<string> = {
312
+ name: 'slow-code',
313
+ execute: vi.fn(async () => {
314
+ await new Promise((r) => setTimeout(r, 10000)) // 10s
315
+ return 'code-result'
316
+ }),
317
+ }
318
+ const generativeHandler = createSuccessHandler('generative-result')
319
+
320
+ const executor = new CascadeExecutor({
321
+ tiers: {
322
+ code: slowCodeHandler,
323
+ generative: generativeHandler,
324
+ },
325
+ timeouts: {
326
+ code: 5000, // 5s timeout for code tier
327
+ },
328
+ })
329
+
330
+ const resultPromise = executor.execute({ input: 'test' })
331
+
332
+ // Advance past code tier timeout
333
+ await vi.advanceTimersByTimeAsync(5001)
334
+
335
+ const result = await resultPromise
336
+
337
+ expect(result.tier).toBe('generative')
338
+ expect(result.value).toBe('generative-result')
339
+ expect(result.history[0]).toMatchObject({
340
+ tier: 'code',
341
+ success: false,
342
+ timedOut: true,
343
+ })
344
+ })
345
+
346
+ it('should trigger escalation when tier times out', async () => {
347
+ const slowHandler: TierHandler<string> = {
348
+ name: 'slow',
349
+ execute: vi.fn(async () => {
350
+ await new Promise((r) => setTimeout(r, 30000)) // 30s
351
+ return 'slow-result'
352
+ }),
353
+ }
354
+ const fastHandler = createSuccessHandler('fast-result')
355
+
356
+ const executor = new CascadeExecutor({
357
+ tiers: {
358
+ code: slowHandler,
359
+ generative: fastHandler,
360
+ },
361
+ timeouts: {
362
+ code: 1000,
363
+ },
364
+ })
365
+
366
+ const resultPromise = executor.execute({ input: 'test' })
367
+ await vi.advanceTimersByTimeAsync(1001)
368
+ const result = await resultPromise
369
+
370
+ expect(result.tier).toBe('generative')
371
+ expect(result.value).toBe('fast-result')
372
+ })
373
+
374
+ it('should support total cascade timeout', async () => {
375
+ const slowHandler: TierHandler<string> = {
376
+ name: 'slow',
377
+ execute: vi.fn(async () => {
378
+ await new Promise((r) => setTimeout(r, 5000))
379
+ return 'result'
380
+ }),
381
+ }
382
+
383
+ const executor = new CascadeExecutor({
384
+ tiers: {
385
+ code: slowHandler,
386
+ generative: slowHandler,
387
+ agentic: slowHandler,
388
+ human: slowHandler,
389
+ },
390
+ totalTimeout: 3000, // Total cascade must complete in 3s
391
+ })
392
+
393
+ const resultPromise = executor.execute({ input: 'test' })
394
+ // Add a catch to prevent unhandled rejection before we can await it
395
+ resultPromise.catch(() => {})
396
+ await vi.advanceTimersByTimeAsync(3001)
397
+
398
+ await expect(resultPromise).rejects.toThrow(CascadeTimeoutError)
399
+ })
400
+
401
+ it('should handle timeout gracefully without data loss', async () => {
402
+ const partialResults: string[] = []
403
+
404
+ const codeHandler: TierHandler<string> = {
405
+ name: 'code',
406
+ execute: vi.fn(async () => {
407
+ partialResults.push('code-started')
408
+ await new Promise((r) => setTimeout(r, 2000))
409
+ partialResults.push('code-completed')
410
+ return 'code-result'
411
+ }),
412
+ }
413
+
414
+ const generativeHandler: TierHandler<string> = {
415
+ name: 'generative',
416
+ execute: vi.fn(async () => {
417
+ partialResults.push('generative-started')
418
+ return 'generative-result'
419
+ }),
420
+ }
421
+
422
+ const executor = new CascadeExecutor({
423
+ tiers: {
424
+ code: codeHandler,
425
+ generative: generativeHandler,
426
+ },
427
+ timeouts: {
428
+ code: 1000,
429
+ },
430
+ })
431
+
432
+ const resultPromise = executor.execute({ input: 'test' })
433
+
434
+ // Let code tier start
435
+ await vi.advanceTimersByTimeAsync(100)
436
+ expect(partialResults).toContain('code-started')
437
+
438
+ // Timeout code tier
439
+ await vi.advanceTimersByTimeAsync(1000)
440
+
441
+ const result = await resultPromise
442
+
443
+ // Generative should have completed
444
+ expect(result.value).toBe('generative-result')
445
+ expect(partialResults).toContain('generative-started')
446
+ })
447
+
448
+ it('should use default tier timeouts from capability-tiers', async () => {
449
+ const slowCodeHandler: TierHandler<string> = {
450
+ name: 'slow-code',
451
+ execute: vi.fn(async () => {
452
+ await new Promise((r) => setTimeout(r, 10000)) // 10s (code default is 5s)
453
+ return 'code-result'
454
+ }),
455
+ }
456
+ const generativeHandler = createSuccessHandler('generative-result')
457
+
458
+ const executor = new CascadeExecutor({
459
+ tiers: {
460
+ code: slowCodeHandler,
461
+ generative: generativeHandler,
462
+ },
463
+ useDefaultTimeouts: true, // Use timeouts from capability-tiers
464
+ })
465
+
466
+ const resultPromise = executor.execute({ input: 'test' })
467
+
468
+ // Code tier default timeout is 5000ms
469
+ await vi.advanceTimersByTimeAsync(5001)
470
+
471
+ const result = await resultPromise
472
+ expect(result.tier).toBe('generative')
473
+ })
474
+
475
+ it('should include timeout duration in error details', async () => {
476
+ const slowHandler: TierHandler<string> = {
477
+ name: 'slow',
478
+ execute: vi.fn(async () => {
479
+ await new Promise((r) => setTimeout(r, 60000))
480
+ return 'result'
481
+ }),
482
+ }
483
+
484
+ const executor = new CascadeExecutor({
485
+ tiers: {
486
+ code: slowHandler,
487
+ },
488
+ totalTimeout: 5000,
489
+ })
490
+
491
+ const resultPromise = executor.execute({ input: 'test' })
492
+ // Add a catch to prevent unhandled rejection before we can await it
493
+ resultPromise.catch(() => {})
494
+ await vi.advanceTimersByTimeAsync(5001)
495
+
496
+ try {
497
+ await resultPromise
498
+ expect.fail('Should have thrown')
499
+ } catch (error) {
500
+ expect(error).toBeInstanceOf(CascadeTimeoutError)
501
+ const timeoutError = error as CascadeTimeoutError
502
+ expect(timeoutError.timeout).toBe(5000)
503
+ expect(timeoutError.elapsed).toBeGreaterThanOrEqual(5000)
504
+ }
505
+ })
506
+ })
507
+
508
+ // ============================================================================
509
+ // 5W+H Events Tests
510
+ // ============================================================================
511
+
512
+ describe('5W+H events', () => {
513
+ it('should record Who (actor identification) in events', async () => {
514
+ const codeHandler = createSuccessHandler('result')
515
+ const events: FiveWHEvent[] = []
516
+
517
+ const executor = new CascadeExecutor({
518
+ tiers: {
519
+ code: codeHandler,
520
+ },
521
+ actor: 'test-system',
522
+ onEvent: (event) => events.push(event),
523
+ })
524
+
525
+ const resultPromise = executor.execute({ input: 'test' })
526
+ await vi.runAllTimersAsync()
527
+ await resultPromise
528
+
529
+ expect(events.length).toBeGreaterThan(0)
530
+ expect(events.every((e) => e.who === 'test-system')).toBe(true)
531
+ })
532
+
533
+ it('should record What (action description) in events', async () => {
534
+ const codeHandler = createSuccessHandler('result')
535
+ const events: FiveWHEvent[] = []
536
+
537
+ const executor = new CascadeExecutor({
538
+ tiers: {
539
+ code: codeHandler,
540
+ },
541
+ onEvent: (event) => events.push(event),
542
+ })
543
+
544
+ const resultPromise = executor.execute({ input: 'test' })
545
+ await vi.runAllTimersAsync()
546
+ await resultPromise
547
+
548
+ const tierEvent = events.find((e) => e.what.includes('code'))
549
+ expect(tierEvent).toBeDefined()
550
+ expect(tierEvent?.what).toContain('execute')
551
+ })
552
+
553
+ it('should record When (timestamp) in events', async () => {
554
+ const now = Date.now()
555
+ vi.setSystemTime(now)
556
+
557
+ const codeHandler = createSuccessHandler('result')
558
+ const events: FiveWHEvent[] = []
559
+
560
+ const executor = new CascadeExecutor({
561
+ tiers: {
562
+ code: codeHandler,
563
+ },
564
+ onEvent: (event) => events.push(event),
565
+ })
566
+
567
+ const resultPromise = executor.execute({ input: 'test' })
568
+ await vi.runAllTimersAsync()
569
+ await resultPromise
570
+
571
+ expect(events.length).toBeGreaterThan(0)
572
+ expect(events.every((e) => typeof e.when === 'number')).toBe(true)
573
+ expect(events[0]?.when).toBe(now)
574
+ })
575
+
576
+ it('should record Where (context/location) in events', async () => {
577
+ const codeHandler = createSuccessHandler('result')
578
+ const events: FiveWHEvent[] = []
579
+
580
+ const executor = new CascadeExecutor({
581
+ tiers: {
582
+ code: codeHandler,
583
+ },
584
+ cascadeName: 'test-cascade',
585
+ onEvent: (event) => events.push(event),
586
+ })
587
+
588
+ const resultPromise = executor.execute({ input: 'test' })
589
+ await vi.runAllTimersAsync()
590
+ await resultPromise
591
+
592
+ expect(events.length).toBeGreaterThan(0)
593
+ expect(events.every((e) => e.where === 'test-cascade')).toBe(true)
594
+ })
595
+
596
+ it('should record Why (reason/justification) in events', async () => {
597
+ const codeHandler = createFailureHandler('Validation failed')
598
+ const generativeHandler = createSuccessHandler('result')
599
+ const events: FiveWHEvent[] = []
600
+
601
+ const executor = new CascadeExecutor({
602
+ tiers: {
603
+ code: codeHandler,
604
+ generative: generativeHandler,
605
+ },
606
+ onEvent: (event) => events.push(event),
607
+ })
608
+
609
+ const resultPromise = executor.execute({ input: 'test' })
610
+ await vi.runAllTimersAsync()
611
+ await resultPromise
612
+
613
+ // Find escalation event
614
+ const escalationEvent = events.find((e) => e.what.includes('escalat'))
615
+ expect(escalationEvent).toBeDefined()
616
+ expect(escalationEvent?.why).toContain('Validation failed')
617
+ })
618
+
619
+ it('should record How (method/approach) in events', async () => {
620
+ const codeHandler = createSuccessHandler('result')
621
+ const events: FiveWHEvent[] = []
622
+
623
+ const executor = new CascadeExecutor({
624
+ tiers: {
625
+ code: codeHandler,
626
+ },
627
+ onEvent: (event) => events.push(event),
628
+ })
629
+
630
+ const resultPromise = executor.execute({ input: 'test' })
631
+ await vi.runAllTimersAsync()
632
+ await resultPromise
633
+
634
+ const completionEvent = events.find((e) => e.how.status === 'completed')
635
+ expect(completionEvent).toBeDefined()
636
+ expect(completionEvent?.how).toMatchObject({
637
+ status: 'completed',
638
+ })
639
+ expect(completionEvent?.how.duration).toBeGreaterThanOrEqual(0)
640
+ })
641
+
642
+ it('should emit start and complete events for each tier', async () => {
643
+ const codeHandler = createFailureHandler('Failed')
644
+ const generativeHandler = createSuccessHandler('result')
645
+ const events: FiveWHEvent[] = []
646
+
647
+ const executor = new CascadeExecutor({
648
+ tiers: {
649
+ code: codeHandler,
650
+ generative: generativeHandler,
651
+ },
652
+ onEvent: (event) => events.push(event),
653
+ })
654
+
655
+ const resultPromise = executor.execute({ input: 'test' })
656
+ await vi.runAllTimersAsync()
657
+ await resultPromise
658
+
659
+ // Should have start and end events for both tiers
660
+ const codeStart = events.find((e) => e.what.includes('code') && e.how.status === 'running')
661
+ const codeEnd = events.find((e) => e.what.includes('code') && e.how.status === 'failed')
662
+ const genStart = events.find(
663
+ (e) => e.what.includes('generative') && e.how.status === 'running'
664
+ )
665
+ const genEnd = events.find(
666
+ (e) => e.what.includes('generative') && e.how.status === 'completed'
667
+ )
668
+
669
+ expect(codeStart).toBeDefined()
670
+ expect(codeEnd).toBeDefined()
671
+ expect(genStart).toBeDefined()
672
+ expect(genEnd).toBeDefined()
673
+ })
674
+
675
+ it('should emit cascade-level start and complete events', async () => {
676
+ const codeHandler = createSuccessHandler('result')
677
+ const events: FiveWHEvent[] = []
678
+
679
+ const executor = new CascadeExecutor({
680
+ tiers: {
681
+ code: codeHandler,
682
+ },
683
+ cascadeName: 'test-cascade',
684
+ onEvent: (event) => events.push(event),
685
+ })
686
+
687
+ const resultPromise = executor.execute({ input: 'test' })
688
+ await vi.runAllTimersAsync()
689
+ await resultPromise
690
+
691
+ const cascadeStart = events.find((e) => e.what === 'cascade-start')
692
+ const cascadeComplete = events.find((e) => e.what === 'cascade-complete')
693
+
694
+ expect(cascadeStart).toBeDefined()
695
+ expect(cascadeComplete).toBeDefined()
696
+ })
697
+
698
+ it('should include input/output metadata in How', async () => {
699
+ const codeHandler = createSuccessHandler({ processed: true })
700
+ const events: FiveWHEvent[] = []
701
+
702
+ const executor = new CascadeExecutor({
703
+ tiers: {
704
+ code: codeHandler,
705
+ },
706
+ onEvent: (event) => events.push(event),
707
+ })
708
+
709
+ const resultPromise = executor.execute({ input: 'test-data' })
710
+ await vi.runAllTimersAsync()
711
+ await resultPromise
712
+
713
+ const completionEvent = events.find((e) => e.how.status === 'completed')
714
+ expect(completionEvent?.how.metadata).toBeDefined()
715
+ })
716
+ })
717
+
718
+ // ============================================================================
719
+ // Additional Integration Tests
720
+ // ============================================================================
721
+
722
+ describe('integration', () => {
723
+ it('should integrate with CascadeContext for tracing', async () => {
724
+ const codeHandler = createSuccessHandler('result')
725
+
726
+ const executor = new CascadeExecutor({
727
+ tiers: {
728
+ code: codeHandler,
729
+ },
730
+ })
731
+
732
+ const resultPromise = executor.execute({ input: 'test' })
733
+ await vi.runAllTimersAsync()
734
+ const result = await resultPromise
735
+
736
+ expect(result.context).toBeDefined()
737
+ expect(result.context.correlationId).toBeDefined()
738
+ expect(result.context.steps.length).toBeGreaterThan(0)
739
+ })
740
+
741
+ it('should pass context to tier handlers', async () => {
742
+ let receivedContext: any
743
+
744
+ const codeHandler: TierHandler<string> = {
745
+ name: 'context-checker',
746
+ execute: vi.fn(async (input, context) => {
747
+ receivedContext = context
748
+ return 'result'
749
+ }),
750
+ }
751
+
752
+ const executor = new CascadeExecutor({
753
+ tiers: {
754
+ code: codeHandler,
755
+ },
756
+ })
757
+
758
+ const resultPromise = executor.execute({ input: 'test' })
759
+ await vi.runAllTimersAsync()
760
+ await resultPromise
761
+
762
+ expect(receivedContext).toBeDefined()
763
+ expect(receivedContext.correlationId).toBeDefined()
764
+ expect(receivedContext.tier).toBe('code')
765
+ })
766
+
767
+ it('should support custom tier skip conditions', async () => {
768
+ const codeHandler = createSuccessHandler('code-result')
769
+ const generativeHandler = createSuccessHandler('generative-result')
770
+
771
+ const executor = new CascadeExecutor({
772
+ tiers: {
773
+ code: codeHandler,
774
+ generative: generativeHandler,
775
+ },
776
+ skipConditions: {
777
+ code: (input) => input.skipCode === true,
778
+ },
779
+ })
780
+
781
+ const resultPromise = executor.execute({ input: 'test', skipCode: true })
782
+ await vi.runAllTimersAsync()
783
+ const result = await resultPromise
784
+
785
+ expect(codeHandler.execute).not.toHaveBeenCalled()
786
+ expect(result.tier).toBe('generative')
787
+ expect(result.skippedTiers).toContain('code')
788
+ })
789
+
790
+ it('should support retry per tier using RetryPolicy', async () => {
791
+ let attempts = 0
792
+ const flakeyHandler: TierHandler<string> = {
793
+ name: 'flakey',
794
+ execute: vi.fn(async () => {
795
+ attempts++
796
+ if (attempts < 3) {
797
+ throw new Error('Temporary failure')
798
+ }
799
+ return 'success'
800
+ }),
801
+ }
802
+
803
+ const executor = new CascadeExecutor({
804
+ tiers: {
805
+ code: flakeyHandler,
806
+ },
807
+ retryConfig: {
808
+ code: {
809
+ maxRetries: 3,
810
+ baseDelay: 100,
811
+ },
812
+ },
813
+ })
814
+
815
+ const resultPromise = executor.execute({ input: 'test' })
816
+
817
+ // Process retries
818
+ await vi.advanceTimersByTimeAsync(100) // First retry delay
819
+ await vi.advanceTimersByTimeAsync(200) // Second retry delay (exponential)
820
+ await vi.runAllTimersAsync()
821
+
822
+ const result = await resultPromise
823
+
824
+ expect(attempts).toBe(3)
825
+ expect(result.value).toBe('success')
826
+ })
827
+
828
+ it('should provide execution metrics', async () => {
829
+ const codeHandler: TierHandler<string> = {
830
+ name: 'code',
831
+ execute: vi.fn(async () => {
832
+ await new Promise((r) => setTimeout(r, 100))
833
+ return 'result'
834
+ }),
835
+ }
836
+
837
+ const executor = new CascadeExecutor({
838
+ tiers: {
839
+ code: codeHandler,
840
+ },
841
+ })
842
+
843
+ const resultPromise = executor.execute({ input: 'test' })
844
+ await vi.advanceTimersByTimeAsync(100)
845
+ const result = await resultPromise
846
+
847
+ expect(result.metrics).toBeDefined()
848
+ expect(result.metrics.totalDuration).toBeGreaterThanOrEqual(100)
849
+ expect(result.metrics.tierDurations.code).toBeGreaterThanOrEqual(100)
850
+ })
851
+ })
852
+ })