ai-workflows 2.0.2 → 2.1.3
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 +4 -5
- package/.turbo/turbo-test.log +169 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +303 -184
- package/dist/barrier.d.ts +153 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +339 -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 +4 -1
- package/dist/context.js.map +1 -1
- 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/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 +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +52 -18
- package/dist/on.js.map +1 -1
- package/dist/send.d.ts +0 -5
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +1 -14
- package/dist/send.js.map +1 -1
- package/dist/timer-registry.d.ts +52 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +120 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +171 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +22 -18
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -16
- package/src/barrier.ts +466 -0
- package/src/cascade-context.ts +488 -0
- package/src/cascade-executor.ts +587 -0
- package/src/context.js +83 -0
- package/src/context.ts +12 -7
- package/src/dependency-graph.ts +518 -0
- package/src/every.js +267 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +414 -0
- package/src/index.js +71 -0
- package/src/index.ts +78 -0
- package/src/on.js +79 -0
- package/src/on.ts +81 -25
- package/src/send.js +111 -0
- package/src/send.ts +1 -16
- package/src/timer-registry.ts +145 -0
- package/src/types.js +4 -0
- package/src/types.ts +218 -11
- package/src/workflow.js +455 -0
- package/src/workflow.ts +32 -23
- package/test/barrier-join.test.ts +434 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +859 -0
- package/test/context.test.js +116 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/every.test.js +282 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/on.test.js +80 -0
- package/test/schedule-timer-cleanup.test.ts +344 -0
- package/test/send-race-conditions.test.ts +410 -0
- package/test/send.test.js +89 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/types-event-handler.test.ts +225 -0
- package/test/types-proxy-autocomplete.test.ts +345 -0
- package/test/workflow.test.js +224 -0
- package/vitest.config.js +7 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createCascadeContext,
|
|
4
|
+
recordStep,
|
|
5
|
+
withCascadeContext,
|
|
6
|
+
type CascadeContext,
|
|
7
|
+
type CascadeStep,
|
|
8
|
+
} from '../src/cascade-context.js'
|
|
9
|
+
|
|
10
|
+
describe('cascade-context', () => {
|
|
11
|
+
describe('correlation ID generation and propagation', () => {
|
|
12
|
+
it('should generate unique correlation ID on creation', () => {
|
|
13
|
+
const ctx1 = createCascadeContext()
|
|
14
|
+
const ctx2 = createCascadeContext()
|
|
15
|
+
|
|
16
|
+
expect(ctx1.correlationId).toBeDefined()
|
|
17
|
+
expect(ctx2.correlationId).toBeDefined()
|
|
18
|
+
expect(ctx1.correlationId).not.toBe(ctx2.correlationId)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should accept a custom correlation ID', () => {
|
|
22
|
+
const customId = 'custom-correlation-123'
|
|
23
|
+
const ctx = createCascadeContext({ correlationId: customId })
|
|
24
|
+
|
|
25
|
+
expect(ctx.correlationId).toBe(customId)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should generate UUID v4 format correlation IDs', () => {
|
|
29
|
+
const ctx = createCascadeContext()
|
|
30
|
+
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
31
|
+
|
|
32
|
+
expect(ctx.correlationId).toMatch(uuidV4Regex)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should propagate correlation ID to child contexts', () => {
|
|
36
|
+
const parent = createCascadeContext()
|
|
37
|
+
const child = createCascadeContext({ parent })
|
|
38
|
+
|
|
39
|
+
expect(child.correlationId).toBe(parent.correlationId)
|
|
40
|
+
expect(child.parentId).toBe(parent.spanId)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should have unique span IDs for each context', () => {
|
|
44
|
+
const parent = createCascadeContext()
|
|
45
|
+
const child1 = createCascadeContext({ parent })
|
|
46
|
+
const child2 = createCascadeContext({ parent })
|
|
47
|
+
|
|
48
|
+
expect(parent.spanId).toBeDefined()
|
|
49
|
+
expect(child1.spanId).toBeDefined()
|
|
50
|
+
expect(child2.spanId).toBeDefined()
|
|
51
|
+
expect(child1.spanId).not.toBe(parent.spanId)
|
|
52
|
+
expect(child1.spanId).not.toBe(child2.spanId)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('step timing metadata', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.useFakeTimers()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.useRealTimers()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should record step start time', () => {
|
|
66
|
+
const ctx = createCascadeContext()
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
vi.setSystemTime(now)
|
|
69
|
+
|
|
70
|
+
const step = recordStep(ctx, 'process-data')
|
|
71
|
+
|
|
72
|
+
expect(step.startedAt).toBe(now)
|
|
73
|
+
expect(step.name).toBe('process-data')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should record step completion time and duration', () => {
|
|
77
|
+
const ctx = createCascadeContext()
|
|
78
|
+
const startTime = Date.now()
|
|
79
|
+
vi.setSystemTime(startTime)
|
|
80
|
+
|
|
81
|
+
const step = recordStep(ctx, 'process-data')
|
|
82
|
+
|
|
83
|
+
vi.advanceTimersByTime(150)
|
|
84
|
+
step.complete()
|
|
85
|
+
|
|
86
|
+
expect(step.completedAt).toBe(startTime + 150)
|
|
87
|
+
expect(step.duration).toBe(150)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should record step failure with error', () => {
|
|
91
|
+
const ctx = createCascadeContext()
|
|
92
|
+
const step = recordStep(ctx, 'failing-step')
|
|
93
|
+
const error = new Error('Step failed')
|
|
94
|
+
|
|
95
|
+
step.fail(error)
|
|
96
|
+
|
|
97
|
+
expect(step.status).toBe('failed')
|
|
98
|
+
expect(step.error).toBe(error)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should track step status transitions', () => {
|
|
102
|
+
const ctx = createCascadeContext()
|
|
103
|
+
const step = recordStep(ctx, 'status-step')
|
|
104
|
+
|
|
105
|
+
expect(step.status).toBe('running')
|
|
106
|
+
|
|
107
|
+
step.complete()
|
|
108
|
+
expect(step.status).toBe('completed')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should support step metadata', () => {
|
|
112
|
+
const ctx = createCascadeContext()
|
|
113
|
+
const step = recordStep(ctx, 'metadata-step', {
|
|
114
|
+
tier: 'code',
|
|
115
|
+
input: { query: 'test' },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(step.metadata).toEqual({
|
|
119
|
+
tier: 'code',
|
|
120
|
+
input: { query: 'test' },
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should allow adding metadata after creation', () => {
|
|
125
|
+
const ctx = createCascadeContext()
|
|
126
|
+
const step = recordStep(ctx, 'metadata-step')
|
|
127
|
+
|
|
128
|
+
step.addMetadata({ result: 'success', tokens: 100 })
|
|
129
|
+
|
|
130
|
+
expect(step.metadata).toEqual({
|
|
131
|
+
result: 'success',
|
|
132
|
+
tokens: 100,
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('cascade path recording', () => {
|
|
138
|
+
it('should record execution path', () => {
|
|
139
|
+
const ctx = createCascadeContext()
|
|
140
|
+
|
|
141
|
+
recordStep(ctx, 'step-1').complete()
|
|
142
|
+
recordStep(ctx, 'step-2').complete()
|
|
143
|
+
recordStep(ctx, 'step-3').complete()
|
|
144
|
+
|
|
145
|
+
expect(ctx.path).toEqual(['step-1', 'step-2', 'step-3'])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should include all steps in context', () => {
|
|
149
|
+
const ctx = createCascadeContext()
|
|
150
|
+
|
|
151
|
+
const step1 = recordStep(ctx, 'code-tier')
|
|
152
|
+
step1.complete()
|
|
153
|
+
const step2 = recordStep(ctx, 'generative-tier')
|
|
154
|
+
step2.complete()
|
|
155
|
+
|
|
156
|
+
expect(ctx.steps).toHaveLength(2)
|
|
157
|
+
expect(ctx.steps[0]?.name).toBe('code-tier')
|
|
158
|
+
expect(ctx.steps[1]?.name).toBe('generative-tier')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should track nested paths in child contexts', () => {
|
|
162
|
+
const parent = createCascadeContext()
|
|
163
|
+
recordStep(parent, 'parent-step-1').complete()
|
|
164
|
+
|
|
165
|
+
const child = createCascadeContext({ parent })
|
|
166
|
+
recordStep(child, 'child-step-1').complete()
|
|
167
|
+
recordStep(child, 'child-step-2').complete()
|
|
168
|
+
|
|
169
|
+
expect(child.fullPath).toEqual([
|
|
170
|
+
'parent-step-1',
|
|
171
|
+
'child-step-1',
|
|
172
|
+
'child-step-2',
|
|
173
|
+
])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should provide depth information', () => {
|
|
177
|
+
const root = createCascadeContext()
|
|
178
|
+
const child = createCascadeContext({ parent: root })
|
|
179
|
+
const grandchild = createCascadeContext({ parent: child })
|
|
180
|
+
|
|
181
|
+
expect(root.depth).toBe(0)
|
|
182
|
+
expect(child.depth).toBe(1)
|
|
183
|
+
expect(grandchild.depth).toBe(2)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('context inheritance in nested operations', () => {
|
|
188
|
+
it('should inherit correlation ID in nested withCascadeContext', async () => {
|
|
189
|
+
let innerCorrelationId: string | undefined
|
|
190
|
+
let outerCorrelationId: string | undefined
|
|
191
|
+
|
|
192
|
+
await withCascadeContext(async (outer) => {
|
|
193
|
+
outerCorrelationId = outer.correlationId
|
|
194
|
+
|
|
195
|
+
await withCascadeContext(async (inner) => {
|
|
196
|
+
innerCorrelationId = inner.correlationId
|
|
197
|
+
}, { parent: outer })
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect(innerCorrelationId).toBe(outerCorrelationId)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should record steps from callback execution', async () => {
|
|
204
|
+
const ctx = await withCascadeContext(async (ctx) => {
|
|
205
|
+
recordStep(ctx, 'async-step-1').complete()
|
|
206
|
+
recordStep(ctx, 'async-step-2').complete()
|
|
207
|
+
return ctx
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(ctx.steps).toHaveLength(2)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should handle errors and record failed steps', async () => {
|
|
214
|
+
let caughtError: Error | undefined
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await withCascadeContext(async (ctx) => {
|
|
218
|
+
const step = recordStep(ctx, 'failing-step')
|
|
219
|
+
throw new Error('Intentional failure')
|
|
220
|
+
})
|
|
221
|
+
} catch (error) {
|
|
222
|
+
caughtError = error as Error
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
expect(caughtError?.message).toBe('Intentional failure')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should support options for context creation', async () => {
|
|
229
|
+
const ctx = await withCascadeContext(async (ctx) => ctx, {
|
|
230
|
+
correlationId: 'custom-id',
|
|
231
|
+
name: 'test-cascade',
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
expect(ctx.correlationId).toBe('custom-id')
|
|
235
|
+
expect(ctx.name).toBe('test-cascade')
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('context serialization for distributed systems', () => {
|
|
240
|
+
it('should serialize to JSON-compatible format', () => {
|
|
241
|
+
const ctx = createCascadeContext({ name: 'test-cascade' })
|
|
242
|
+
recordStep(ctx, 'step-1').complete()
|
|
243
|
+
|
|
244
|
+
const serialized = ctx.serialize()
|
|
245
|
+
|
|
246
|
+
expect(typeof serialized).toBe('object')
|
|
247
|
+
expect(serialized.correlationId).toBe(ctx.correlationId)
|
|
248
|
+
expect(serialized.spanId).toBe(ctx.spanId)
|
|
249
|
+
expect(serialized.name).toBe('test-cascade')
|
|
250
|
+
expect(serialized.steps).toHaveLength(1)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should serialize timing information', () => {
|
|
254
|
+
vi.useFakeTimers()
|
|
255
|
+
const startTime = Date.now()
|
|
256
|
+
vi.setSystemTime(startTime)
|
|
257
|
+
|
|
258
|
+
const ctx = createCascadeContext()
|
|
259
|
+
const step = recordStep(ctx, 'timed-step')
|
|
260
|
+
vi.advanceTimersByTime(100)
|
|
261
|
+
step.complete()
|
|
262
|
+
|
|
263
|
+
const serialized = ctx.serialize()
|
|
264
|
+
|
|
265
|
+
expect(serialized.steps[0]?.startedAt).toBe(startTime)
|
|
266
|
+
expect(serialized.steps[0]?.completedAt).toBe(startTime + 100)
|
|
267
|
+
expect(serialized.steps[0]?.duration).toBe(100)
|
|
268
|
+
|
|
269
|
+
vi.useRealTimers()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should deserialize from serialized format', () => {
|
|
273
|
+
const original = createCascadeContext({ name: 'original' })
|
|
274
|
+
recordStep(original, 'step-1').complete()
|
|
275
|
+
|
|
276
|
+
const serialized = original.serialize()
|
|
277
|
+
const restored = createCascadeContext({ fromSerialized: serialized })
|
|
278
|
+
|
|
279
|
+
expect(restored.correlationId).toBe(original.correlationId)
|
|
280
|
+
expect(restored.spanId).toBe(original.spanId)
|
|
281
|
+
expect(restored.name).toBe('original')
|
|
282
|
+
expect(restored.steps).toHaveLength(1)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should include parent reference in serialization', () => {
|
|
286
|
+
const parent = createCascadeContext()
|
|
287
|
+
const child = createCascadeContext({ parent })
|
|
288
|
+
|
|
289
|
+
const serialized = child.serialize()
|
|
290
|
+
|
|
291
|
+
expect(serialized.parentId).toBe(parent.spanId)
|
|
292
|
+
expect(serialized.correlationId).toBe(parent.correlationId)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('should support W3C trace context format for distributed tracing', () => {
|
|
296
|
+
const ctx = createCascadeContext()
|
|
297
|
+
|
|
298
|
+
const traceContext = ctx.toTraceContext()
|
|
299
|
+
|
|
300
|
+
// W3C trace context format: version-traceid-parentid-flags
|
|
301
|
+
expect(traceContext.traceparent).toMatch(
|
|
302
|
+
/^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should create context from W3C trace context', () => {
|
|
307
|
+
const original = createCascadeContext()
|
|
308
|
+
const traceContext = original.toTraceContext()
|
|
309
|
+
|
|
310
|
+
const restored = createCascadeContext({ fromTraceContext: traceContext })
|
|
311
|
+
|
|
312
|
+
// Should have same trace ID but different span ID
|
|
313
|
+
expect(restored.correlationId).toBe(original.correlationId)
|
|
314
|
+
expect(restored.parentId).toBe(original.spanId)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('5W+H event generation helpers', () => {
|
|
319
|
+
it('should generate 5W+H event from step', () => {
|
|
320
|
+
const ctx = createCascadeContext({ name: 'cascade-test' })
|
|
321
|
+
const step = recordStep(ctx, 'process-request', {
|
|
322
|
+
tier: 'code',
|
|
323
|
+
actor: 'system',
|
|
324
|
+
object: 'request',
|
|
325
|
+
action: 'process',
|
|
326
|
+
reason: 'User requested data processing',
|
|
327
|
+
})
|
|
328
|
+
step.complete()
|
|
329
|
+
|
|
330
|
+
const event = step.to5WHEvent()
|
|
331
|
+
|
|
332
|
+
expect(event.who).toBe('system')
|
|
333
|
+
expect(event.what).toBe('process')
|
|
334
|
+
expect(event.when).toBeDefined()
|
|
335
|
+
expect(event.where).toBe('cascade-test')
|
|
336
|
+
expect(event.why).toBe('User requested data processing')
|
|
337
|
+
expect(event.how).toBeDefined()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should include timing in 5W+H event', () => {
|
|
341
|
+
vi.useFakeTimers()
|
|
342
|
+
const now = Date.now()
|
|
343
|
+
vi.setSystemTime(now)
|
|
344
|
+
|
|
345
|
+
const ctx = createCascadeContext()
|
|
346
|
+
const step = recordStep(ctx, 'timed-step')
|
|
347
|
+
vi.advanceTimersByTime(50)
|
|
348
|
+
step.complete()
|
|
349
|
+
|
|
350
|
+
const event = step.to5WHEvent()
|
|
351
|
+
|
|
352
|
+
expect(event.when).toBe(now)
|
|
353
|
+
expect(event.how).toMatchObject({
|
|
354
|
+
duration: 50,
|
|
355
|
+
status: 'completed',
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
vi.useRealTimers()
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('context visualization', () => {
|
|
363
|
+
it('should format context as readable string', () => {
|
|
364
|
+
const ctx = createCascadeContext({ name: 'test-workflow' })
|
|
365
|
+
recordStep(ctx, 'step-1').complete()
|
|
366
|
+
recordStep(ctx, 'step-2').complete()
|
|
367
|
+
|
|
368
|
+
const formatted = ctx.format()
|
|
369
|
+
|
|
370
|
+
expect(formatted).toContain('test-workflow')
|
|
371
|
+
expect(formatted).toContain('step-1')
|
|
372
|
+
expect(formatted).toContain('step-2')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('should format as tree for nested contexts', () => {
|
|
376
|
+
const parent = createCascadeContext({ name: 'parent' })
|
|
377
|
+
recordStep(parent, 'parent-step').complete()
|
|
378
|
+
|
|
379
|
+
const child = createCascadeContext({ parent, name: 'child' })
|
|
380
|
+
recordStep(child, 'child-step').complete()
|
|
381
|
+
|
|
382
|
+
const tree = child.formatTree()
|
|
383
|
+
|
|
384
|
+
expect(tree).toContain('parent')
|
|
385
|
+
expect(tree).toContain('child')
|
|
386
|
+
expect(tree).toContain('parent-step')
|
|
387
|
+
expect(tree).toContain('child-step')
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
})
|