ai-workflows 2.0.2 → 2.1.1

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.
@@ -0,0 +1,225 @@
1
+ /**
2
+ * TDD RED Phase Tests: EventHandler generic order
3
+ * Issue: primitives.org.ai-01s
4
+ *
5
+ * These tests verify that EventHandler uses <TOutput, TInput> order
6
+ * (output first, then input), consistent with Promise<T> patterns.
7
+ *
8
+ * Current implementation uses <T, R> where T is input, R is output.
9
+ * These tests should FAIL until the type order is fixed.
10
+ *
11
+ * The current signature is:
12
+ * EventHandler<T = unknown, R = unknown> = (data: T, $) => R | void | Promise<R | void>
13
+ *
14
+ * The desired signature is:
15
+ * EventHandler<TOutput = unknown, TInput = unknown> = (data: TInput, $) => TOutput | void | Promise<TOutput | void>
16
+ *
17
+ * TEST STRATEGY:
18
+ * - Tests use @ts-expect-error to mark lines that SHOULD error with CURRENT types
19
+ * - When types are FIXED, these @ts-expect-error comments will become errors
20
+ * (because the expected error won't occur)
21
+ * - This makes the test file fail to compile when types are correct = TDD RED
22
+ */
23
+ import { describe, it, expect, expectTypeOf, assertType } from 'vitest'
24
+ import type { EventHandler, WorkflowContext } from '../src/types.js'
25
+ import { Workflow } from '../src/workflow.js'
26
+
27
+ describe('EventHandler generic order - TDD RED', () => {
28
+ // Test data types
29
+ interface CustomerData {
30
+ id: string
31
+ name: string
32
+ email: string
33
+ }
34
+
35
+ interface ProcessResult {
36
+ success: boolean
37
+ processedAt: number
38
+ }
39
+
40
+ interface OrderInput {
41
+ orderId: string
42
+ items: string[]
43
+ }
44
+
45
+ describe('EventHandler<TOutput, TInput> order - documents current behavior', () => {
46
+ /**
47
+ * This test documents the CURRENT behavior:
48
+ * - EventHandler<T, R> where T is INPUT (first), R is OUTPUT (second)
49
+ *
50
+ * When we CHANGE to <TOutput, TInput>:
51
+ * - The @ts-expect-error comments below will fail (no error to expect)
52
+ * - The test file won't compile until @ts-expect-error is removed
53
+ */
54
+
55
+ it('documents: FIXED types have output first, input second', () => {
56
+ // FIXED: EventHandler<Output, Input>
57
+ // - First generic (TOutput) is the return type
58
+ // - Second generic (TInput) is the data parameter type
59
+ //
60
+ // EventHandler<ProcessResult, OrderInput> means:
61
+ // - data: OrderInput (second generic)
62
+ // - returns: ProcessResult (first generic)
63
+ const handler: EventHandler<ProcessResult, OrderInput> = (data, $) => {
64
+ // With FIXED types, data is OrderInput (second generic)
65
+ const _orderId: string = data.orderId
66
+ const _items: string[] = data.items
67
+
68
+ // Return type is ProcessResult (first generic)
69
+ return { success: true, processedAt: Date.now() }
70
+ }
71
+
72
+ expect(handler).toBeDefined()
73
+ })
74
+
75
+ it('documents: calling handler with correct args works with fixed types', () => {
76
+ const handler: EventHandler<ProcessResult, OrderInput> = (data, $) => {
77
+ // Fixed: data is OrderInput, returns ProcessResult
78
+ return { success: true, processedAt: Date.now() }
79
+ }
80
+
81
+ // With FIXED types, first arg should be OrderInput
82
+ handler({ orderId: '1', items: [] }, {} as WorkflowContext)
83
+ })
84
+
85
+ it('PASSES: data is second generic with fixed types', () => {
86
+ // With FIXED types <TOutput, TInput>:
87
+ // EventHandler<ProcessResult, OrderInput> means:
88
+ // - data: OrderInput (second generic)
89
+ // - return: ProcessResult (first generic)
90
+
91
+ const handler: EventHandler<ProcessResult, OrderInput> = (data, $) => {
92
+ // Data is correctly typed as OrderInput (second generic)
93
+ const orderId: string = data.orderId
94
+ const items: string[] = data.items
95
+
96
+ return { success: true, processedAt: Date.now() }
97
+ }
98
+
99
+ // Calling with OrderInput as data - now works correctly
100
+ handler({ orderId: '123', items: ['a'] }, {} as WorkflowContext)
101
+ })
102
+ })
103
+
104
+ describe('WorkflowContext.do<TResult, TInput> order - FIXED types', () => {
105
+ /**
106
+ * FIXED $.do signature: <TResult, TInput>(event, data: TInput) => Promise<TResult>
107
+ *
108
+ * The order puts Result first (like Promise<T>)
109
+ */
110
+ it('documents: FIXED $.do has result first, input second', () => {
111
+ const workflow = Workflow($ => {
112
+ $.on.Order.process(() => ({ success: true, processedAt: Date.now() }))
113
+ })
114
+
115
+ // With FIXED types: $.do<TResult, TInput>
116
+ // - First generic (TResult) is the result/output type
117
+ // - Second generic (TInput) is the data/input type
118
+
119
+ // $.do<ProcessResult, OrderInput> means data: OrderInput, returns Promise<ProcessResult>
120
+ workflow.$.do<ProcessResult, OrderInput>('Order.process', { orderId: '123', items: [] })
121
+ })
122
+
123
+ it('PASSES: $.do accepts data as second generic type', async () => {
124
+ const workflow = Workflow($ => {
125
+ $.on.Order.process((order: OrderInput) => ({
126
+ success: true,
127
+ processedAt: Date.now()
128
+ }))
129
+ })
130
+
131
+ try {
132
+ // With FIXED types, data is OrderInput (second generic), result is ProcessResult (first generic)
133
+ const result: ProcessResult = await workflow.$.do<ProcessResult, OrderInput>(
134
+ 'Order.process',
135
+ { orderId: '123', items: ['item1'] } // OrderInput - accepted with fixed types
136
+ )
137
+ } catch {
138
+ // Expected to throw - we're testing types, not runtime
139
+ }
140
+ })
141
+ })
142
+
143
+ describe('WorkflowContext.try<TResult, TInput> order - FIXED types', () => {
144
+ it('documents: FIXED $.try has result first, input second', () => {
145
+ const workflow = Workflow($ => {
146
+ $.on.Test.action(() => ({ orderId: 'x', items: [] }))
147
+ })
148
+
149
+ // Same pattern as $.do - don't await to avoid runtime error
150
+ // $.try<ProcessResult, OrderInput> means data: OrderInput, returns Promise<ProcessResult>
151
+ const _promise = workflow.$.try<ProcessResult, OrderInput>('Test.action', { orderId: 'x', items: [] })
152
+ })
153
+
154
+ it('PASSES: $.try accepts data as second generic type', async () => {
155
+ const workflow = Workflow($ => {
156
+ $.on.Payment.validate((input: { amount: number }) => ({ valid: input.amount > 0 }))
157
+ })
158
+
159
+ try {
160
+ // With FIXED types, data is { amount: number }, result is { valid: boolean }
161
+ const result: { valid: boolean } = await workflow.$.try<{ valid: boolean }, { amount: number }>(
162
+ 'Payment.validate',
163
+ { amount: 100 } // Accepted with fixed types
164
+ )
165
+ } catch {
166
+ // Expected
167
+ }
168
+ })
169
+ })
170
+
171
+ describe('Type inference consistency', () => {
172
+ /**
173
+ * When the generic order is fixed, these patterns should all work:
174
+ *
175
+ * EventHandler<void, CustomerData> - fire and forget with CustomerData input
176
+ * EventHandler<ProcessResult, void> - returns ProcessResult, no input needed
177
+ * EventHandler<string, number> - takes number, returns string
178
+ *
179
+ * Like Promise<T> where T is what you get, not what you put in
180
+ */
181
+
182
+ it('should support EventHandler<void, InputType> for fire-and-forget handlers', () => {
183
+ // A handler that receives data but returns nothing
184
+ type FireForgetHandler = EventHandler<void, CustomerData>
185
+
186
+ const handler: FireForgetHandler = (customer, $) => {
187
+ // customer should be CustomerData (second generic = input)
188
+ // Note: we don't call $.log here to avoid runtime issues with mock context
189
+ console.log(`Processing ${customer.name} (${customer.email})`)
190
+ // Returns void (first generic = output)
191
+ }
192
+
193
+ // Should be callable with CustomerData - using real workflow context
194
+ const workflow = Workflow(_ => {})
195
+ handler({ id: '1', name: 'John', email: 'john@example.com' }, workflow.$)
196
+ })
197
+
198
+ it('should support EventHandler<OutputType, void> for parameterless handlers', () => {
199
+ // A handler that takes no data but returns a result
200
+ // This is less common but should be expressible
201
+ type NoInputHandler = EventHandler<ProcessResult, void>
202
+
203
+ // Note: This is a stretch case - handlers always receive data param
204
+ // but we should be able to type it as void/undefined
205
+ })
206
+
207
+ it('should mirror Promise<T> convention where T is the resolved value', () => {
208
+ // Just as Promise<string> means "will resolve to string"
209
+ // EventHandler<string, number> should mean "will return string (given number input)"
210
+
211
+ // Test that first generic controls return type
212
+ type StringReturningHandler = EventHandler<string, number>
213
+
214
+ const handler: StringReturningHandler = (num, $) => {
215
+ return `The number is ${num}`
216
+ }
217
+
218
+ const result = handler(42, {} as WorkflowContext)
219
+ // With correct types, result should be string | void | Promise<...>
220
+ if (typeof result === 'string') {
221
+ expect(result).toBe('The number is 42')
222
+ }
223
+ })
224
+ })
225
+ })
@@ -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
+ })