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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD RED Phase Tests: OnProxy/EveryProxy autocomplete
|
|
3
|
+
* Issue: primitives.org.ai-5qe
|
|
4
|
+
*
|
|
5
|
+
* These tests verify that TypeScript sees known properties on $.on and $.every
|
|
6
|
+
* instead of treating everything as unknown from index signatures.
|
|
7
|
+
*
|
|
8
|
+
* Current implementation uses pure index signatures which causes:
|
|
9
|
+
* - $.on to be typed as { [noun: string]: { [event: string]: ... } }
|
|
10
|
+
* - $.every has union type with index signature
|
|
11
|
+
* - IDE autocomplete doesn't show known patterns (hour, day, Monday, etc.)
|
|
12
|
+
*
|
|
13
|
+
* The issue is that index signatures make TypeScript treat all properties
|
|
14
|
+
* as having the same type, which hides known properties from autocomplete.
|
|
15
|
+
*
|
|
16
|
+
* These tests should FAIL until proper typing with explicit known properties is implemented.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, expectTypeOf } from 'vitest'
|
|
19
|
+
import type { WorkflowContext, OnProxy, EveryProxy, ScheduleHandler, EventHandler } from '../src/types.js'
|
|
20
|
+
import { Workflow, createTestContext } from '../src/workflow.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Helper type to check if a type has specific known keys
|
|
24
|
+
* This fails if the type only has an index signature
|
|
25
|
+
*/
|
|
26
|
+
type HasKnownKey<T, K extends string> = K extends keyof T ? true : false
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Expected OnProxy type with explicit known nouns
|
|
30
|
+
* This is what we WANT the type to look like
|
|
31
|
+
*/
|
|
32
|
+
type ExpectedOnProxy = {
|
|
33
|
+
// Known nouns (for autocomplete)
|
|
34
|
+
Customer: { [event: string]: (handler: EventHandler) => void }
|
|
35
|
+
Order: { [event: string]: (handler: EventHandler) => void }
|
|
36
|
+
Payment: { [event: string]: (handler: EventHandler) => void }
|
|
37
|
+
User: { [event: string]: (handler: EventHandler) => void }
|
|
38
|
+
// Index signature for dynamic nouns
|
|
39
|
+
[noun: string]: { [event: string]: (handler: EventHandler) => void }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Expected EveryProxy type with explicit known patterns
|
|
44
|
+
*/
|
|
45
|
+
type ExpectedEveryProxy = {
|
|
46
|
+
// Callable
|
|
47
|
+
(description: string, handler: ScheduleHandler): void
|
|
48
|
+
// Known time units (for autocomplete)
|
|
49
|
+
second: (handler: ScheduleHandler) => void
|
|
50
|
+
minute: (handler: ScheduleHandler) => void
|
|
51
|
+
hour: (handler: ScheduleHandler) => void
|
|
52
|
+
day: (handler: ScheduleHandler) => void
|
|
53
|
+
week: (handler: ScheduleHandler) => void
|
|
54
|
+
// Known days
|
|
55
|
+
Monday: ((handler: ScheduleHandler) => void) & { at9am: (handler: ScheduleHandler) => void }
|
|
56
|
+
Tuesday: ((handler: ScheduleHandler) => void) & { at9am: (handler: ScheduleHandler) => void }
|
|
57
|
+
// etc...
|
|
58
|
+
// Plural forms
|
|
59
|
+
seconds: (value: number) => (handler: ScheduleHandler) => void
|
|
60
|
+
minutes: (value: number) => (handler: ScheduleHandler) => void
|
|
61
|
+
hours: (value: number) => (handler: ScheduleHandler) => void
|
|
62
|
+
// Index signature for unknown patterns
|
|
63
|
+
[key: string]: unknown
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('OnProxy/EveryProxy autocomplete - TDD RED', () => {
|
|
67
|
+
describe('OnProxy known property types - documents current limitations', () => {
|
|
68
|
+
/**
|
|
69
|
+
* Current OnProxy type:
|
|
70
|
+
* { [noun: string]: { [event: string]: (handler: EventHandler) => void } }
|
|
71
|
+
*
|
|
72
|
+
* This makes TypeScript unable to distinguish known nouns from unknown ones.
|
|
73
|
+
* IDE autocomplete will not suggest "Customer", "Order", etc.
|
|
74
|
+
*
|
|
75
|
+
* Desired OnProxy type should have explicit known properties for autocomplete.
|
|
76
|
+
*
|
|
77
|
+
* TEST STRATEGY:
|
|
78
|
+
* - Document that current types use pure index signatures
|
|
79
|
+
* - When types are fixed to have explicit properties, @ts-expect-error will fail
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
it('documents: CURRENT OnProxy uses pure index signature (no explicit keys)', () => {
|
|
83
|
+
// With current types, OnProxy is { [noun: string]: ... }
|
|
84
|
+
// This means 'Customer' is NOT an explicit key, just matches index signature
|
|
85
|
+
//
|
|
86
|
+
// HasKnownKey returns true for index signatures because any string is valid
|
|
87
|
+
// So we can't easily test this at compile time
|
|
88
|
+
|
|
89
|
+
// Instead, document that accessing onProxy.Customer gives same type as any other key
|
|
90
|
+
const onProxy: OnProxy = {} as OnProxy
|
|
91
|
+
|
|
92
|
+
// These are all the same type because of index signature
|
|
93
|
+
type CustomerType = typeof onProxy.Customer
|
|
94
|
+
type RandomType = typeof onProxy.RandomNoun
|
|
95
|
+
|
|
96
|
+
// Both should be the same - this is CURRENT behavior we want to change
|
|
97
|
+
expectTypeOf<CustomerType>().toEqualTypeOf<RandomType>()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('documents: IDE autocomplete shows nothing for $.on. with current types', () => {
|
|
101
|
+
// This is a documentation test - can't test autocomplete at runtime
|
|
102
|
+
// But we document that pure index signatures don't provide suggestions
|
|
103
|
+
|
|
104
|
+
const workflow = Workflow($ => {})
|
|
105
|
+
|
|
106
|
+
// When typing "workflow.$.on." the IDE shows NO suggestions
|
|
107
|
+
// because OnProxy is { [noun: string]: ... } with no explicit keys
|
|
108
|
+
|
|
109
|
+
// With FIXED types, IDE would suggest: Customer, Order, Payment, User, etc.
|
|
110
|
+
|
|
111
|
+
// Current behavior: any string access works but no autocomplete
|
|
112
|
+
const _customer = workflow.$.on.Customer // Works but no suggestion
|
|
113
|
+
const _random = workflow.$.on.AnyRandomString // Also works, same type
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('PASSES: OnProxy has explicit known nouns', () => {
|
|
117
|
+
// With FIXED types: OnProxy has explicit Customer, Order, etc. keys
|
|
118
|
+
|
|
119
|
+
type OnProxyKeys = keyof OnProxy
|
|
120
|
+
// With explicit keys: keyof includes 'Customer' | 'Order' | string
|
|
121
|
+
|
|
122
|
+
// This now works - Customer is a valid OnProxyKeys value
|
|
123
|
+
const _customerKey: OnProxyKeys = 'Customer'
|
|
124
|
+
const _orderKey: OnProxyKeys = 'Order'
|
|
125
|
+
const _paymentKey: OnProxyKeys = 'Payment'
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('EveryProxy known property types - documents current limitations', () => {
|
|
130
|
+
/**
|
|
131
|
+
* Current EveryProxy type has a complex union with index signature:
|
|
132
|
+
* { (description, handler): void } & { [key: string]: ... }
|
|
133
|
+
*
|
|
134
|
+
* This makes it hard for TypeScript to determine specific property types.
|
|
135
|
+
* Known patterns like .hour, .Monday should be explicitly typed
|
|
136
|
+
* for better IDE autocomplete.
|
|
137
|
+
*
|
|
138
|
+
* TEST STRATEGY:
|
|
139
|
+
* - Document current union type behavior
|
|
140
|
+
* - Use @ts-expect-error for things that will work when fixed
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
it('documents: CURRENT EveryProxy uses union type with index signature', () => {
|
|
144
|
+
// Current EveryProxy type makes property access return a union type
|
|
145
|
+
// not a specific type like (handler: ScheduleHandler) => void
|
|
146
|
+
|
|
147
|
+
const workflow = Workflow($ => {})
|
|
148
|
+
const every = workflow.$.every
|
|
149
|
+
|
|
150
|
+
// With current types, $.every.hour is a union type, not specifically typed
|
|
151
|
+
type HourType = typeof every.hour
|
|
152
|
+
|
|
153
|
+
// Document that hour is a complex union, not just a function
|
|
154
|
+
// (This is the limitation we want to fix)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('documents: $.every.hour works but has union type', () => {
|
|
158
|
+
const workflow = Workflow($ => {})
|
|
159
|
+
|
|
160
|
+
// $.every.hour IS callable in practice (runtime works)
|
|
161
|
+
// But TypeScript types it as a union which is confusing
|
|
162
|
+
const hour = workflow.$.every.hour
|
|
163
|
+
|
|
164
|
+
// This passes because union includes function type
|
|
165
|
+
expectTypeOf(hour).toBeFunction()
|
|
166
|
+
|
|
167
|
+
// Runtime: hour(() => {}) works
|
|
168
|
+
// Type: hour is union type, not specifically (handler) => void
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('PASSES: $.every.Monday.at9am is directly typed', () => {
|
|
172
|
+
const workflow = Workflow($ => {})
|
|
173
|
+
|
|
174
|
+
const monday = workflow.$.every.Monday
|
|
175
|
+
|
|
176
|
+
// With FIXED types: monday.at9am is (handler: ScheduleHandler) => void
|
|
177
|
+
const at9am: (handler: ScheduleHandler) => void = monday.at9am
|
|
178
|
+
|
|
179
|
+
// Can call it directly
|
|
180
|
+
at9am(() => {})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('documents: $.every.minutes curried call works at runtime', () => {
|
|
184
|
+
const workflow = Workflow($ => {})
|
|
185
|
+
|
|
186
|
+
// $.every.minutes(30) works at runtime
|
|
187
|
+
// Types may be confusing due to union
|
|
188
|
+
const minutes = workflow.$.every.minutes
|
|
189
|
+
|
|
190
|
+
// First call with number
|
|
191
|
+
expectTypeOf(minutes).toBeFunction()
|
|
192
|
+
|
|
193
|
+
// Runtime works, types are union
|
|
194
|
+
const every30 = workflow.$.every.minutes(30)
|
|
195
|
+
expectTypeOf(every30).toBeFunction()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('PASSES: keyof EveryProxy includes known patterns', () => {
|
|
199
|
+
// With FIXED types: keyof EveryProxy includes 'hour' | 'Monday' | 'minutes' | string
|
|
200
|
+
|
|
201
|
+
type EveryProxyKeys = keyof EveryProxy
|
|
202
|
+
|
|
203
|
+
// These are all valid EveryProxy keys
|
|
204
|
+
const _hourKey: EveryProxyKeys = 'hour'
|
|
205
|
+
const _mondayKey: EveryProxyKeys = 'Monday'
|
|
206
|
+
const _minutesKey: EveryProxyKeys = 'minutes'
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('documents: IDE autocomplete limited with current union types', () => {
|
|
210
|
+
// When typing "workflow.$.every." the IDE may show:
|
|
211
|
+
// - Some suggestions from union type
|
|
212
|
+
// - But not clear, specific suggestions for known patterns
|
|
213
|
+
|
|
214
|
+
const workflow = Workflow($ => {})
|
|
215
|
+
|
|
216
|
+
// With FIXED types, IDE would clearly suggest:
|
|
217
|
+
// - hour, minute, second, day, week
|
|
218
|
+
// - Monday, Tuesday, Wednesday, etc.
|
|
219
|
+
// - minutes, hours, seconds, etc.
|
|
220
|
+
|
|
221
|
+
// Current behavior: union type makes suggestions confusing
|
|
222
|
+
type HourType = typeof workflow.$.every.hour
|
|
223
|
+
type MinutesType = typeof workflow.$.every.minutes
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('$.send type safety', () => {
|
|
228
|
+
it('should have $.send callable and typed', () => {
|
|
229
|
+
const workflow = Workflow($ => {})
|
|
230
|
+
|
|
231
|
+
// $.send should be a function
|
|
232
|
+
expectTypeOf(workflow.$.send).toBeFunction()
|
|
233
|
+
|
|
234
|
+
// Should not be unknown
|
|
235
|
+
expectTypeOf(workflow.$.send).not.toBeUnknown()
|
|
236
|
+
|
|
237
|
+
// Should return Promise<void>
|
|
238
|
+
expectTypeOf(workflow.$.send<{ id: string }>).returns.toEqualTypeOf<Promise<void>>()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should type-check send call with data', async () => {
|
|
242
|
+
const workflow = Workflow($ => {
|
|
243
|
+
$.on.Email.welcome((data, ctx) => {
|
|
244
|
+
ctx.log('Welcome email sent')
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// $.send should accept event name and typed data
|
|
249
|
+
await workflow.$.send('Email.welcome', { to: 'test@example.com' })
|
|
250
|
+
|
|
251
|
+
// The data parameter type should be inferred or explicitly typed
|
|
252
|
+
await workflow.$.send<{ to: string }>('Email.welcome', { to: 'test@example.com' })
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('$.state accessibility', () => {
|
|
257
|
+
it('should have $.state accessible and typed', () => {
|
|
258
|
+
const workflow = Workflow($ => {})
|
|
259
|
+
|
|
260
|
+
// $.state should be accessible
|
|
261
|
+
expectTypeOf(workflow.$.state).not.toBeUnknown()
|
|
262
|
+
|
|
263
|
+
// Should be a record type
|
|
264
|
+
expectTypeOf(workflow.$.state).toMatchTypeOf<Record<string, unknown>>()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should allow reading and writing state', () => {
|
|
268
|
+
const workflow = Workflow($ => {
|
|
269
|
+
// Writing to state
|
|
270
|
+
$.state.userId = '123'
|
|
271
|
+
$.state.counter = 0
|
|
272
|
+
|
|
273
|
+
// Reading from state
|
|
274
|
+
const userId = $.state.userId
|
|
275
|
+
const counter = $.state.counter
|
|
276
|
+
|
|
277
|
+
expect(userId).toBe('123')
|
|
278
|
+
expect(counter).toBe(0)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should have $.set and $.get methods typed', () => {
|
|
283
|
+
const workflow = Workflow($ => {})
|
|
284
|
+
|
|
285
|
+
// $.set should be callable
|
|
286
|
+
expectTypeOf(workflow.$.set).toBeFunction()
|
|
287
|
+
expectTypeOf(workflow.$.set<string>).toBeCallableWith('key', 'value')
|
|
288
|
+
|
|
289
|
+
// $.get should be callable and return typed value
|
|
290
|
+
expectTypeOf(workflow.$.get).toBeFunction()
|
|
291
|
+
expectTypeOf(workflow.$.get<string>).returns.toEqualTypeOf<string | undefined>()
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('Dynamic noun/event access still works', () => {
|
|
296
|
+
/**
|
|
297
|
+
* Even with explicit known properties, dynamic access should still work
|
|
298
|
+
* through the index signature fallback.
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
it('should allow $.on.DynamicNoun.dynamicEvent access', () => {
|
|
302
|
+
const workflow = Workflow($ => {
|
|
303
|
+
// Dynamic access should still work
|
|
304
|
+
$.on.SomeNewNoun.someEvent(() => {})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
expect(workflow.definition.events).toHaveLength(1)
|
|
308
|
+
expect(workflow.definition.events[0]?.noun).toBe('SomeNewNoun')
|
|
309
|
+
expect(workflow.definition.events[0]?.event).toBe('someEvent')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('should allow $.on[variable] access pattern', () => {
|
|
313
|
+
const workflow = Workflow($ => {})
|
|
314
|
+
|
|
315
|
+
const nounName = 'DynamicEntity' as string
|
|
316
|
+
const events = workflow.$.on[nounName]
|
|
317
|
+
|
|
318
|
+
// Should still be accessible and usable
|
|
319
|
+
expectTypeOf(events).not.toBeUnknown()
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('createTestContext type safety', () => {
|
|
324
|
+
it('should return properly typed context', () => {
|
|
325
|
+
const ctx = createTestContext()
|
|
326
|
+
|
|
327
|
+
// Should have all WorkflowContext properties
|
|
328
|
+
expectTypeOf(ctx.send).toBeFunction()
|
|
329
|
+
expectTypeOf(ctx.on).toEqualTypeOf<OnProxy>()
|
|
330
|
+
expectTypeOf(ctx.every).toEqualTypeOf<EveryProxy>()
|
|
331
|
+
expectTypeOf(ctx.state).toMatchTypeOf<Record<string, unknown>>()
|
|
332
|
+
expectTypeOf(ctx.getState).toBeFunction()
|
|
333
|
+
expectTypeOf(ctx.set).toBeFunction()
|
|
334
|
+
expectTypeOf(ctx.get).toBeFunction()
|
|
335
|
+
expectTypeOf(ctx.log).toBeFunction()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should have emittedEvents property typed', () => {
|
|
339
|
+
const ctx = createTestContext()
|
|
340
|
+
|
|
341
|
+
// Should have emittedEvents array
|
|
342
|
+
expectTypeOf(ctx.emittedEvents).toEqualTypeOf<Array<{ event: string; data: unknown }>>()
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
})
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { Workflow, createTestContext, parseEvent } from '../src/workflow.js';
|
|
3
|
+
import { clearEventHandlers } from '../src/on.js';
|
|
4
|
+
import { clearScheduleHandlers } from '../src/every.js';
|
|
5
|
+
describe('Workflow - unified $ API', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
clearEventHandlers();
|
|
8
|
+
clearScheduleHandlers();
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
14
|
+
describe('Workflow()', () => {
|
|
15
|
+
it('should create a workflow with $ context', () => {
|
|
16
|
+
const workflow = Workflow($ => {
|
|
17
|
+
// Just accessing $ to verify it works
|
|
18
|
+
expect($).toBeDefined();
|
|
19
|
+
expect($.on).toBeDefined();
|
|
20
|
+
expect($.every).toBeDefined();
|
|
21
|
+
expect($.send).toBeDefined();
|
|
22
|
+
expect($.log).toBeDefined();
|
|
23
|
+
expect($.get).toBeDefined();
|
|
24
|
+
expect($.set).toBeDefined();
|
|
25
|
+
expect($.getState).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
expect(workflow).toBeDefined();
|
|
28
|
+
expect(workflow.$).toBeDefined();
|
|
29
|
+
expect(workflow.send).toBeDefined();
|
|
30
|
+
expect(workflow.start).toBeDefined();
|
|
31
|
+
expect(workflow.stop).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
it('should capture event handlers registered via $.on', () => {
|
|
34
|
+
const workflow = Workflow($ => {
|
|
35
|
+
$.on.Customer.created(() => { });
|
|
36
|
+
$.on.Order.completed(() => { });
|
|
37
|
+
});
|
|
38
|
+
expect(workflow.definition.events).toHaveLength(2);
|
|
39
|
+
expect(workflow.definition.events[0]?.noun).toBe('Customer');
|
|
40
|
+
expect(workflow.definition.events[0]?.event).toBe('created');
|
|
41
|
+
expect(workflow.definition.events[1]?.noun).toBe('Order');
|
|
42
|
+
expect(workflow.definition.events[1]?.event).toBe('completed');
|
|
43
|
+
});
|
|
44
|
+
it('should capture schedule handlers registered via $.every', () => {
|
|
45
|
+
const workflow = Workflow($ => {
|
|
46
|
+
$.every.hour(() => { });
|
|
47
|
+
$.every.Monday.at9am(() => { });
|
|
48
|
+
});
|
|
49
|
+
expect(workflow.definition.schedules).toHaveLength(2);
|
|
50
|
+
});
|
|
51
|
+
it('should capture function source code', () => {
|
|
52
|
+
const workflow = Workflow($ => {
|
|
53
|
+
$.on.Test.event(async (data, ctx) => {
|
|
54
|
+
ctx.log('Test event', data);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// Source code is captured (variable names may be minified)
|
|
58
|
+
expect(workflow.definition.events[0]?.source).toBeDefined();
|
|
59
|
+
expect(workflow.definition.events[0]?.source.length).toBeGreaterThan(0);
|
|
60
|
+
expect(workflow.definition.events[0]?.source).toContain('Test event');
|
|
61
|
+
});
|
|
62
|
+
it('should deliver events to registered handlers', async () => {
|
|
63
|
+
const handler = vi.fn();
|
|
64
|
+
const workflow = Workflow($ => {
|
|
65
|
+
$.on.Customer.created(handler);
|
|
66
|
+
});
|
|
67
|
+
await workflow.start();
|
|
68
|
+
await workflow.send('Customer.created', { id: '123', name: 'John' });
|
|
69
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(handler).toHaveBeenCalledWith({ id: '123', name: 'John' }, expect.objectContaining({
|
|
71
|
+
send: expect.any(Function),
|
|
72
|
+
log: expect.any(Function),
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
it('should allow chained event sending from handlers', async () => {
|
|
76
|
+
const welcomeHandler = vi.fn();
|
|
77
|
+
const workflow = Workflow($ => {
|
|
78
|
+
$.on.Customer.created(async (customer, $) => {
|
|
79
|
+
await $.send('Email.welcome', { to: customer.email });
|
|
80
|
+
});
|
|
81
|
+
$.on.Email.welcome(welcomeHandler);
|
|
82
|
+
});
|
|
83
|
+
await workflow.start();
|
|
84
|
+
await workflow.send('Customer.created', { name: 'John', email: 'john@example.com' });
|
|
85
|
+
expect(welcomeHandler).toHaveBeenCalledWith({ to: 'john@example.com' }, expect.anything());
|
|
86
|
+
});
|
|
87
|
+
it('should track events in state history', async () => {
|
|
88
|
+
const workflow = Workflow($ => {
|
|
89
|
+
$.on.Test.event(() => { });
|
|
90
|
+
});
|
|
91
|
+
await workflow.start();
|
|
92
|
+
await workflow.send('Test.event', { data: 'test' });
|
|
93
|
+
expect(workflow.state.history).toHaveLength(1);
|
|
94
|
+
expect(workflow.state.history[0]).toMatchObject({
|
|
95
|
+
type: 'event',
|
|
96
|
+
name: 'Test.event',
|
|
97
|
+
data: { data: 'test' },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it('should trigger schedule handlers', async () => {
|
|
101
|
+
const handler = vi.fn();
|
|
102
|
+
const workflow = Workflow($ => {
|
|
103
|
+
$.every.seconds(1)(handler);
|
|
104
|
+
});
|
|
105
|
+
await workflow.start();
|
|
106
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
107
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
108
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
109
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
110
|
+
await workflow.stop();
|
|
111
|
+
});
|
|
112
|
+
it('should stop schedule handlers on stop', async () => {
|
|
113
|
+
const handler = vi.fn();
|
|
114
|
+
const workflow = Workflow($ => {
|
|
115
|
+
$.every.seconds(1)(handler);
|
|
116
|
+
});
|
|
117
|
+
await workflow.start();
|
|
118
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
119
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
120
|
+
await workflow.stop();
|
|
121
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
122
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
123
|
+
});
|
|
124
|
+
it('should support $.set and $.get for context data', async () => {
|
|
125
|
+
const workflow = Workflow($ => {
|
|
126
|
+
$.on.Test.set(async (data, $) => {
|
|
127
|
+
$.set('value', data.value);
|
|
128
|
+
});
|
|
129
|
+
$.on.Test.get(async (_, $) => {
|
|
130
|
+
const value = $.get('value');
|
|
131
|
+
$.log('Got value', value);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
await workflow.start();
|
|
135
|
+
await workflow.send('Test.set', { value: 42 });
|
|
136
|
+
expect(workflow.state.context.value).toBe(42);
|
|
137
|
+
expect(workflow.$.get('value')).toBe(42);
|
|
138
|
+
});
|
|
139
|
+
it('should use initial context from options', () => {
|
|
140
|
+
const workflow = Workflow($ => { }, { context: { counter: 100 } });
|
|
141
|
+
expect(workflow.state.context.counter).toBe(100);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('parseEvent', () => {
|
|
145
|
+
it('should parse valid event strings', () => {
|
|
146
|
+
expect(parseEvent('Customer.created')).toEqual({
|
|
147
|
+
noun: 'Customer',
|
|
148
|
+
event: 'created',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('should return null for invalid event strings', () => {
|
|
152
|
+
expect(parseEvent('invalid')).toBeNull();
|
|
153
|
+
expect(parseEvent('too.many.parts')).toBeNull();
|
|
154
|
+
expect(parseEvent('')).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('createTestContext', () => {
|
|
158
|
+
it('should create a $ context for testing', () => {
|
|
159
|
+
const $ = createTestContext();
|
|
160
|
+
expect($.send).toBeDefined();
|
|
161
|
+
expect($.on).toBeDefined();
|
|
162
|
+
expect($.every).toBeDefined();
|
|
163
|
+
expect($.log).toBeDefined();
|
|
164
|
+
expect($.get).toBeDefined();
|
|
165
|
+
expect($.set).toBeDefined();
|
|
166
|
+
expect($.getState).toBeDefined();
|
|
167
|
+
expect($.emittedEvents).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
it('should track emitted events', async () => {
|
|
170
|
+
const $ = createTestContext();
|
|
171
|
+
await $.send('Test.event1', { a: 1 });
|
|
172
|
+
await $.send('Test.event2', { b: 2 });
|
|
173
|
+
expect($.emittedEvents).toHaveLength(2);
|
|
174
|
+
expect($.emittedEvents[0]).toEqual({ event: 'Test.event1', data: { a: 1 } });
|
|
175
|
+
expect($.emittedEvents[1]).toEqual({ event: 'Test.event2', data: { b: 2 } });
|
|
176
|
+
});
|
|
177
|
+
it('should support get/set', () => {
|
|
178
|
+
const $ = createTestContext();
|
|
179
|
+
$.set('key', 'value');
|
|
180
|
+
expect($.get('key')).toBe('value');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('$.every patterns', () => {
|
|
184
|
+
it('should support $.every.hour', () => {
|
|
185
|
+
const workflow = Workflow($ => {
|
|
186
|
+
$.every.hour(() => { });
|
|
187
|
+
});
|
|
188
|
+
expect(workflow.definition.schedules[0]?.interval).toEqual({
|
|
189
|
+
type: 'cron',
|
|
190
|
+
expression: '0 * * * *',
|
|
191
|
+
natural: 'hour',
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it('should support $.every.Monday.at9am', () => {
|
|
195
|
+
const workflow = Workflow($ => {
|
|
196
|
+
$.every.Monday.at9am(() => { });
|
|
197
|
+
});
|
|
198
|
+
expect(workflow.definition.schedules[0]?.interval).toEqual({
|
|
199
|
+
type: 'cron',
|
|
200
|
+
expression: '0 9 * * 1',
|
|
201
|
+
natural: 'Monday.at9am',
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
it('should support $.every.minutes(30)', () => {
|
|
205
|
+
const workflow = Workflow($ => {
|
|
206
|
+
$.every.minutes(30)(() => { });
|
|
207
|
+
});
|
|
208
|
+
expect(workflow.definition.schedules[0]?.interval).toEqual({
|
|
209
|
+
type: 'minute',
|
|
210
|
+
value: 30,
|
|
211
|
+
natural: '30 minutes',
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
it('should support $.every("natural language")', () => {
|
|
215
|
+
const workflow = Workflow($ => {
|
|
216
|
+
$.every('first Monday of the month', () => { });
|
|
217
|
+
});
|
|
218
|
+
expect(workflow.definition.schedules[0]?.interval).toEqual({
|
|
219
|
+
type: 'natural',
|
|
220
|
+
description: 'first Monday of the month',
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|