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,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ai-workflows package
|
|
3
|
+
*
|
|
4
|
+
* Tests the workflow, event handling, scheduling, and coordination primitives.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
8
|
+
import {
|
|
9
|
+
Workflow,
|
|
10
|
+
createTestContext,
|
|
11
|
+
parseEvent,
|
|
12
|
+
on,
|
|
13
|
+
every,
|
|
14
|
+
send,
|
|
15
|
+
getEventBus,
|
|
16
|
+
registerEventHandler,
|
|
17
|
+
getEventHandlers,
|
|
18
|
+
clearEventHandlers,
|
|
19
|
+
registerScheduleHandler,
|
|
20
|
+
getScheduleHandlers,
|
|
21
|
+
clearScheduleHandlers,
|
|
22
|
+
createWorkflowContext,
|
|
23
|
+
createIsolatedContext,
|
|
24
|
+
createCascadeContext,
|
|
25
|
+
recordStep,
|
|
26
|
+
DependencyGraph,
|
|
27
|
+
CircularDependencyError,
|
|
28
|
+
MissingDependencyError,
|
|
29
|
+
topologicalSort,
|
|
30
|
+
getExecutionLevels,
|
|
31
|
+
Barrier,
|
|
32
|
+
createBarrier,
|
|
33
|
+
waitForAll,
|
|
34
|
+
waitForAny,
|
|
35
|
+
withConcurrencyLimit,
|
|
36
|
+
CascadeExecutor,
|
|
37
|
+
workflow,
|
|
38
|
+
WorkflowBuilder,
|
|
39
|
+
WorkflowStateAdapter,
|
|
40
|
+
type WorkflowInstance,
|
|
41
|
+
type CascadeContext,
|
|
42
|
+
} from '../src/index.js'
|
|
43
|
+
|
|
44
|
+
// Skip tests if no gateway configured
|
|
45
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
46
|
+
|
|
47
|
+
describe('ai-workflows exports', () => {
|
|
48
|
+
it('should export Workflow', () => {
|
|
49
|
+
expect(Workflow).toBeDefined()
|
|
50
|
+
expect(typeof Workflow).toBe('function')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should export event handling functions', () => {
|
|
54
|
+
expect(on).toBeDefined()
|
|
55
|
+
expect(registerEventHandler).toBeDefined()
|
|
56
|
+
expect(getEventHandlers).toBeDefined()
|
|
57
|
+
expect(clearEventHandlers).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should export scheduling functions', () => {
|
|
61
|
+
expect(every).toBeDefined()
|
|
62
|
+
expect(registerScheduleHandler).toBeDefined()
|
|
63
|
+
expect(getScheduleHandlers).toBeDefined()
|
|
64
|
+
expect(clearScheduleHandlers).toBeDefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should export event emission', () => {
|
|
68
|
+
expect(send).toBeDefined()
|
|
69
|
+
expect(getEventBus).toBeDefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should export context utilities', () => {
|
|
73
|
+
expect(createWorkflowContext).toBeDefined()
|
|
74
|
+
expect(createIsolatedContext).toBeDefined()
|
|
75
|
+
expect(createTestContext).toBeDefined()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should export cascade context', () => {
|
|
79
|
+
expect(createCascadeContext).toBeDefined()
|
|
80
|
+
expect(recordStep).toBeDefined()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should export dependency graph', () => {
|
|
84
|
+
expect(DependencyGraph).toBeDefined()
|
|
85
|
+
expect(CircularDependencyError).toBeDefined()
|
|
86
|
+
expect(MissingDependencyError).toBeDefined()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should export topological sort', () => {
|
|
90
|
+
expect(topologicalSort).toBeDefined()
|
|
91
|
+
expect(getExecutionLevels).toBeDefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should export barrier utilities', () => {
|
|
95
|
+
expect(Barrier).toBeDefined()
|
|
96
|
+
expect(createBarrier).toBeDefined()
|
|
97
|
+
expect(waitForAll).toBeDefined()
|
|
98
|
+
expect(waitForAny).toBeDefined()
|
|
99
|
+
expect(withConcurrencyLimit).toBeDefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should export cascade executor', () => {
|
|
103
|
+
expect(CascadeExecutor).toBeDefined()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should export workflow builder', () => {
|
|
107
|
+
expect(workflow).toBeDefined()
|
|
108
|
+
expect(WorkflowBuilder).toBeDefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should export state adapter', () => {
|
|
112
|
+
expect(WorkflowStateAdapter).toBeDefined()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('Workflow creation', () => {
|
|
117
|
+
let workflowInstance: WorkflowInstance
|
|
118
|
+
|
|
119
|
+
afterEach(async () => {
|
|
120
|
+
if (workflowInstance) {
|
|
121
|
+
await workflowInstance.destroy()
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should create a basic workflow', () => {
|
|
126
|
+
workflowInstance = Workflow(($) => {
|
|
127
|
+
// Empty workflow
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(workflowInstance).toBeDefined()
|
|
131
|
+
expect(workflowInstance.definition).toBeDefined()
|
|
132
|
+
expect(workflowInstance.state).toBeDefined()
|
|
133
|
+
expect(workflowInstance.$).toBeDefined()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should register event handlers', () => {
|
|
137
|
+
workflowInstance = Workflow(($) => {
|
|
138
|
+
$.on.Customer.created(async (data, $) => {
|
|
139
|
+
// handler
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
$.on.Order.completed(async (data, $) => {
|
|
143
|
+
// handler
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(workflowInstance.definition.events.length).toBe(2)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('Event parsing', () => {
|
|
152
|
+
it('should parse valid event strings', () => {
|
|
153
|
+
const result = parseEvent('Customer.created')
|
|
154
|
+
expect(result).toEqual({ noun: 'Customer', event: 'created' })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should parse different event names', () => {
|
|
158
|
+
expect(parseEvent('Order.completed')).toEqual({ noun: 'Order', event: 'completed' })
|
|
159
|
+
expect(parseEvent('User.updated')).toEqual({ noun: 'User', event: 'updated' })
|
|
160
|
+
expect(parseEvent('Payment.failed')).toEqual({ noun: 'Payment', event: 'failed' })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should return null for invalid event strings', () => {
|
|
164
|
+
expect(parseEvent('invalid')).toBeNull()
|
|
165
|
+
expect(parseEvent('')).toBeNull()
|
|
166
|
+
expect(parseEvent('too.many.parts')).toBeNull()
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('DependencyGraph', () => {
|
|
171
|
+
it('should add nodes', () => {
|
|
172
|
+
const graph = new DependencyGraph()
|
|
173
|
+
|
|
174
|
+
graph.addNode('a')
|
|
175
|
+
graph.addNode('b')
|
|
176
|
+
graph.addNode('c')
|
|
177
|
+
|
|
178
|
+
expect(graph.hasNode('a')).toBe(true)
|
|
179
|
+
expect(graph.hasNode('b')).toBe(true)
|
|
180
|
+
expect(graph.hasNode('c')).toBe(true)
|
|
181
|
+
expect(graph.hasNode('d')).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should add edges (dependencies)', () => {
|
|
185
|
+
const graph = new DependencyGraph()
|
|
186
|
+
|
|
187
|
+
graph.addNode('a')
|
|
188
|
+
graph.addNode('b')
|
|
189
|
+
graph.addEdge('a', 'b') // b depends on a
|
|
190
|
+
|
|
191
|
+
expect(graph.getDependencies('b')).toContain('a')
|
|
192
|
+
expect(graph.getDependents('a')).toContain('b')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should detect circular dependencies', () => {
|
|
196
|
+
const graph = new DependencyGraph()
|
|
197
|
+
|
|
198
|
+
graph.addNode('a')
|
|
199
|
+
graph.addNode('b')
|
|
200
|
+
graph.addNode('c')
|
|
201
|
+
|
|
202
|
+
graph.addEdge('a', 'b')
|
|
203
|
+
graph.addEdge('b', 'c')
|
|
204
|
+
|
|
205
|
+
expect(() => graph.addEdge('c', 'a')).toThrow(CircularDependencyError)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should get parallel execution groups', () => {
|
|
209
|
+
const graph = new DependencyGraph()
|
|
210
|
+
|
|
211
|
+
graph.addNode('a')
|
|
212
|
+
graph.addNode('b')
|
|
213
|
+
graph.addNode('c')
|
|
214
|
+
graph.addNode('d')
|
|
215
|
+
|
|
216
|
+
graph.addEdge('a', 'c')
|
|
217
|
+
graph.addEdge('b', 'c')
|
|
218
|
+
graph.addEdge('c', 'd')
|
|
219
|
+
|
|
220
|
+
const groups = graph.getParallelGroups()
|
|
221
|
+
|
|
222
|
+
// First group (level 0): a, b (no dependencies)
|
|
223
|
+
expect(groups.length).toBeGreaterThanOrEqual(2)
|
|
224
|
+
expect(groups[0].nodes).toContain('a')
|
|
225
|
+
expect(groups[0].nodes).toContain('b')
|
|
226
|
+
expect(groups[0].level).toBe(0)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('Topological Sort', () => {
|
|
231
|
+
it('should sort nodes in dependency order', () => {
|
|
232
|
+
const nodes = [
|
|
233
|
+
{ id: 'a', dependencies: [] },
|
|
234
|
+
{ id: 'b', dependencies: [] },
|
|
235
|
+
{ id: 'c', dependencies: ['a', 'b'] },
|
|
236
|
+
{ id: 'd', dependencies: ['c'] },
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
const result = topologicalSort(nodes)
|
|
240
|
+
const sorted = result.order
|
|
241
|
+
|
|
242
|
+
expect(sorted.indexOf('a')).toBeLessThan(sorted.indexOf('c'))
|
|
243
|
+
expect(sorted.indexOf('b')).toBeLessThan(sorted.indexOf('c'))
|
|
244
|
+
expect(sorted.indexOf('c')).toBeLessThan(sorted.indexOf('d'))
|
|
245
|
+
expect(result.hasCycle).toBe(false)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should get execution levels', () => {
|
|
249
|
+
const nodes = [
|
|
250
|
+
{ id: 'a', dependencies: [] },
|
|
251
|
+
{ id: 'b', dependencies: [] },
|
|
252
|
+
{ id: 'c', dependencies: ['a', 'b'] },
|
|
253
|
+
{ id: 'd', dependencies: ['c'] },
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
const levels = getExecutionLevels(nodes)
|
|
257
|
+
|
|
258
|
+
// Level 0: a, b
|
|
259
|
+
// Level 1: c
|
|
260
|
+
// Level 2: d
|
|
261
|
+
expect(levels.length).toBe(3)
|
|
262
|
+
expect(levels[0].nodes).toContain('a')
|
|
263
|
+
expect(levels[0].nodes).toContain('b')
|
|
264
|
+
expect(levels[1].nodes).toContain('c')
|
|
265
|
+
expect(levels[2].nodes).toContain('d')
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe('Barrier', () => {
|
|
270
|
+
it('should wait for all promises', async () => {
|
|
271
|
+
const results = await waitForAll([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
|
|
272
|
+
|
|
273
|
+
expect(results).toEqual([1, 2, 3])
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should limit concurrency', async () => {
|
|
277
|
+
let concurrent = 0
|
|
278
|
+
let maxConcurrent = 0
|
|
279
|
+
|
|
280
|
+
const tasks = Array.from({ length: 10 }, (_, i) => async () => {
|
|
281
|
+
concurrent++
|
|
282
|
+
maxConcurrent = Math.max(maxConcurrent, concurrent)
|
|
283
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
284
|
+
concurrent--
|
|
285
|
+
return i
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const results = await withConcurrencyLimit(tasks, 3)
|
|
289
|
+
|
|
290
|
+
expect(results.length).toBe(10)
|
|
291
|
+
expect(maxConcurrent).toBeLessThanOrEqual(3)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('Cascade Context', () => {
|
|
296
|
+
it('should create cascade context with correlation ID', () => {
|
|
297
|
+
const ctx = createCascadeContext({
|
|
298
|
+
correlationId: 'corr-123',
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
expect(ctx).toBeDefined()
|
|
302
|
+
expect(ctx.correlationId).toBe('corr-123')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should record steps in cascade context', () => {
|
|
306
|
+
const ctx = createCascadeContext({
|
|
307
|
+
correlationId: 'corr-456',
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// recordStep takes (ctx, name, metadata?) and returns a step
|
|
311
|
+
const step1 = recordStep(ctx, 'step1')
|
|
312
|
+
step1.complete()
|
|
313
|
+
|
|
314
|
+
const step2 = recordStep(ctx, 'step2')
|
|
315
|
+
step2.complete()
|
|
316
|
+
|
|
317
|
+
expect(ctx.steps.length).toBe(2)
|
|
318
|
+
expect(ctx.steps[0].name).toBe('step1')
|
|
319
|
+
expect(ctx.steps[1].name).toBe('step2')
|
|
320
|
+
expect(ctx.steps[0].status).toBe('completed')
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
describe('CascadeExecutor', () => {
|
|
325
|
+
it('should execute code tier first', async () => {
|
|
326
|
+
const executor = new CascadeExecutor({
|
|
327
|
+
tiers: {
|
|
328
|
+
code: {
|
|
329
|
+
name: 'code-handler',
|
|
330
|
+
execute: async () => 'code result',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const result = await executor.execute('test input')
|
|
336
|
+
|
|
337
|
+
// Result doesn't have 'success' - it returns value on success or throws
|
|
338
|
+
expect(result.tier).toBe('code')
|
|
339
|
+
expect(result.value).toBe('code result')
|
|
340
|
+
expect(result.history).toBeDefined()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should cascade to next tier on failure', async () => {
|
|
344
|
+
const executor = new CascadeExecutor({
|
|
345
|
+
tiers: {
|
|
346
|
+
code: {
|
|
347
|
+
name: 'code-handler',
|
|
348
|
+
execute: async () => {
|
|
349
|
+
throw new Error('Code failed')
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
generative: {
|
|
353
|
+
name: 'generative-handler',
|
|
354
|
+
execute: async () => 'generative result',
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
timeouts: {
|
|
358
|
+
code: 100,
|
|
359
|
+
generative: 5000,
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const result = await executor.execute('test input')
|
|
364
|
+
|
|
365
|
+
expect(result.tier).toBe('generative')
|
|
366
|
+
expect(result.value).toBe('generative result')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should track execution history', async () => {
|
|
370
|
+
const executor = new CascadeExecutor({
|
|
371
|
+
tiers: {
|
|
372
|
+
code: {
|
|
373
|
+
name: 'code-handler',
|
|
374
|
+
execute: async () => {
|
|
375
|
+
throw new Error('Code failed')
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
generative: {
|
|
379
|
+
name: 'generative-handler',
|
|
380
|
+
execute: async () => 'ok',
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const result = await executor.execute('test')
|
|
386
|
+
|
|
387
|
+
expect(result.history.length).toBeGreaterThan(0)
|
|
388
|
+
expect(result.tier).toBe('generative')
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe('WorkflowBuilder DSL', () => {
|
|
393
|
+
it('should create workflow with builder', () => {
|
|
394
|
+
const wf = workflow('test-workflow')
|
|
395
|
+
.step('step1', async () => ({ value: 1 }))
|
|
396
|
+
.step('step2', async () => ({ value: 2 }))
|
|
397
|
+
.build()
|
|
398
|
+
|
|
399
|
+
expect(wf).toBeDefined()
|
|
400
|
+
expect(wf.name).toBe('test-workflow')
|
|
401
|
+
expect(wf.steps.length).toBe(2)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('should support conditional steps', () => {
|
|
405
|
+
const wf = workflow('conditional-workflow')
|
|
406
|
+
.step('check', async () => ({ shouldContinue: true }))
|
|
407
|
+
.when((ctx) => ctx.result?.check?.shouldContinue === true)
|
|
408
|
+
.then(workflow('continue-flow').step('continue', async () => ({ continued: true })))
|
|
409
|
+
.build()
|
|
410
|
+
|
|
411
|
+
expect(wf).toBeDefined()
|
|
412
|
+
expect(wf.steps.length).toBe(2)
|
|
413
|
+
expect(wf.steps[1].type).toBe('conditional')
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('should support timeout configuration', () => {
|
|
417
|
+
const wf = workflow('timeout-workflow')
|
|
418
|
+
.step('slow', async () => ({ done: true }))
|
|
419
|
+
.timeout(5000)
|
|
420
|
+
.build()
|
|
421
|
+
|
|
422
|
+
expect(wf).toBeDefined()
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('WorkflowStateAdapter exports', () => {
|
|
427
|
+
it('should export WorkflowStateAdapter class', () => {
|
|
428
|
+
// WorkflowStateAdapter requires a database connection
|
|
429
|
+
expect(WorkflowStateAdapter).toBeDefined()
|
|
430
|
+
expect(typeof WorkflowStateAdapter).toBe('function')
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('Error handling', () => {
|
|
435
|
+
it('should handle missing dependencies gracefully', () => {
|
|
436
|
+
const graph = new DependencyGraph()
|
|
437
|
+
|
|
438
|
+
graph.addNode('a')
|
|
439
|
+
|
|
440
|
+
expect(() => graph.addEdge('a', 'nonexistent')).toThrow(MissingDependencyError)
|
|
441
|
+
})
|
|
442
|
+
})
|