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.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/.turbo/turbo-test.log +169 -0
  3. package/CHANGELOG.md +29 -0
  4. package/LICENSE +21 -0
  5. package/README.md +303 -184
  6. package/dist/barrier.d.ts +153 -0
  7. package/dist/barrier.d.ts.map +1 -0
  8. package/dist/barrier.js +339 -0
  9. package/dist/barrier.js.map +1 -0
  10. package/dist/cascade-context.d.ts +149 -0
  11. package/dist/cascade-context.d.ts.map +1 -0
  12. package/dist/cascade-context.js +324 -0
  13. package/dist/cascade-context.js.map +1 -0
  14. package/dist/cascade-executor.d.ts +196 -0
  15. package/dist/cascade-executor.d.ts.map +1 -0
  16. package/dist/cascade-executor.js +384 -0
  17. package/dist/cascade-executor.js.map +1 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +4 -1
  20. package/dist/context.js.map +1 -1
  21. package/dist/dependency-graph.d.ts +157 -0
  22. package/dist/dependency-graph.d.ts.map +1 -0
  23. package/dist/dependency-graph.js +382 -0
  24. package/dist/dependency-graph.js.map +1 -0
  25. package/dist/every.d.ts +31 -2
  26. package/dist/every.d.ts.map +1 -1
  27. package/dist/every.js +63 -32
  28. package/dist/every.js.map +1 -1
  29. package/dist/graph/index.d.ts +8 -0
  30. package/dist/graph/index.d.ts.map +1 -0
  31. package/dist/graph/index.js +8 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/graph/topological-sort.d.ts +121 -0
  34. package/dist/graph/topological-sort.d.ts.map +1 -0
  35. package/dist/graph/topological-sort.js +292 -0
  36. package/dist/graph/topological-sort.js.map +1 -0
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/on.d.ts +35 -10
  42. package/dist/on.d.ts.map +1 -1
  43. package/dist/on.js +52 -18
  44. package/dist/on.js.map +1 -1
  45. package/dist/send.d.ts +0 -5
  46. package/dist/send.d.ts.map +1 -1
  47. package/dist/send.js +1 -14
  48. package/dist/send.js.map +1 -1
  49. package/dist/timer-registry.d.ts +52 -0
  50. package/dist/timer-registry.d.ts.map +1 -0
  51. package/dist/timer-registry.js +120 -0
  52. package/dist/timer-registry.js.map +1 -0
  53. package/dist/types.d.ts +171 -9
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +17 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/workflow.d.ts.map +1 -1
  58. package/dist/workflow.js +22 -18
  59. package/dist/workflow.js.map +1 -1
  60. package/package.json +12 -16
  61. package/src/barrier.ts +466 -0
  62. package/src/cascade-context.ts +488 -0
  63. package/src/cascade-executor.ts +587 -0
  64. package/src/context.js +83 -0
  65. package/src/context.ts +12 -7
  66. package/src/dependency-graph.ts +518 -0
  67. package/src/every.js +267 -0
  68. package/src/every.ts +104 -35
  69. package/src/graph/index.ts +19 -0
  70. package/src/graph/topological-sort.ts +414 -0
  71. package/src/index.js +71 -0
  72. package/src/index.ts +78 -0
  73. package/src/on.js +79 -0
  74. package/src/on.ts +81 -25
  75. package/src/send.js +111 -0
  76. package/src/send.ts +1 -16
  77. package/src/timer-registry.ts +145 -0
  78. package/src/types.js +4 -0
  79. package/src/types.ts +218 -11
  80. package/src/workflow.js +455 -0
  81. package/src/workflow.ts +32 -23
  82. package/test/barrier-join.test.ts +434 -0
  83. package/test/barrier-unhandled-rejections.test.ts +359 -0
  84. package/test/cascade-context.test.ts +390 -0
  85. package/test/cascade-executor.test.ts +859 -0
  86. package/test/context.test.js +116 -0
  87. package/test/dependency-graph.test.ts +512 -0
  88. package/test/every.test.js +282 -0
  89. package/test/graph/topological-sort.test.ts +586 -0
  90. package/test/on.test.js +80 -0
  91. package/test/schedule-timer-cleanup.test.ts +344 -0
  92. package/test/send-race-conditions.test.ts +410 -0
  93. package/test/send.test.js +89 -0
  94. package/test/type-safety-every.test.ts +303 -0
  95. package/test/types-event-handler.test.ts +225 -0
  96. package/test/types-proxy-autocomplete.test.ts +345 -0
  97. package/test/workflow.test.js +224 -0
  98. 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
+ });
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ export default defineConfig({
3
+ test: {
4
+ globals: true,
5
+ include: ['test/**/*.test.ts'],
6
+ },
7
+ });