ai-workflows 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -1
- package/README.md +305 -184
- package/dist/barrier.d.ts +159 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +377 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -8
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +53 -19
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +77 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +154 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +105 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +136 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +21 -4
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +507 -0
- package/src/cascade-context.ts +495 -0
- package/src/cascade-executor.ts +588 -0
- package/src/context.ts +51 -17
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/dependency-graph.ts +518 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +412 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +81 -26
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +179 -0
- package/src/types.ts +146 -10
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +199 -355
- package/test/barrier-join.test.ts +442 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +852 -0
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +353 -0
- package/test/send-race-conditions.test.ts +400 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -7
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- package/vitest.config.js +0 -7
|
@@ -0,0 +1,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
|
+
})
|