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,116 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createWorkflowContext, createIsolatedContext } from '../src/context.js';
|
|
3
|
+
describe('context - workflow context', () => {
|
|
4
|
+
describe('createWorkflowContext', () => {
|
|
5
|
+
it('should create a context with all required methods', () => {
|
|
6
|
+
const eventBus = { emit: vi.fn() };
|
|
7
|
+
const ctx = createWorkflowContext(eventBus);
|
|
8
|
+
expect(ctx).toHaveProperty('send');
|
|
9
|
+
expect(ctx).toHaveProperty('getState');
|
|
10
|
+
expect(ctx).toHaveProperty('set');
|
|
11
|
+
expect(ctx).toHaveProperty('get');
|
|
12
|
+
expect(ctx).toHaveProperty('log');
|
|
13
|
+
});
|
|
14
|
+
it('should emit events through the event bus', async () => {
|
|
15
|
+
const eventBus = { emit: vi.fn() };
|
|
16
|
+
const ctx = createWorkflowContext(eventBus);
|
|
17
|
+
await ctx.send('Customer.created', { id: '123' });
|
|
18
|
+
expect(eventBus.emit).toHaveBeenCalledWith('Customer.created', { id: '123' });
|
|
19
|
+
});
|
|
20
|
+
it('should track events in history', async () => {
|
|
21
|
+
const eventBus = { emit: vi.fn() };
|
|
22
|
+
const ctx = createWorkflowContext(eventBus);
|
|
23
|
+
await ctx.send('Customer.created', { id: '123' });
|
|
24
|
+
const state = ctx.getState();
|
|
25
|
+
expect(state.history).toHaveLength(1);
|
|
26
|
+
expect(state.history[0]).toMatchObject({
|
|
27
|
+
type: 'event',
|
|
28
|
+
name: 'Customer.created',
|
|
29
|
+
data: { id: '123' },
|
|
30
|
+
});
|
|
31
|
+
expect(state.history[0]?.timestamp).toBeGreaterThan(0);
|
|
32
|
+
});
|
|
33
|
+
it('should store and retrieve context data', () => {
|
|
34
|
+
const eventBus = { emit: vi.fn() };
|
|
35
|
+
const ctx = createWorkflowContext(eventBus);
|
|
36
|
+
ctx.set('userId', '123');
|
|
37
|
+
ctx.set('counter', 42);
|
|
38
|
+
expect(ctx.get('userId')).toBe('123');
|
|
39
|
+
expect(ctx.get('counter')).toBe(42);
|
|
40
|
+
expect(ctx.get('nonexistent')).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
it('should return typed values from get', () => {
|
|
43
|
+
const eventBus = { emit: vi.fn() };
|
|
44
|
+
const ctx = createWorkflowContext(eventBus);
|
|
45
|
+
ctx.set('user', { name: 'John', age: 30 });
|
|
46
|
+
const user = ctx.get('user');
|
|
47
|
+
expect(user?.name).toBe('John');
|
|
48
|
+
expect(user?.age).toBe(30);
|
|
49
|
+
});
|
|
50
|
+
it('should include context data in state', () => {
|
|
51
|
+
const eventBus = { emit: vi.fn() };
|
|
52
|
+
const ctx = createWorkflowContext(eventBus);
|
|
53
|
+
ctx.set('key1', 'value1');
|
|
54
|
+
ctx.set('key2', 'value2');
|
|
55
|
+
const state = ctx.getState();
|
|
56
|
+
expect(state.context).toEqual({
|
|
57
|
+
key1: 'value1',
|
|
58
|
+
key2: 'value2',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it('should return a copy of state to prevent mutation', () => {
|
|
62
|
+
const eventBus = { emit: vi.fn() };
|
|
63
|
+
const ctx = createWorkflowContext(eventBus);
|
|
64
|
+
ctx.set('key', 'original');
|
|
65
|
+
const state1 = ctx.getState();
|
|
66
|
+
state1.context.key = 'mutated';
|
|
67
|
+
const state2 = ctx.getState();
|
|
68
|
+
expect(state2.context.key).toBe('original');
|
|
69
|
+
});
|
|
70
|
+
it('should log messages with history tracking', () => {
|
|
71
|
+
const eventBus = { emit: vi.fn() };
|
|
72
|
+
const ctx = createWorkflowContext(eventBus);
|
|
73
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
74
|
+
ctx.log('Test message', { extra: 'data' });
|
|
75
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
76
|
+
const state = ctx.getState();
|
|
77
|
+
expect(state.history).toHaveLength(1);
|
|
78
|
+
expect(state.history[0]).toMatchObject({
|
|
79
|
+
type: 'action',
|
|
80
|
+
name: 'log',
|
|
81
|
+
data: { message: 'Test message', data: { extra: 'data' } },
|
|
82
|
+
});
|
|
83
|
+
consoleSpy.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('createIsolatedContext', () => {
|
|
87
|
+
it('should create a context not connected to real event bus', async () => {
|
|
88
|
+
const ctx = createIsolatedContext();
|
|
89
|
+
// Should not throw
|
|
90
|
+
await ctx.send('Test.event', { data: 'test' });
|
|
91
|
+
// Should track the event
|
|
92
|
+
const emittedEvents = ctx.getEmittedEvents();
|
|
93
|
+
expect(emittedEvents).toHaveLength(1);
|
|
94
|
+
expect(emittedEvents[0]).toEqual({
|
|
95
|
+
event: 'Test.event',
|
|
96
|
+
data: { data: 'test' },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('should track multiple emitted events', async () => {
|
|
100
|
+
const ctx = createIsolatedContext();
|
|
101
|
+
await ctx.send('Event1', { a: 1 });
|
|
102
|
+
await ctx.send('Event2', { b: 2 });
|
|
103
|
+
await ctx.send('Event3', { c: 3 });
|
|
104
|
+
const emittedEvents = ctx.getEmittedEvents();
|
|
105
|
+
expect(emittedEvents).toHaveLength(3);
|
|
106
|
+
});
|
|
107
|
+
it('should have all standard context methods', () => {
|
|
108
|
+
const ctx = createIsolatedContext();
|
|
109
|
+
expect(typeof ctx.send).toBe('function');
|
|
110
|
+
expect(typeof ctx.getState).toBe('function');
|
|
111
|
+
expect(typeof ctx.set).toBe('function');
|
|
112
|
+
expect(typeof ctx.get).toBe('function');
|
|
113
|
+
expect(typeof ctx.log).toBe('function');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { every, registerScheduleHandler, getScheduleHandlers, clearScheduleHandlers, toCron, intervalToMs, formatInterval, setCronConverter, } from '../src/every.js';
|
|
3
|
+
describe('every - schedule handler registration', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
clearScheduleHandlers();
|
|
6
|
+
});
|
|
7
|
+
describe('every.interval pattern', () => {
|
|
8
|
+
it('should register handler for every.hour', () => {
|
|
9
|
+
const handler = () => { };
|
|
10
|
+
every.hour(handler);
|
|
11
|
+
const handlers = getScheduleHandlers();
|
|
12
|
+
expect(handlers).toHaveLength(1);
|
|
13
|
+
expect(handlers[0]?.interval).toEqual({
|
|
14
|
+
type: 'cron',
|
|
15
|
+
expression: '0 * * * *',
|
|
16
|
+
natural: 'hour',
|
|
17
|
+
});
|
|
18
|
+
expect(handlers[0]?.source).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
it('should register handler for every.day', () => {
|
|
21
|
+
every.day(() => { });
|
|
22
|
+
const handlers = getScheduleHandlers();
|
|
23
|
+
expect(handlers[0]?.interval).toEqual({
|
|
24
|
+
type: 'cron',
|
|
25
|
+
expression: '0 0 * * *',
|
|
26
|
+
natural: 'day',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it('should register handler for every.minute', () => {
|
|
30
|
+
every.minute(() => { });
|
|
31
|
+
const handlers = getScheduleHandlers();
|
|
32
|
+
expect(handlers[0]?.interval).toEqual({
|
|
33
|
+
type: 'cron',
|
|
34
|
+
expression: '* * * * *',
|
|
35
|
+
natural: 'minute',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('should register handler for every.week', () => {
|
|
39
|
+
every.week(() => { });
|
|
40
|
+
const handlers = getScheduleHandlers();
|
|
41
|
+
expect(handlers[0]?.interval).toEqual({
|
|
42
|
+
type: 'cron',
|
|
43
|
+
expression: '0 0 * * 0',
|
|
44
|
+
natural: 'week',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('every.Day pattern', () => {
|
|
49
|
+
it('should register handler for every.Monday', () => {
|
|
50
|
+
every.Monday(() => { });
|
|
51
|
+
const handlers = getScheduleHandlers();
|
|
52
|
+
expect(handlers[0]?.interval).toEqual({
|
|
53
|
+
type: 'cron',
|
|
54
|
+
expression: '0 0 * * 1',
|
|
55
|
+
natural: 'Monday',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('should register handler for every.Friday', () => {
|
|
59
|
+
every.Friday(() => { });
|
|
60
|
+
const handlers = getScheduleHandlers();
|
|
61
|
+
expect(handlers[0]?.interval).toEqual({
|
|
62
|
+
type: 'cron',
|
|
63
|
+
expression: '0 0 * * 5',
|
|
64
|
+
natural: 'Friday',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('should register handler for every.weekday', () => {
|
|
68
|
+
every.weekday(() => { });
|
|
69
|
+
const handlers = getScheduleHandlers();
|
|
70
|
+
expect(handlers[0]?.interval).toEqual({
|
|
71
|
+
type: 'cron',
|
|
72
|
+
expression: '0 0 * * 1-5',
|
|
73
|
+
natural: 'weekday',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('should register handler for every.weekend', () => {
|
|
77
|
+
every.weekend(() => { });
|
|
78
|
+
const handlers = getScheduleHandlers();
|
|
79
|
+
expect(handlers[0]?.interval).toEqual({
|
|
80
|
+
type: 'cron',
|
|
81
|
+
expression: '0 0 * * 0,6',
|
|
82
|
+
natural: 'weekend',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('every.Day.atTime pattern', () => {
|
|
87
|
+
it('should register handler for every.Monday.at9am', () => {
|
|
88
|
+
every.Monday.at9am(() => { });
|
|
89
|
+
const handlers = getScheduleHandlers();
|
|
90
|
+
expect(handlers[0]?.interval).toEqual({
|
|
91
|
+
type: 'cron',
|
|
92
|
+
expression: '0 9 * * 1',
|
|
93
|
+
natural: 'Monday.at9am',
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('should register handler for every.Friday.at5pm', () => {
|
|
97
|
+
every.Friday.at5pm(() => { });
|
|
98
|
+
const handlers = getScheduleHandlers();
|
|
99
|
+
expect(handlers[0]?.interval).toEqual({
|
|
100
|
+
type: 'cron',
|
|
101
|
+
expression: '0 17 * * 5',
|
|
102
|
+
natural: 'Friday.at5pm',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('should register handler for every.weekday.at8am', () => {
|
|
106
|
+
every.weekday.at8am(() => { });
|
|
107
|
+
const handlers = getScheduleHandlers();
|
|
108
|
+
expect(handlers[0]?.interval).toEqual({
|
|
109
|
+
type: 'cron',
|
|
110
|
+
expression: '0 8 * * 1-5',
|
|
111
|
+
natural: 'weekday.at8am',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it('should register handler for every.Thursday.atnoon', () => {
|
|
115
|
+
every.Thursday.atnoon(() => { });
|
|
116
|
+
const handlers = getScheduleHandlers();
|
|
117
|
+
expect(handlers[0]?.interval).toEqual({
|
|
118
|
+
type: 'cron',
|
|
119
|
+
expression: '0 12 * * 4',
|
|
120
|
+
natural: 'Thursday.atnoon',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('should register handler for every.Sunday.atmidnight', () => {
|
|
124
|
+
every.Sunday.atmidnight(() => { });
|
|
125
|
+
const handlers = getScheduleHandlers();
|
|
126
|
+
expect(handlers[0]?.interval).toEqual({
|
|
127
|
+
type: 'cron',
|
|
128
|
+
expression: '0 0 * * 0',
|
|
129
|
+
natural: 'Sunday.atmidnight',
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('every.units(value) pattern', () => {
|
|
134
|
+
it('should register handler for every.minutes(30)', () => {
|
|
135
|
+
every.minutes(30)(() => { });
|
|
136
|
+
const handlers = getScheduleHandlers();
|
|
137
|
+
expect(handlers[0]?.interval).toEqual({
|
|
138
|
+
type: 'minute',
|
|
139
|
+
value: 30,
|
|
140
|
+
natural: '30 minutes',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('should register handler for every.hours(4)', () => {
|
|
144
|
+
every.hours(4)(() => { });
|
|
145
|
+
const handlers = getScheduleHandlers();
|
|
146
|
+
expect(handlers[0]?.interval).toEqual({
|
|
147
|
+
type: 'hour',
|
|
148
|
+
value: 4,
|
|
149
|
+
natural: '4 hours',
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
it('should register handler for every.seconds(10)', () => {
|
|
153
|
+
every.seconds(10)(() => { });
|
|
154
|
+
const handlers = getScheduleHandlers();
|
|
155
|
+
expect(handlers[0]?.interval).toEqual({
|
|
156
|
+
type: 'second',
|
|
157
|
+
value: 10,
|
|
158
|
+
natural: '10 seconds',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('every(natural language) pattern', () => {
|
|
163
|
+
it('should register handler with natural language description', () => {
|
|
164
|
+
every('first Monday of the month at 9am', () => { });
|
|
165
|
+
const handlers = getScheduleHandlers();
|
|
166
|
+
expect(handlers[0]?.interval).toEqual({
|
|
167
|
+
type: 'natural',
|
|
168
|
+
description: 'first Monday of the month at 9am',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
it('should capture source code', () => {
|
|
172
|
+
every('weekly', async ($) => {
|
|
173
|
+
$.log('weekly task');
|
|
174
|
+
});
|
|
175
|
+
const handlers = getScheduleHandlers();
|
|
176
|
+
expect(handlers[0]?.source).toContain('$.log');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('registerScheduleHandler', () => {
|
|
180
|
+
it('should register handler directly', () => {
|
|
181
|
+
const handler = () => { };
|
|
182
|
+
registerScheduleHandler({ type: 'hour', value: 2 }, handler);
|
|
183
|
+
const handlers = getScheduleHandlers();
|
|
184
|
+
expect(handlers).toHaveLength(1);
|
|
185
|
+
expect(handlers[0]?.interval).toEqual({ type: 'hour', value: 2 });
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe('clearScheduleHandlers', () => {
|
|
189
|
+
it('should clear all handlers', () => {
|
|
190
|
+
every.hour(() => { });
|
|
191
|
+
every.day(() => { });
|
|
192
|
+
expect(getScheduleHandlers()).toHaveLength(2);
|
|
193
|
+
clearScheduleHandlers();
|
|
194
|
+
expect(getScheduleHandlers()).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe('toCron', () => {
|
|
199
|
+
it('should return known patterns directly', async () => {
|
|
200
|
+
expect(await toCron('hour')).toBe('0 * * * *');
|
|
201
|
+
expect(await toCron('day')).toBe('0 0 * * *');
|
|
202
|
+
expect(await toCron('Monday')).toBe('0 0 * * 1');
|
|
203
|
+
expect(await toCron('weekday')).toBe('0 0 * * 1-5');
|
|
204
|
+
});
|
|
205
|
+
it('should return cron expression if already valid', async () => {
|
|
206
|
+
expect(await toCron('0 9 * * 1-5')).toBe('0 9 * * 1-5');
|
|
207
|
+
expect(await toCron('*/15 * * * *')).toBe('*/15 * * * *');
|
|
208
|
+
});
|
|
209
|
+
it('should throw for unknown patterns without converter', async () => {
|
|
210
|
+
await expect(toCron('every 15 minutes during business hours')).rejects.toThrow('Unknown schedule pattern');
|
|
211
|
+
});
|
|
212
|
+
it('should use custom converter when set', async () => {
|
|
213
|
+
setCronConverter(async (desc) => {
|
|
214
|
+
if (desc.includes('business hours')) {
|
|
215
|
+
return '*/15 9-17 * * 1-5';
|
|
216
|
+
}
|
|
217
|
+
return '* * * * *';
|
|
218
|
+
});
|
|
219
|
+
expect(await toCron('every 15 minutes during business hours')).toBe('*/15 9-17 * * 1-5');
|
|
220
|
+
// Reset converter
|
|
221
|
+
setCronConverter(async () => '* * * * *');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('intervalToMs', () => {
|
|
225
|
+
it('should convert second intervals', () => {
|
|
226
|
+
expect(intervalToMs({ type: 'second' })).toBe(1000);
|
|
227
|
+
expect(intervalToMs({ type: 'second', value: 5 })).toBe(5000);
|
|
228
|
+
});
|
|
229
|
+
it('should convert minute intervals', () => {
|
|
230
|
+
expect(intervalToMs({ type: 'minute' })).toBe(60000);
|
|
231
|
+
expect(intervalToMs({ type: 'minute', value: 30 })).toBe(1800000);
|
|
232
|
+
});
|
|
233
|
+
it('should convert hour intervals', () => {
|
|
234
|
+
expect(intervalToMs({ type: 'hour' })).toBe(3600000);
|
|
235
|
+
expect(intervalToMs({ type: 'hour', value: 4 })).toBe(14400000);
|
|
236
|
+
});
|
|
237
|
+
it('should convert day intervals', () => {
|
|
238
|
+
expect(intervalToMs({ type: 'day' })).toBe(86400000);
|
|
239
|
+
expect(intervalToMs({ type: 'day', value: 2 })).toBe(172800000);
|
|
240
|
+
});
|
|
241
|
+
it('should convert week intervals', () => {
|
|
242
|
+
expect(intervalToMs({ type: 'week' })).toBe(604800000);
|
|
243
|
+
expect(intervalToMs({ type: 'week', value: 2 })).toBe(1209600000);
|
|
244
|
+
});
|
|
245
|
+
it('should return 0 for cron intervals', () => {
|
|
246
|
+
expect(intervalToMs({ type: 'cron', expression: '0 * * * *' })).toBe(0);
|
|
247
|
+
});
|
|
248
|
+
it('should return 0 for natural intervals', () => {
|
|
249
|
+
expect(intervalToMs({ type: 'natural', description: 'every hour' })).toBe(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
describe('formatInterval', () => {
|
|
253
|
+
it('should format second intervals', () => {
|
|
254
|
+
expect(formatInterval({ type: 'second' })).toBe('every second');
|
|
255
|
+
expect(formatInterval({ type: 'second', value: 5 })).toBe('every 5 seconds');
|
|
256
|
+
});
|
|
257
|
+
it('should format minute intervals', () => {
|
|
258
|
+
expect(formatInterval({ type: 'minute' })).toBe('every minute');
|
|
259
|
+
expect(formatInterval({ type: 'minute', value: 30 })).toBe('every 30 minutes');
|
|
260
|
+
});
|
|
261
|
+
it('should format hour intervals', () => {
|
|
262
|
+
expect(formatInterval({ type: 'hour' })).toBe('every hour');
|
|
263
|
+
expect(formatInterval({ type: 'hour', value: 4 })).toBe('every 4 hours');
|
|
264
|
+
});
|
|
265
|
+
it('should format day intervals', () => {
|
|
266
|
+
expect(formatInterval({ type: 'day' })).toBe('every day');
|
|
267
|
+
expect(formatInterval({ type: 'day', value: 2 })).toBe('every 2 days');
|
|
268
|
+
});
|
|
269
|
+
it('should format week intervals', () => {
|
|
270
|
+
expect(formatInterval({ type: 'week' })).toBe('every week');
|
|
271
|
+
expect(formatInterval({ type: 'week', value: 2 })).toBe('every 2 weeks');
|
|
272
|
+
});
|
|
273
|
+
it('should format cron intervals', () => {
|
|
274
|
+
expect(formatInterval({ type: 'cron', expression: '0 9 * * 1' })).toBe('cron: 0 9 * * 1');
|
|
275
|
+
});
|
|
276
|
+
it('should format natural intervals', () => {
|
|
277
|
+
expect(formatInterval({ type: 'natural', description: 'every hour during business hours' })).toBe('every hour during business hours');
|
|
278
|
+
});
|
|
279
|
+
it('should use natural description when available', () => {
|
|
280
|
+
expect(formatInterval({ type: 'cron', expression: '0 9 * * 1', natural: 'Monday.at9am' })).toBe('Monday.at9am');
|
|
281
|
+
});
|
|
282
|
+
});
|
package/test/on.test.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { on, registerEventHandler, getEventHandlers, clearEventHandlers } from '../src/on.js';
|
|
3
|
+
describe('on - event handler registration', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
clearEventHandlers();
|
|
6
|
+
});
|
|
7
|
+
describe('on.Noun.event pattern', () => {
|
|
8
|
+
it('should register handler for Customer.created', () => {
|
|
9
|
+
const handler = () => { };
|
|
10
|
+
on.Customer.created(handler);
|
|
11
|
+
const handlers = getEventHandlers();
|
|
12
|
+
expect(handlers).toHaveLength(1);
|
|
13
|
+
expect(handlers[0]?.noun).toBe('Customer');
|
|
14
|
+
expect(handlers[0]?.event).toBe('created');
|
|
15
|
+
expect(handlers[0]?.handler).toBe(handler);
|
|
16
|
+
expect(handlers[0]?.source).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
it('should register handler for Order.completed', () => {
|
|
19
|
+
const handler = () => { };
|
|
20
|
+
on.Order.completed(handler);
|
|
21
|
+
const handlers = getEventHandlers();
|
|
22
|
+
expect(handlers).toHaveLength(1);
|
|
23
|
+
expect(handlers[0]?.noun).toBe('Order');
|
|
24
|
+
expect(handlers[0]?.event).toBe('completed');
|
|
25
|
+
});
|
|
26
|
+
it('should register multiple handlers', () => {
|
|
27
|
+
on.Customer.created(() => { });
|
|
28
|
+
on.Customer.updated(() => { });
|
|
29
|
+
on.Order.completed(() => { });
|
|
30
|
+
const handlers = getEventHandlers();
|
|
31
|
+
expect(handlers).toHaveLength(3);
|
|
32
|
+
});
|
|
33
|
+
it('should register multiple handlers for same event', () => {
|
|
34
|
+
on.Customer.created(() => { });
|
|
35
|
+
on.Customer.created(() => { });
|
|
36
|
+
const handlers = getEventHandlers();
|
|
37
|
+
expect(handlers).toHaveLength(2);
|
|
38
|
+
expect(handlers[0]?.noun).toBe('Customer');
|
|
39
|
+
expect(handlers[0]?.event).toBe('created');
|
|
40
|
+
expect(handlers[1]?.noun).toBe('Customer');
|
|
41
|
+
expect(handlers[1]?.event).toBe('created');
|
|
42
|
+
});
|
|
43
|
+
it('should capture function source code', () => {
|
|
44
|
+
on.Test.event(async (data, $) => {
|
|
45
|
+
$.log('test', data);
|
|
46
|
+
});
|
|
47
|
+
const handlers = getEventHandlers();
|
|
48
|
+
expect(handlers[0]?.source).toContain('$.log');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('registerEventHandler', () => {
|
|
52
|
+
it('should register handler directly', () => {
|
|
53
|
+
const handler = () => { };
|
|
54
|
+
registerEventHandler('Payment', 'failed', handler);
|
|
55
|
+
const handlers = getEventHandlers();
|
|
56
|
+
expect(handlers).toHaveLength(1);
|
|
57
|
+
expect(handlers[0]?.noun).toBe('Payment');
|
|
58
|
+
expect(handlers[0]?.event).toBe('failed');
|
|
59
|
+
expect(handlers[0]?.handler).toBe(handler);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('clearEventHandlers', () => {
|
|
63
|
+
it('should clear all handlers', () => {
|
|
64
|
+
on.Customer.created(() => { });
|
|
65
|
+
on.Order.completed(() => { });
|
|
66
|
+
expect(getEventHandlers()).toHaveLength(2);
|
|
67
|
+
clearEventHandlers();
|
|
68
|
+
expect(getEventHandlers()).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('getEventHandlers', () => {
|
|
72
|
+
it('should return a copy of handlers array', () => {
|
|
73
|
+
on.Customer.created(() => { });
|
|
74
|
+
const handlers1 = getEventHandlers();
|
|
75
|
+
const handlers2 = getEventHandlers();
|
|
76
|
+
expect(handlers1).not.toBe(handlers2);
|
|
77
|
+
expect(handlers1).toEqual(handlers2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { send, parseEvent } from '../src/send.js';
|
|
3
|
+
import { on, clearEventHandlers } from '../src/on.js';
|
|
4
|
+
describe('send - event emission', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
clearEventHandlers();
|
|
7
|
+
});
|
|
8
|
+
describe('parseEvent', () => {
|
|
9
|
+
it('should parse valid event strings', () => {
|
|
10
|
+
expect(parseEvent('Customer.created')).toEqual({
|
|
11
|
+
noun: 'Customer',
|
|
12
|
+
event: 'created',
|
|
13
|
+
});
|
|
14
|
+
expect(parseEvent('Order.completed')).toEqual({
|
|
15
|
+
noun: 'Order',
|
|
16
|
+
event: 'completed',
|
|
17
|
+
});
|
|
18
|
+
expect(parseEvent('Payment.failed')).toEqual({
|
|
19
|
+
noun: 'Payment',
|
|
20
|
+
event: 'failed',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
it('should return null for invalid event strings', () => {
|
|
24
|
+
expect(parseEvent('invalid')).toBeNull();
|
|
25
|
+
expect(parseEvent('too.many.parts')).toBeNull();
|
|
26
|
+
expect(parseEvent('')).toBeNull();
|
|
27
|
+
expect(parseEvent('.')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('send', () => {
|
|
31
|
+
it('should emit event to registered handler', async () => {
|
|
32
|
+
const handler = vi.fn();
|
|
33
|
+
on.Customer.created(handler);
|
|
34
|
+
await send('Customer.created', { id: '123', name: 'John' });
|
|
35
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(handler).toHaveBeenCalledWith({ id: '123', name: 'John' }, expect.objectContaining({
|
|
37
|
+
send: expect.any(Function),
|
|
38
|
+
getState: expect.any(Function),
|
|
39
|
+
set: expect.any(Function),
|
|
40
|
+
get: expect.any(Function),
|
|
41
|
+
log: expect.any(Function),
|
|
42
|
+
}));
|
|
43
|
+
});
|
|
44
|
+
it('should emit event to multiple handlers', async () => {
|
|
45
|
+
const handler1 = vi.fn();
|
|
46
|
+
const handler2 = vi.fn();
|
|
47
|
+
on.Customer.created(handler1);
|
|
48
|
+
on.Customer.created(handler2);
|
|
49
|
+
await send('Customer.created', { id: '123' });
|
|
50
|
+
expect(handler1).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
it('should not throw when no handlers are registered', async () => {
|
|
54
|
+
await expect(send('Customer.created', { id: '123' })).resolves.not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
it('should not call handlers for different events', async () => {
|
|
57
|
+
const handler = vi.fn();
|
|
58
|
+
on.Customer.updated(handler);
|
|
59
|
+
await send('Customer.created', { id: '123' });
|
|
60
|
+
expect(handler).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
it('should not call handlers for different nouns', async () => {
|
|
63
|
+
const handler = vi.fn();
|
|
64
|
+
on.Order.created(handler);
|
|
65
|
+
await send('Customer.created', { id: '123' });
|
|
66
|
+
expect(handler).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('should handle async handlers', async () => {
|
|
69
|
+
let completed = false;
|
|
70
|
+
on.Customer.created(async () => {
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
72
|
+
completed = true;
|
|
73
|
+
});
|
|
74
|
+
await send('Customer.created', { id: '123' });
|
|
75
|
+
expect(completed).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('should continue with other handlers if one throws', async () => {
|
|
78
|
+
const handler1 = vi.fn().mockRejectedValue(new Error('Handler 1 failed'));
|
|
79
|
+
const handler2 = vi.fn();
|
|
80
|
+
on.Customer.created(handler1);
|
|
81
|
+
on.Customer.created(handler2);
|
|
82
|
+
// Should not throw
|
|
83
|
+
await expect(send('Customer.created', { id: '123' })).resolves.not.toThrow();
|
|
84
|
+
// Both handlers should be called
|
|
85
|
+
expect(handler1).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|