ai-workflows 2.1.3 → 2.4.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 +14 -1
- package/README.md +2 -0
- package/dist/barrier.d.ts +6 -0
- package/dist/barrier.d.ts.map +1 -1
- package/dist/barrier.js +45 -7
- package/dist/barrier.js.map +1 -1
- package/dist/cascade-context.d.ts.map +1 -1
- package/dist/cascade-context.js +25 -25
- package/dist/cascade-context.js.map +1 -1
- package/dist/cascade-executor.d.ts.map +1 -1
- package/dist/cascade-executor.js +1 -1
- package/dist/cascade-executor.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -7
- 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/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/graph/topological-sort.d.ts.map +1 -1
- package/dist/graph/topological-sort.js +5 -5
- package/dist/graph/topological-sort.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -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.map +1 -1
- package/dist/on.js +3 -3
- 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 +25 -0
- package/dist/timer-registry.d.ts.map +1 -1
- package/dist/timer-registry.js +42 -8
- package/dist/timer-registry.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -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 +132 -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 +30 -13
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +48 -7
- package/src/cascade-context.ts +36 -29
- package/src/cascade-executor.ts +3 -2
- package/src/context.ts +41 -12
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -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/graph/topological-sort.ts +6 -8
- package/src/index.ts +69 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +8 -9
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +44 -10
- package/src/types.ts +32 -17
- 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 +188 -351
- package/test/barrier-join.test.ts +32 -24
- package/test/cascade-executor.test.ts +9 -16
- 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/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/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 +30 -21
- package/test/send-race-conditions.test.ts +30 -40
- 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 -169
- package/LICENSE +0 -21
- 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,1117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DurableStep.cascade() Tests (RED Phase)
|
|
3
|
+
*
|
|
4
|
+
* Tests for DurableStep.cascade() - durable tiered execution pattern that
|
|
5
|
+
* combines code -> generative -> agentic -> human escalation with
|
|
6
|
+
* Cloudflare Workflows durability guarantees.
|
|
7
|
+
*
|
|
8
|
+
* These tests define the expected behavior for DurableStep.cascade() before implementation.
|
|
9
|
+
* All tests SHOULD FAIL because DurableStep.cascade() does not exist yet.
|
|
10
|
+
*
|
|
11
|
+
* Uses @cloudflare/vitest-pool-workers - NO MOCKS.
|
|
12
|
+
* Tests run against real Cloudflare Workflows bindings with AI Gateway.
|
|
13
|
+
*
|
|
14
|
+
* Bead: aip-9390
|
|
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 DurableStep.cascade() is not yet implemented.
|
|
24
|
+
// This is the RED phase of TDD.
|
|
25
|
+
// ============================================================================
|
|
26
|
+
import {
|
|
27
|
+
DurableStep,
|
|
28
|
+
StepContext,
|
|
29
|
+
type StepConfig,
|
|
30
|
+
type WorkflowStep,
|
|
31
|
+
} from '../../src/worker/durable-step.js'
|
|
32
|
+
|
|
33
|
+
// Import cascade types - these will need to be defined in DurableStep
|
|
34
|
+
// These imports WILL FAIL because the types don't exist yet
|
|
35
|
+
import type {
|
|
36
|
+
CascadeConfig,
|
|
37
|
+
CascadeTierConfig,
|
|
38
|
+
CascadeTierResult,
|
|
39
|
+
CascadeResult,
|
|
40
|
+
CascadeContext,
|
|
41
|
+
DurableCascadeStep,
|
|
42
|
+
} from '../../src/worker/durable-step.js'
|
|
43
|
+
|
|
44
|
+
// Import the TestWorkflow that should be defined in worker.ts
|
|
45
|
+
import { TestWorkflow } from '../../src/worker.js'
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Type Definitions for Test Environment
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
interface TestEnv {
|
|
52
|
+
WORKFLOW: Workflow
|
|
53
|
+
AI: Ai
|
|
54
|
+
AI_GATEWAY: AiGateway
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Helper Functions
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a workflow instance from the binding.
|
|
63
|
+
*/
|
|
64
|
+
async function getWorkflowInstance(name?: string): Promise<WorkflowInstance> {
|
|
65
|
+
const instance = await env.WORKFLOW.create({
|
|
66
|
+
id: name ?? crypto.randomUUID(),
|
|
67
|
+
})
|
|
68
|
+
return instance
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run a workflow and wait for it to complete.
|
|
73
|
+
*/
|
|
74
|
+
async function runWorkflow<T>(instance: WorkflowInstance, params?: unknown): Promise<T> {
|
|
75
|
+
const status = await instance.status()
|
|
76
|
+
if (status.status === 'queued' || status.status === 'running') {
|
|
77
|
+
let current = status
|
|
78
|
+
while (current.status !== 'complete' && current.status !== 'errored') {
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
80
|
+
current = await instance.status()
|
|
81
|
+
}
|
|
82
|
+
if (current.status === 'errored') {
|
|
83
|
+
const error = current.error as unknown
|
|
84
|
+
let errorMessage: string
|
|
85
|
+
|
|
86
|
+
if (typeof error === 'string') {
|
|
87
|
+
errorMessage = error
|
|
88
|
+
} else if (error && typeof error === 'object') {
|
|
89
|
+
const err = error as Record<string, unknown>
|
|
90
|
+
errorMessage = (err.message ??
|
|
91
|
+
err.name ??
|
|
92
|
+
err.error ??
|
|
93
|
+
(err.cause &&
|
|
94
|
+
typeof err.cause === 'object' &&
|
|
95
|
+
(err.cause as Record<string, unknown>).message) ??
|
|
96
|
+
JSON.stringify(error)) as string
|
|
97
|
+
} else {
|
|
98
|
+
errorMessage = String(error ?? 'Unknown error')
|
|
99
|
+
}
|
|
100
|
+
throw new Error(errorMessage)
|
|
101
|
+
}
|
|
102
|
+
return current.output as T
|
|
103
|
+
}
|
|
104
|
+
return status.output as T
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// 1. DurableStep.cascade() - Static Method Existence
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
describe('DurableStep.cascade()', () => {
|
|
112
|
+
describe('static method existence', () => {
|
|
113
|
+
it('should expose DurableStep.cascade() as a static method', () => {
|
|
114
|
+
// DurableStep.cascade() should be a static factory method
|
|
115
|
+
expect(DurableStep.cascade).toBeDefined()
|
|
116
|
+
expect(typeof DurableStep.cascade).toBe('function')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should create a DurableCascadeStep with cascade configuration', () => {
|
|
120
|
+
const cascadeStep = DurableStep.cascade('process-refund', {
|
|
121
|
+
code: async (input) => ({ approved: true }),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
expect(cascadeStep).toBeDefined()
|
|
125
|
+
expect(cascadeStep.name).toBe('process-refund')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should return a DurableCascadeStep that can be run with workflow step', async () => {
|
|
129
|
+
const cascadeStep = DurableStep.cascade('test-cascade', {
|
|
130
|
+
code: async (input) => ({ result: 'code' }),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Should have a run method like regular DurableStep
|
|
134
|
+
expect(cascadeStep.run).toBeDefined()
|
|
135
|
+
expect(typeof cascadeStep.run).toBe('function')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// 2. Cascade Tier Definition
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
describe('cascade tier definition', () => {
|
|
144
|
+
it('should accept code tier handler', () => {
|
|
145
|
+
const cascadeStep = DurableStep.cascade('code-only', {
|
|
146
|
+
code: async (input: { amount: number }) => {
|
|
147
|
+
if (input.amount < 100) {
|
|
148
|
+
return { approved: true, reason: 'Auto-approved small amount' }
|
|
149
|
+
}
|
|
150
|
+
throw new Error('Amount too large for code tier')
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
expect(cascadeStep).toBeDefined()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should accept generative tier handler with AI context', () => {
|
|
158
|
+
const cascadeStep = DurableStep.cascade('with-generative', {
|
|
159
|
+
code: async (input) => {
|
|
160
|
+
throw new Error('Escalate to generative')
|
|
161
|
+
},
|
|
162
|
+
generative: async (input, ctx) => {
|
|
163
|
+
// ctx should have ai binding for AI Gateway
|
|
164
|
+
const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
165
|
+
messages: [{ role: 'user', content: `Process: ${JSON.stringify(input)}` }],
|
|
166
|
+
})
|
|
167
|
+
return { decision: result.response }
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
expect(cascadeStep).toBeDefined()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should accept agentic tier handler with tools', () => {
|
|
175
|
+
const cascadeStep = DurableStep.cascade('with-agentic', {
|
|
176
|
+
code: async (input) => {
|
|
177
|
+
throw new Error('Escalate to agentic')
|
|
178
|
+
},
|
|
179
|
+
agentic: async (input, ctx) => {
|
|
180
|
+
// Agentic tier can use tools and make multiple AI calls
|
|
181
|
+
const analysis = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
182
|
+
messages: [{ role: 'user', content: 'Analyze this request' }],
|
|
183
|
+
})
|
|
184
|
+
return { agentDecision: analysis.response }
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
expect(cascadeStep).toBeDefined()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should accept human tier handler for human-in-the-loop', () => {
|
|
192
|
+
const cascadeStep = DurableStep.cascade('with-human', {
|
|
193
|
+
code: async (input) => {
|
|
194
|
+
throw new Error('Escalate to human')
|
|
195
|
+
},
|
|
196
|
+
human: async (input, ctx) => {
|
|
197
|
+
// Human tier should create a human review request and wait
|
|
198
|
+
const reviewId = await ctx.requestHumanReview({
|
|
199
|
+
type: 'refund-approval',
|
|
200
|
+
data: input,
|
|
201
|
+
assignee: 'finance-team',
|
|
202
|
+
})
|
|
203
|
+
return { reviewId, status: 'pending-human' }
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
expect(cascadeStep).toBeDefined()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should accept all four tiers in a complete cascade', () => {
|
|
211
|
+
const cascadeStep = DurableStep.cascade('full-cascade', {
|
|
212
|
+
code: async (input) => {
|
|
213
|
+
// Try deterministic rules first
|
|
214
|
+
if (input.amount < 50) return { approved: true }
|
|
215
|
+
throw new Error('Needs AI review')
|
|
216
|
+
},
|
|
217
|
+
generative: async (input, ctx) => {
|
|
218
|
+
// Try simple AI decision
|
|
219
|
+
const result = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
220
|
+
messages: [{ role: 'user', content: 'Should we approve?' }],
|
|
221
|
+
})
|
|
222
|
+
if (result.response.includes('yes')) return { approved: true }
|
|
223
|
+
throw new Error('Needs deeper analysis')
|
|
224
|
+
},
|
|
225
|
+
agentic: async (input, ctx) => {
|
|
226
|
+
// Try agentic reasoning with tools
|
|
227
|
+
const analysis = await ctx.ai.run('@cf/meta/llama-3-8b-instruct', {
|
|
228
|
+
messages: [{ role: 'user', content: 'Deep analysis needed' }],
|
|
229
|
+
})
|
|
230
|
+
if (analysis.response.includes('approve')) return { approved: true }
|
|
231
|
+
throw new Error('Needs human review')
|
|
232
|
+
},
|
|
233
|
+
human: async (input, ctx) => {
|
|
234
|
+
// Fall back to human
|
|
235
|
+
return ctx.requestHumanReview({ type: 'manual-review', data: input })
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
expect(cascadeStep).toBeDefined()
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// 3. Cascade Execution Order
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
describe('cascade execution order', () => {
|
|
248
|
+
it('should execute tiers in order: code -> generative -> agentic -> human', async () => {
|
|
249
|
+
const instance = await getWorkflowInstance('cascade-order-test-1')
|
|
250
|
+
|
|
251
|
+
const result = await runWorkflow<{
|
|
252
|
+
executionOrder: string[]
|
|
253
|
+
finalTier: string
|
|
254
|
+
}>(instance)
|
|
255
|
+
|
|
256
|
+
expect(result.executionOrder).toEqual(['code', 'generative', 'agentic', 'human'])
|
|
257
|
+
expect(result.finalTier).toBe('human')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should short-circuit when a tier succeeds', async () => {
|
|
261
|
+
const instance = await getWorkflowInstance('cascade-shortcircuit-test-1')
|
|
262
|
+
|
|
263
|
+
const result = await runWorkflow<{
|
|
264
|
+
executedTiers: string[]
|
|
265
|
+
successTier: string
|
|
266
|
+
value: unknown
|
|
267
|
+
}>(instance)
|
|
268
|
+
|
|
269
|
+
// If code tier succeeds, should not execute other tiers
|
|
270
|
+
expect(result.executedTiers).toEqual(['code'])
|
|
271
|
+
expect(result.successTier).toBe('code')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should escalate to next tier on failure', async () => {
|
|
275
|
+
const instance = await getWorkflowInstance('cascade-escalate-test-1')
|
|
276
|
+
|
|
277
|
+
const result = await runWorkflow<{
|
|
278
|
+
executedTiers: string[]
|
|
279
|
+
successTier: string
|
|
280
|
+
errors: Array<{ tier: string; error: string }>
|
|
281
|
+
}>(instance)
|
|
282
|
+
|
|
283
|
+
// Code fails, generative succeeds
|
|
284
|
+
expect(result.executedTiers).toEqual(['code', 'generative'])
|
|
285
|
+
expect(result.successTier).toBe('generative')
|
|
286
|
+
expect(result.errors).toHaveLength(1)
|
|
287
|
+
expect(result.errors[0].tier).toBe('code')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should skip unconfigured tiers gracefully', async () => {
|
|
291
|
+
const instance = await getWorkflowInstance('cascade-skip-test-1')
|
|
292
|
+
|
|
293
|
+
const result = await runWorkflow<{
|
|
294
|
+
executedTiers: string[]
|
|
295
|
+
skippedTiers: string[]
|
|
296
|
+
successTier: string
|
|
297
|
+
}>(instance)
|
|
298
|
+
|
|
299
|
+
// Only code and human configured, generative and agentic skipped
|
|
300
|
+
expect(result.skippedTiers).toContain('generative')
|
|
301
|
+
expect(result.skippedTiers).toContain('agentic')
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// 4. AI Tier -> Human Tier Fallback
|
|
307
|
+
// ============================================================================
|
|
308
|
+
|
|
309
|
+
describe('AI to human fallback', () => {
|
|
310
|
+
it('should escalate from generative to human when AI fails', async () => {
|
|
311
|
+
const instance = await getWorkflowInstance('ai-human-fallback-test-1')
|
|
312
|
+
|
|
313
|
+
const result = await runWorkflow<{
|
|
314
|
+
aiTierFailed: boolean
|
|
315
|
+
humanTierInvoked: boolean
|
|
316
|
+
finalResult: unknown
|
|
317
|
+
}>(instance)
|
|
318
|
+
|
|
319
|
+
expect(result.aiTierFailed).toBe(true)
|
|
320
|
+
expect(result.humanTierInvoked).toBe(true)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('should preserve AI tier error context for human review', async () => {
|
|
324
|
+
const instance = await getWorkflowInstance('ai-error-context-test-1')
|
|
325
|
+
|
|
326
|
+
const result = await runWorkflow<{
|
|
327
|
+
humanReviewContext: {
|
|
328
|
+
previousTierErrors: Array<{
|
|
329
|
+
tier: string
|
|
330
|
+
error: string
|
|
331
|
+
attempt: number
|
|
332
|
+
}>
|
|
333
|
+
}
|
|
334
|
+
}>(instance)
|
|
335
|
+
|
|
336
|
+
expect(result.humanReviewContext.previousTierErrors).toBeDefined()
|
|
337
|
+
expect(result.humanReviewContext.previousTierErrors.length).toBeGreaterThan(0)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should include AI reasoning in human escalation', async () => {
|
|
341
|
+
const instance = await getWorkflowInstance('ai-reasoning-test-1')
|
|
342
|
+
|
|
343
|
+
const result = await runWorkflow<{
|
|
344
|
+
humanReviewData: {
|
|
345
|
+
aiAttempts: Array<{
|
|
346
|
+
tier: string
|
|
347
|
+
reasoning: string
|
|
348
|
+
confidence: number
|
|
349
|
+
}>
|
|
350
|
+
escalationReason: string
|
|
351
|
+
}
|
|
352
|
+
}>(instance)
|
|
353
|
+
|
|
354
|
+
expect(result.humanReviewData.aiAttempts).toBeDefined()
|
|
355
|
+
expect(result.humanReviewData.escalationReason).toBeDefined()
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('should support custom escalation conditions', async () => {
|
|
359
|
+
const instance = await getWorkflowInstance('custom-escalation-test-1')
|
|
360
|
+
|
|
361
|
+
// Cascade with custom condition: escalate if confidence < 0.8
|
|
362
|
+
const result = await runWorkflow<{
|
|
363
|
+
aiConfidence: number
|
|
364
|
+
escalatedDueToLowConfidence: boolean
|
|
365
|
+
humanInvoked: boolean
|
|
366
|
+
}>(instance)
|
|
367
|
+
|
|
368
|
+
expect(result.aiConfidence).toBeLessThan(0.8)
|
|
369
|
+
expect(result.escalatedDueToLowConfidence).toBe(true)
|
|
370
|
+
expect(result.humanInvoked).toBe(true)
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// 5. Multiple AI Providers (Fast Model -> Slow Model)
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
describe('multiple AI providers cascade', () => {
|
|
379
|
+
it('should support fast model before slow model in generative tier', async () => {
|
|
380
|
+
const instance = await getWorkflowInstance('fast-slow-model-test-1')
|
|
381
|
+
|
|
382
|
+
const result = await runWorkflow<{
|
|
383
|
+
modelUsed: string
|
|
384
|
+
attemptedModels: string[]
|
|
385
|
+
response: string
|
|
386
|
+
}>(instance)
|
|
387
|
+
|
|
388
|
+
// Fast model should be tried first
|
|
389
|
+
expect(result.attemptedModels[0]).toBe('@cf/meta/llama-3-8b-instruct')
|
|
390
|
+
// If fast fails, slow model
|
|
391
|
+
expect(result.attemptedModels).toContain('@cf/meta/llama-3-70b-instruct')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('should cascade through model tiers within generative tier', async () => {
|
|
395
|
+
const instance = await getWorkflowInstance('model-cascade-test-1')
|
|
396
|
+
|
|
397
|
+
const result = await runWorkflow<{
|
|
398
|
+
modelAttempts: Array<{
|
|
399
|
+
model: string
|
|
400
|
+
success: boolean
|
|
401
|
+
latencyMs: number
|
|
402
|
+
}>
|
|
403
|
+
finalModel: string
|
|
404
|
+
}>(instance)
|
|
405
|
+
|
|
406
|
+
expect(result.modelAttempts.length).toBeGreaterThan(1)
|
|
407
|
+
expect(result.finalModel).toBeDefined()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should support custom model ordering', async () => {
|
|
411
|
+
const instance = await getWorkflowInstance('custom-model-order-test-1')
|
|
412
|
+
|
|
413
|
+
const result = await runWorkflow<{
|
|
414
|
+
modelOrder: string[]
|
|
415
|
+
selectedModel: string
|
|
416
|
+
}>(instance)
|
|
417
|
+
|
|
418
|
+
// Custom order: cheapest -> balanced -> premium
|
|
419
|
+
expect(result.modelOrder).toEqual([
|
|
420
|
+
'@cf/meta/llama-3-8b-instruct',
|
|
421
|
+
'@cf/mistral/mistral-7b-instruct-v0.1',
|
|
422
|
+
'@cf/meta/llama-3-70b-instruct',
|
|
423
|
+
])
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should respect model-specific timeouts', async () => {
|
|
427
|
+
const instance = await getWorkflowInstance('model-timeout-test-1')
|
|
428
|
+
|
|
429
|
+
const result = await runWorkflow<{
|
|
430
|
+
modelResults: Array<{
|
|
431
|
+
model: string
|
|
432
|
+
timedOut: boolean
|
|
433
|
+
timeoutMs: number
|
|
434
|
+
}>
|
|
435
|
+
}>(instance)
|
|
436
|
+
|
|
437
|
+
// Fast model has shorter timeout than slow model
|
|
438
|
+
const fastModel = result.modelResults.find((m) => m.model.includes('8b'))
|
|
439
|
+
const slowModel = result.modelResults.find((m) => m.model.includes('70b'))
|
|
440
|
+
|
|
441
|
+
expect(fastModel?.timeoutMs).toBeLessThan(slowModel?.timeoutMs ?? 0)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('should use AI Gateway caching for deterministic tests', async () => {
|
|
445
|
+
const instance = await getWorkflowInstance('ai-gateway-cache-test-1')
|
|
446
|
+
|
|
447
|
+
const result = await runWorkflow<{
|
|
448
|
+
cacheHit: boolean
|
|
449
|
+
cachedResponse: string
|
|
450
|
+
responseTime: number
|
|
451
|
+
}>(instance)
|
|
452
|
+
|
|
453
|
+
// Second request should hit cache
|
|
454
|
+
expect(result.cacheHit).toBe(true)
|
|
455
|
+
expect(result.responseTime).toBeLessThan(100) // Cached response should be fast
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// 6. Tier Timeout Configuration
|
|
461
|
+
// ============================================================================
|
|
462
|
+
|
|
463
|
+
describe('tier timeout configuration', () => {
|
|
464
|
+
it('should support per-tier timeout configuration', async () => {
|
|
465
|
+
const instance = await getWorkflowInstance('tier-timeout-config-test-1')
|
|
466
|
+
|
|
467
|
+
const result = await runWorkflow<{
|
|
468
|
+
tierTimeouts: Record<string, number>
|
|
469
|
+
appliedTimeouts: Record<string, number>
|
|
470
|
+
}>(instance)
|
|
471
|
+
|
|
472
|
+
expect(result.tierTimeouts).toEqual({
|
|
473
|
+
code: 5000,
|
|
474
|
+
generative: 30000,
|
|
475
|
+
agentic: 300000,
|
|
476
|
+
human: 86400000,
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should use default timeouts when not specified', async () => {
|
|
481
|
+
const instance = await getWorkflowInstance('default-timeout-test-1')
|
|
482
|
+
|
|
483
|
+
const result = await runWorkflow<{
|
|
484
|
+
usedDefaults: boolean
|
|
485
|
+
defaultTimeouts: Record<string, number>
|
|
486
|
+
}>(instance)
|
|
487
|
+
|
|
488
|
+
expect(result.usedDefaults).toBe(true)
|
|
489
|
+
expect(result.defaultTimeouts.code).toBe(5000)
|
|
490
|
+
expect(result.defaultTimeouts.generative).toBe(30000)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('should escalate on tier timeout', async () => {
|
|
494
|
+
const instance = await getWorkflowInstance('timeout-escalation-test-1')
|
|
495
|
+
|
|
496
|
+
const result = await runWorkflow<{
|
|
497
|
+
timedOutTier: string
|
|
498
|
+
escalatedToTier: string
|
|
499
|
+
timeoutError: string
|
|
500
|
+
}>(instance)
|
|
501
|
+
|
|
502
|
+
expect(result.timedOutTier).toBe('code')
|
|
503
|
+
expect(result.escalatedToTier).toBe('generative')
|
|
504
|
+
expect(result.timeoutError).toContain('timeout')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('should record timeout in tier result', async () => {
|
|
508
|
+
const instance = await getWorkflowInstance('timeout-record-test-1')
|
|
509
|
+
|
|
510
|
+
const result = await runWorkflow<{
|
|
511
|
+
tierResults: Array<{
|
|
512
|
+
tier: string
|
|
513
|
+
timedOut: boolean
|
|
514
|
+
duration: number
|
|
515
|
+
configuredTimeout: number
|
|
516
|
+
}>
|
|
517
|
+
}>(instance)
|
|
518
|
+
|
|
519
|
+
const timedOutTier = result.tierResults.find((t) => t.timedOut)
|
|
520
|
+
expect(timedOutTier).toBeDefined()
|
|
521
|
+
expect(timedOutTier?.duration).toBeGreaterThanOrEqual(timedOutTier?.configuredTimeout ?? 0)
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('should support total cascade timeout', async () => {
|
|
525
|
+
const instance = await getWorkflowInstance('total-timeout-test-1')
|
|
526
|
+
|
|
527
|
+
// Cascade with 60s total timeout
|
|
528
|
+
await expect(runWorkflow(instance)).rejects.toThrow(/cascade.*timeout/i)
|
|
529
|
+
})
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// 7. Tier Success Conditions
|
|
534
|
+
// ============================================================================
|
|
535
|
+
|
|
536
|
+
describe('tier success conditions', () => {
|
|
537
|
+
it('should treat returned value as success', async () => {
|
|
538
|
+
const instance = await getWorkflowInstance('return-success-test-1')
|
|
539
|
+
|
|
540
|
+
const result = await runWorkflow<{
|
|
541
|
+
tierStatus: string
|
|
542
|
+
returnedValue: unknown
|
|
543
|
+
}>(instance)
|
|
544
|
+
|
|
545
|
+
expect(result.tierStatus).toBe('success')
|
|
546
|
+
expect(result.returnedValue).toBeDefined()
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('should treat thrown error as failure and escalate', async () => {
|
|
550
|
+
const instance = await getWorkflowInstance('throw-failure-test-1')
|
|
551
|
+
|
|
552
|
+
const result = await runWorkflow<{
|
|
553
|
+
failedTier: string
|
|
554
|
+
escalatedTo: string
|
|
555
|
+
error: string
|
|
556
|
+
}>(instance)
|
|
557
|
+
|
|
558
|
+
expect(result.failedTier).toBe('code')
|
|
559
|
+
expect(result.escalatedTo).toBe('generative')
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('should support custom success condition function', async () => {
|
|
563
|
+
const instance = await getWorkflowInstance('custom-success-test-1')
|
|
564
|
+
|
|
565
|
+
const result = await runWorkflow<{
|
|
566
|
+
tierResult: { confidence: number }
|
|
567
|
+
customConditionResult: boolean
|
|
568
|
+
finalStatus: string
|
|
569
|
+
}>(instance)
|
|
570
|
+
|
|
571
|
+
// Custom condition: success only if confidence > 0.9
|
|
572
|
+
expect(result.tierResult.confidence).toBeLessThan(0.9)
|
|
573
|
+
expect(result.customConditionResult).toBe(false)
|
|
574
|
+
expect(result.finalStatus).toBe('escalated')
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it('should support partial success with escalation', async () => {
|
|
578
|
+
const instance = await getWorkflowInstance('partial-success-test-1')
|
|
579
|
+
|
|
580
|
+
const result = await runWorkflow<{
|
|
581
|
+
partialResult: { approved: boolean; confidence: number }
|
|
582
|
+
needsHumanReview: boolean
|
|
583
|
+
escalatedWithPartialResult: boolean
|
|
584
|
+
}>(instance)
|
|
585
|
+
|
|
586
|
+
// Tier returned a result but flagged for human review
|
|
587
|
+
expect(result.partialResult.approved).toBe(true)
|
|
588
|
+
expect(result.partialResult.confidence).toBeLessThan(0.8)
|
|
589
|
+
expect(result.needsHumanReview).toBe(true)
|
|
590
|
+
expect(result.escalatedWithPartialResult).toBe(true)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('should support retry before escalation', async () => {
|
|
594
|
+
const instance = await getWorkflowInstance('retry-before-escalate-test-1')
|
|
595
|
+
|
|
596
|
+
const result = await runWorkflow<{
|
|
597
|
+
tierAttempts: number
|
|
598
|
+
maxRetries: number
|
|
599
|
+
finallyEscalated: boolean
|
|
600
|
+
}>(instance)
|
|
601
|
+
|
|
602
|
+
// Should retry 3 times before escalating
|
|
603
|
+
expect(result.tierAttempts).toBe(3)
|
|
604
|
+
expect(result.maxRetries).toBe(3)
|
|
605
|
+
expect(result.finallyEscalated).toBe(true)
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// 8. Tier Result Merging
|
|
611
|
+
// ============================================================================
|
|
612
|
+
|
|
613
|
+
describe('tier result merging', () => {
|
|
614
|
+
it('should accumulate results from all attempted tiers', async () => {
|
|
615
|
+
const instance = await getWorkflowInstance('result-accumulate-test-1')
|
|
616
|
+
|
|
617
|
+
const result = await runWorkflow<{
|
|
618
|
+
allTierResults: Array<{
|
|
619
|
+
tier: string
|
|
620
|
+
result: unknown
|
|
621
|
+
status: string
|
|
622
|
+
}>
|
|
623
|
+
}>(instance)
|
|
624
|
+
|
|
625
|
+
expect(result.allTierResults.length).toBeGreaterThan(1)
|
|
626
|
+
expect(result.allTierResults.every((t) => t.tier && t.status)).toBe(true)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('should merge partial results into final result', async () => {
|
|
630
|
+
const instance = await getWorkflowInstance('result-merge-test-1')
|
|
631
|
+
|
|
632
|
+
const result = await runWorkflow<{
|
|
633
|
+
mergedResult: {
|
|
634
|
+
codeAnalysis: unknown
|
|
635
|
+
aiRecommendation: unknown
|
|
636
|
+
humanDecision: unknown
|
|
637
|
+
}
|
|
638
|
+
contributingTiers: string[]
|
|
639
|
+
}>(instance)
|
|
640
|
+
|
|
641
|
+
// Each tier contributes to the final merged result
|
|
642
|
+
expect(result.mergedResult.codeAnalysis).toBeDefined()
|
|
643
|
+
expect(result.contributingTiers.length).toBeGreaterThan(1)
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
it('should provide access to individual tier results', async () => {
|
|
647
|
+
const instance = await getWorkflowInstance('individual-results-test-1')
|
|
648
|
+
|
|
649
|
+
const result = await runWorkflow<CascadeResult<unknown>>(instance)
|
|
650
|
+
|
|
651
|
+
expect(result.history).toBeDefined()
|
|
652
|
+
expect(Array.isArray(result.history)).toBe(true)
|
|
653
|
+
expect(result.history[0]).toHaveProperty('tier')
|
|
654
|
+
expect(result.history[0]).toHaveProperty('success')
|
|
655
|
+
expect(result.history[0]).toHaveProperty('duration')
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it('should support custom result merger function', async () => {
|
|
659
|
+
const instance = await getWorkflowInstance('custom-merger-test-1')
|
|
660
|
+
|
|
661
|
+
const result = await runWorkflow<{
|
|
662
|
+
customMergedResult: {
|
|
663
|
+
consensus: string
|
|
664
|
+
sources: string[]
|
|
665
|
+
}
|
|
666
|
+
}>(instance)
|
|
667
|
+
|
|
668
|
+
expect(result.customMergedResult.consensus).toBeDefined()
|
|
669
|
+
expect(result.customMergedResult.sources.length).toBeGreaterThan(0)
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
it('should preserve tier metadata in results', async () => {
|
|
673
|
+
const instance = await getWorkflowInstance('tier-metadata-test-1')
|
|
674
|
+
|
|
675
|
+
const result = await runWorkflow<{
|
|
676
|
+
tierMetadata: Array<{
|
|
677
|
+
tier: string
|
|
678
|
+
startTime: number
|
|
679
|
+
endTime: number
|
|
680
|
+
latencyMs: number
|
|
681
|
+
attempts: number
|
|
682
|
+
}>
|
|
683
|
+
}>(instance)
|
|
684
|
+
|
|
685
|
+
expect(result.tierMetadata.every((m) => m.latencyMs >= 0)).toBe(true)
|
|
686
|
+
expect(result.tierMetadata.every((m) => m.attempts >= 1)).toBe(true)
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
// ============================================================================
|
|
691
|
+
// 9. Error Propagation Through Tiers
|
|
692
|
+
// ============================================================================
|
|
693
|
+
|
|
694
|
+
describe('error propagation through tiers', () => {
|
|
695
|
+
it('should propagate error to next tier with context', async () => {
|
|
696
|
+
const instance = await getWorkflowInstance('error-propagate-test-1')
|
|
697
|
+
|
|
698
|
+
const result = await runWorkflow<{
|
|
699
|
+
receivedErrors: Array<{
|
|
700
|
+
fromTier: string
|
|
701
|
+
error: string
|
|
702
|
+
}>
|
|
703
|
+
currentTier: string
|
|
704
|
+
}>(instance)
|
|
705
|
+
|
|
706
|
+
expect(result.receivedErrors.length).toBeGreaterThan(0)
|
|
707
|
+
expect(result.receivedErrors[0].fromTier).toBeDefined()
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
it('should accumulate errors from all failed tiers', async () => {
|
|
711
|
+
const instance = await getWorkflowInstance('error-accumulate-test-1')
|
|
712
|
+
|
|
713
|
+
const result = await runWorkflow<{
|
|
714
|
+
allErrors: Array<{
|
|
715
|
+
tier: string
|
|
716
|
+
error: string
|
|
717
|
+
timestamp: number
|
|
718
|
+
}>
|
|
719
|
+
totalFailures: number
|
|
720
|
+
}>(instance)
|
|
721
|
+
|
|
722
|
+
expect(result.allErrors.length).toBe(result.totalFailures)
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('should throw AllTiersFailedError when all tiers fail', async () => {
|
|
726
|
+
const instance = await getWorkflowInstance('all-tiers-fail-test-1')
|
|
727
|
+
|
|
728
|
+
await expect(runWorkflow(instance)).rejects.toThrow(/all.*tiers.*failed/i)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
it('should include error history in AllTiersFailedError', async () => {
|
|
732
|
+
const instance = await getWorkflowInstance('error-history-test-1')
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
await runWorkflow(instance)
|
|
736
|
+
expect.fail('Should have thrown')
|
|
737
|
+
} catch (error: unknown) {
|
|
738
|
+
const err = error as Error & { history?: Array<{ tier: string; error: Error }> }
|
|
739
|
+
expect(err.history).toBeDefined()
|
|
740
|
+
expect(err.history?.length).toBeGreaterThan(0)
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
it('should support custom error handlers per tier', async () => {
|
|
745
|
+
const instance = await getWorkflowInstance('custom-error-handler-test-1')
|
|
746
|
+
|
|
747
|
+
const result = await runWorkflow<{
|
|
748
|
+
errorHandled: boolean
|
|
749
|
+
handlerTier: string
|
|
750
|
+
recoveredValue: unknown
|
|
751
|
+
}>(instance)
|
|
752
|
+
|
|
753
|
+
expect(result.errorHandled).toBe(true)
|
|
754
|
+
expect(result.recoveredValue).toBeDefined()
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('should support error transformation between tiers', async () => {
|
|
758
|
+
const instance = await getWorkflowInstance('error-transform-test-1')
|
|
759
|
+
|
|
760
|
+
const result = await runWorkflow<{
|
|
761
|
+
originalError: string
|
|
762
|
+
transformedError: string
|
|
763
|
+
transformedForHuman: boolean
|
|
764
|
+
}>(instance)
|
|
765
|
+
|
|
766
|
+
// Technical error transformed to human-readable
|
|
767
|
+
expect(result.originalError).toContain('ECONNREFUSED')
|
|
768
|
+
expect(result.transformedError).toContain('service temporarily unavailable')
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
// ============================================================================
|
|
773
|
+
// 10. Durable Cascade Checkpoints
|
|
774
|
+
// ============================================================================
|
|
775
|
+
|
|
776
|
+
describe('durable cascade checkpoints', () => {
|
|
777
|
+
it('should persist tier results as durable checkpoints', async () => {
|
|
778
|
+
const instance = await getWorkflowInstance('durable-checkpoint-test-1')
|
|
779
|
+
|
|
780
|
+
const result = await runWorkflow<{
|
|
781
|
+
checkpointsCreated: number
|
|
782
|
+
checkpointIds: string[]
|
|
783
|
+
}>(instance)
|
|
784
|
+
|
|
785
|
+
expect(result.checkpointsCreated).toBeGreaterThan(0)
|
|
786
|
+
expect(result.checkpointIds.length).toBe(result.checkpointsCreated)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('should resume cascade from last successful tier on restart', async () => {
|
|
790
|
+
const instance = await getWorkflowInstance('cascade-resume-test-1')
|
|
791
|
+
|
|
792
|
+
const result = await runWorkflow<{
|
|
793
|
+
resumedFromTier: string
|
|
794
|
+
tiersReExecuted: string[]
|
|
795
|
+
tiersSkipped: string[]
|
|
796
|
+
}>(instance)
|
|
797
|
+
|
|
798
|
+
// Should not re-execute already completed tiers
|
|
799
|
+
expect(result.tiersSkipped.length).toBeGreaterThan(0)
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
it('should store tier inputs and outputs durably', async () => {
|
|
803
|
+
const instance = await getWorkflowInstance('durable-io-test-1')
|
|
804
|
+
|
|
805
|
+
const result = await runWorkflow<{
|
|
806
|
+
storedTierData: Array<{
|
|
807
|
+
tier: string
|
|
808
|
+
input: unknown
|
|
809
|
+
output: unknown
|
|
810
|
+
storedAt: number
|
|
811
|
+
}>
|
|
812
|
+
}>(instance)
|
|
813
|
+
|
|
814
|
+
expect(result.storedTierData.every((d) => d.input !== undefined)).toBe(true)
|
|
815
|
+
expect(result.storedTierData.every((d) => d.output !== undefined)).toBe(true)
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('should support snapshot and restore of cascade state', async () => {
|
|
819
|
+
const instance = await getWorkflowInstance('cascade-snapshot-test-1')
|
|
820
|
+
|
|
821
|
+
const result = await runWorkflow<{
|
|
822
|
+
snapshotId: string
|
|
823
|
+
restoredFromSnapshot: boolean
|
|
824
|
+
stateAfterRestore: {
|
|
825
|
+
currentTier: string
|
|
826
|
+
completedTiers: string[]
|
|
827
|
+
}
|
|
828
|
+
}>(instance)
|
|
829
|
+
|
|
830
|
+
expect(result.snapshotId).toBeDefined()
|
|
831
|
+
expect(result.restoredFromSnapshot).toBe(true)
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
// ============================================================================
|
|
836
|
+
// 11. 5W+H Audit Trail
|
|
837
|
+
// ============================================================================
|
|
838
|
+
|
|
839
|
+
describe('5W+H audit trail', () => {
|
|
840
|
+
it('should record Who (actor) for each tier execution', async () => {
|
|
841
|
+
const instance = await getWorkflowInstance('audit-who-test-1')
|
|
842
|
+
|
|
843
|
+
const result = await runWorkflow<{
|
|
844
|
+
auditEvents: Array<{
|
|
845
|
+
who: string
|
|
846
|
+
tier: string
|
|
847
|
+
}>
|
|
848
|
+
}>(instance)
|
|
849
|
+
|
|
850
|
+
expect(result.auditEvents.every((e) => e.who !== undefined)).toBe(true)
|
|
851
|
+
// Different actors for different tiers
|
|
852
|
+
expect(result.auditEvents.some((e) => e.who === 'system')).toBe(true)
|
|
853
|
+
expect(
|
|
854
|
+
result.auditEvents.some((e) => e.who.includes('human') || e.who.includes('user'))
|
|
855
|
+
).toBe(true)
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
it('should record What (action) for each tier', async () => {
|
|
859
|
+
const instance = await getWorkflowInstance('audit-what-test-1')
|
|
860
|
+
|
|
861
|
+
const result = await runWorkflow<{
|
|
862
|
+
auditEvents: Array<{
|
|
863
|
+
what: string
|
|
864
|
+
tier: string
|
|
865
|
+
}>
|
|
866
|
+
}>(instance)
|
|
867
|
+
|
|
868
|
+
expect(result.auditEvents.find((e) => e.tier === 'code')?.what).toContain('execute')
|
|
869
|
+
expect(result.auditEvents.find((e) => e.tier === 'generative')?.what).toContain('ai')
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it('should record When (timestamp) for all events', async () => {
|
|
873
|
+
const instance = await getWorkflowInstance('audit-when-test-1')
|
|
874
|
+
|
|
875
|
+
const result = await runWorkflow<{
|
|
876
|
+
auditEvents: Array<{
|
|
877
|
+
when: number
|
|
878
|
+
tier: string
|
|
879
|
+
}>
|
|
880
|
+
}>(instance)
|
|
881
|
+
|
|
882
|
+
// Events should be in chronological order
|
|
883
|
+
const timestamps = result.auditEvents.map((e) => e.when)
|
|
884
|
+
const sorted = [...timestamps].sort((a, b) => a - b)
|
|
885
|
+
expect(timestamps).toEqual(sorted)
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
it('should record Where (cascade context) for each event', async () => {
|
|
889
|
+
const instance = await getWorkflowInstance('audit-where-test-1')
|
|
890
|
+
|
|
891
|
+
const result = await runWorkflow<{
|
|
892
|
+
auditEvents: Array<{
|
|
893
|
+
where: string
|
|
894
|
+
cascadeName: string
|
|
895
|
+
workflowId: string
|
|
896
|
+
}>
|
|
897
|
+
}>(instance)
|
|
898
|
+
|
|
899
|
+
expect(result.auditEvents.every((e) => e.where === result.auditEvents[0].where)).toBe(true)
|
|
900
|
+
expect(result.auditEvents[0].cascadeName).toBeDefined()
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
it('should record Why (reason) for escalations', async () => {
|
|
904
|
+
const instance = await getWorkflowInstance('audit-why-test-1')
|
|
905
|
+
|
|
906
|
+
const result = await runWorkflow<{
|
|
907
|
+
escalationEvents: Array<{
|
|
908
|
+
why: string
|
|
909
|
+
fromTier: string
|
|
910
|
+
toTier: string
|
|
911
|
+
}>
|
|
912
|
+
}>(instance)
|
|
913
|
+
|
|
914
|
+
expect(result.escalationEvents.length).toBeGreaterThan(0)
|
|
915
|
+
expect(result.escalationEvents.every((e) => e.why !== undefined && e.why !== '')).toBe(true)
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
it('should record How (method/details) for tier execution', async () => {
|
|
919
|
+
const instance = await getWorkflowInstance('audit-how-test-1')
|
|
920
|
+
|
|
921
|
+
const result = await runWorkflow<{
|
|
922
|
+
auditEvents: Array<{
|
|
923
|
+
how: {
|
|
924
|
+
status: string
|
|
925
|
+
duration: number
|
|
926
|
+
metadata: Record<string, unknown>
|
|
927
|
+
}
|
|
928
|
+
}>
|
|
929
|
+
}>(instance)
|
|
930
|
+
|
|
931
|
+
expect(result.auditEvents.every((e) => e.how.status !== undefined)).toBe(true)
|
|
932
|
+
expect(result.auditEvents.every((e) => e.how.duration >= 0)).toBe(true)
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('should persist audit trail durably', async () => {
|
|
936
|
+
const instance = await getWorkflowInstance('audit-persist-test-1')
|
|
937
|
+
|
|
938
|
+
const result = await runWorkflow<{
|
|
939
|
+
auditTrailPersisted: boolean
|
|
940
|
+
auditRecordCount: number
|
|
941
|
+
canQueryAuditHistory: boolean
|
|
942
|
+
}>(instance)
|
|
943
|
+
|
|
944
|
+
expect(result.auditTrailPersisted).toBe(true)
|
|
945
|
+
expect(result.auditRecordCount).toBeGreaterThan(0)
|
|
946
|
+
expect(result.canQueryAuditHistory).toBe(true)
|
|
947
|
+
})
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
// ============================================================================
|
|
951
|
+
// 12. AI Gateway Integration
|
|
952
|
+
// ============================================================================
|
|
953
|
+
|
|
954
|
+
describe('AI Gateway integration', () => {
|
|
955
|
+
it('should use AI Gateway binding for generative tier', async () => {
|
|
956
|
+
const instance = await getWorkflowInstance('ai-gateway-binding-test-1')
|
|
957
|
+
|
|
958
|
+
const result = await runWorkflow<{
|
|
959
|
+
usedAiGateway: boolean
|
|
960
|
+
gatewayResponse: unknown
|
|
961
|
+
}>(instance)
|
|
962
|
+
|
|
963
|
+
expect(result.usedAiGateway).toBe(true)
|
|
964
|
+
expect(result.gatewayResponse).toBeDefined()
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
it('should support AI Gateway caching for deterministic tests', async () => {
|
|
968
|
+
const instance = await getWorkflowInstance('ai-gateway-caching-test-1')
|
|
969
|
+
|
|
970
|
+
const result = await runWorkflow<{
|
|
971
|
+
firstCallCached: boolean
|
|
972
|
+
secondCallFromCache: boolean
|
|
973
|
+
responsesMatch: boolean
|
|
974
|
+
}>(instance)
|
|
975
|
+
|
|
976
|
+
// First call should cache, second should hit cache
|
|
977
|
+
expect(result.secondCallFromCache).toBe(true)
|
|
978
|
+
expect(result.responsesMatch).toBe(true)
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
it('should provide AI context to tier handlers', async () => {
|
|
982
|
+
const instance = await getWorkflowInstance('ai-context-test-1')
|
|
983
|
+
|
|
984
|
+
const result = await runWorkflow<{
|
|
985
|
+
contextHasAi: boolean
|
|
986
|
+
aiBindingType: string
|
|
987
|
+
}>(instance)
|
|
988
|
+
|
|
989
|
+
expect(result.contextHasAi).toBe(true)
|
|
990
|
+
expect(result.aiBindingType).toBe('function')
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
it('should handle AI Gateway errors gracefully', async () => {
|
|
994
|
+
const instance = await getWorkflowInstance('ai-gateway-error-test-1')
|
|
995
|
+
|
|
996
|
+
const result = await runWorkflow<{
|
|
997
|
+
aiGatewayFailed: boolean
|
|
998
|
+
escalatedAfterAiFailure: boolean
|
|
999
|
+
errorCaptured: string
|
|
1000
|
+
}>(instance)
|
|
1001
|
+
|
|
1002
|
+
expect(result.aiGatewayFailed).toBe(true)
|
|
1003
|
+
expect(result.escalatedAfterAiFailure).toBe(true)
|
|
1004
|
+
})
|
|
1005
|
+
})
|
|
1006
|
+
|
|
1007
|
+
// ============================================================================
|
|
1008
|
+
// 13. Integration with Existing CascadeExecutor
|
|
1009
|
+
// ============================================================================
|
|
1010
|
+
|
|
1011
|
+
describe('CascadeExecutor integration', () => {
|
|
1012
|
+
it('should use CascadeContext for tracing', async () => {
|
|
1013
|
+
const instance = await getWorkflowInstance('cascade-context-test-1')
|
|
1014
|
+
|
|
1015
|
+
const result = await runWorkflow<{
|
|
1016
|
+
cascadeContext: {
|
|
1017
|
+
correlationId: string
|
|
1018
|
+
steps: Array<{ name: string; status: string }>
|
|
1019
|
+
}
|
|
1020
|
+
}>(instance)
|
|
1021
|
+
|
|
1022
|
+
expect(result.cascadeContext.correlationId).toBeDefined()
|
|
1023
|
+
expect(result.cascadeContext.steps.length).toBeGreaterThan(0)
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
it('should emit FiveWH events via onEvent callback', async () => {
|
|
1027
|
+
const instance = await getWorkflowInstance('fivewh-events-test-1')
|
|
1028
|
+
|
|
1029
|
+
const result = await runWorkflow<{
|
|
1030
|
+
eventsEmitted: number
|
|
1031
|
+
eventTypes: string[]
|
|
1032
|
+
}>(instance)
|
|
1033
|
+
|
|
1034
|
+
expect(result.eventsEmitted).toBeGreaterThan(0)
|
|
1035
|
+
expect(result.eventTypes).toContain('tier-code-execute')
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
it('should provide execution metrics', async () => {
|
|
1039
|
+
const instance = await getWorkflowInstance('metrics-test-1')
|
|
1040
|
+
|
|
1041
|
+
const result = await runWorkflow<{
|
|
1042
|
+
metrics: {
|
|
1043
|
+
totalDuration: number
|
|
1044
|
+
tierDurations: Record<string, number>
|
|
1045
|
+
}
|
|
1046
|
+
}>(instance)
|
|
1047
|
+
|
|
1048
|
+
expect(result.metrics.totalDuration).toBeGreaterThan(0)
|
|
1049
|
+
expect(Object.keys(result.metrics.tierDurations).length).toBeGreaterThan(0)
|
|
1050
|
+
})
|
|
1051
|
+
})
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
// ============================================================================
|
|
1055
|
+
// 14. CascadeConfig Type Tests (Compile-time)
|
|
1056
|
+
// ============================================================================
|
|
1057
|
+
|
|
1058
|
+
describe('CascadeConfig types', () => {
|
|
1059
|
+
it('should define CascadeTierConfig type', () => {
|
|
1060
|
+
// This is a compile-time type test
|
|
1061
|
+
const tierConfig: CascadeTierConfig = {
|
|
1062
|
+
timeout: 5000,
|
|
1063
|
+
retries: { limit: 3, delay: 1000, backoff: 'exponential' },
|
|
1064
|
+
successCondition: (result) => result.confidence > 0.9,
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
expect(tierConfig.timeout).toBe(5000)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
it('should define CascadeConfig with all tiers', () => {
|
|
1071
|
+
const config: CascadeConfig<{ approved: boolean }> = {
|
|
1072
|
+
code: async (input) => ({ approved: true }),
|
|
1073
|
+
generative: async (input, ctx) => ({ approved: true }),
|
|
1074
|
+
agentic: async (input, ctx) => ({ approved: true }),
|
|
1075
|
+
human: async (input, ctx) => ({ approved: true }),
|
|
1076
|
+
timeouts: {
|
|
1077
|
+
code: 5000,
|
|
1078
|
+
generative: 30000,
|
|
1079
|
+
agentic: 300000,
|
|
1080
|
+
human: 86400000,
|
|
1081
|
+
},
|
|
1082
|
+
totalTimeout: 100000,
|
|
1083
|
+
onEvent: (event) => console.log(event),
|
|
1084
|
+
resultMerger: (results) => results[results.length - 1]?.value ?? { approved: false },
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
expect(config.code).toBeDefined()
|
|
1088
|
+
expect(config.timeouts?.code).toBe(5000)
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
it('should define CascadeResult type', () => {
|
|
1092
|
+
const result: CascadeResult<{ approved: boolean }> = {
|
|
1093
|
+
value: { approved: true },
|
|
1094
|
+
tier: 'code',
|
|
1095
|
+
history: [
|
|
1096
|
+
{
|
|
1097
|
+
tier: 'code',
|
|
1098
|
+
success: true,
|
|
1099
|
+
value: { approved: true },
|
|
1100
|
+
duration: 100,
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
skippedTiers: ['generative', 'agentic', 'human'],
|
|
1104
|
+
context: {
|
|
1105
|
+
correlationId: 'test-123',
|
|
1106
|
+
steps: [],
|
|
1107
|
+
} as CascadeContext,
|
|
1108
|
+
metrics: {
|
|
1109
|
+
totalDuration: 100,
|
|
1110
|
+
tierDurations: { code: 100 },
|
|
1111
|
+
},
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
expect(result.value.approved).toBe(true)
|
|
1115
|
+
expect(result.tier).toBe('code')
|
|
1116
|
+
})
|
|
1117
|
+
})
|