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.
- package/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-test.log +7 -0
- package/CHANGELOG.md +20 -0
- 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/types.d.ts +83 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +7 -7
- package/dist/workflow.js.map +1 -1
- package/package.json +2 -6
- package/src/context.js +83 -0
- package/src/every.js +267 -0
- package/src/index.js +71 -0
- package/src/on.js +79 -0
- package/src/send.js +111 -0
- package/src/send.ts +1 -16
- package/src/types.js +4 -0
- package/src/types.ts +97 -11
- package/src/workflow.js +455 -0
- package/src/workflow.ts +9 -7
- package/test/context.test.js +116 -0
- package/test/every.test.js +282 -0
- package/test/on.test.js +80 -0
- package/test/send.test.js +89 -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,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
|
+
})
|