ai-workflows 2.1.1 → 2.3.0

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 (211) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -1
  3. package/README.md +305 -184
  4. package/dist/barrier.d.ts +159 -0
  5. package/dist/barrier.d.ts.map +1 -0
  6. package/dist/barrier.js +377 -0
  7. package/dist/barrier.js.map +1 -0
  8. package/dist/cascade-context.d.ts +149 -0
  9. package/dist/cascade-context.d.ts.map +1 -0
  10. package/dist/cascade-context.js +324 -0
  11. package/dist/cascade-context.js.map +1 -0
  12. package/dist/cascade-executor.d.ts +196 -0
  13. package/dist/cascade-executor.d.ts.map +1 -0
  14. package/dist/cascade-executor.js +384 -0
  15. package/dist/cascade-executor.js.map +1 -0
  16. package/dist/context.d.ts.map +1 -1
  17. package/dist/context.js +27 -8
  18. package/dist/context.js.map +1 -1
  19. package/dist/cron-parser.d.ts +65 -0
  20. package/dist/cron-parser.d.ts.map +1 -0
  21. package/dist/cron-parser.js +294 -0
  22. package/dist/cron-parser.js.map +1 -0
  23. package/dist/cron-scheduler.d.ts +117 -0
  24. package/dist/cron-scheduler.d.ts.map +1 -0
  25. package/dist/cron-scheduler.js +176 -0
  26. package/dist/cron-scheduler.js.map +1 -0
  27. package/dist/database-context.d.ts +184 -0
  28. package/dist/database-context.d.ts.map +1 -0
  29. package/dist/database-context.js +428 -0
  30. package/dist/database-context.js.map +1 -0
  31. package/dist/dependency-graph.d.ts +157 -0
  32. package/dist/dependency-graph.d.ts.map +1 -0
  33. package/dist/dependency-graph.js +382 -0
  34. package/dist/dependency-graph.js.map +1 -0
  35. package/dist/digital-objects-adapter.d.ts +159 -0
  36. package/dist/digital-objects-adapter.d.ts.map +1 -0
  37. package/dist/digital-objects-adapter.js +229 -0
  38. package/dist/digital-objects-adapter.js.map +1 -0
  39. package/dist/durable-execution-cloudflare.d.ts +427 -0
  40. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  41. package/dist/durable-execution-cloudflare.js +510 -0
  42. package/dist/durable-execution-cloudflare.js.map +1 -0
  43. package/dist/durable-execution.d.ts +482 -0
  44. package/dist/durable-execution.d.ts.map +1 -0
  45. package/dist/durable-execution.js +594 -0
  46. package/dist/durable-execution.js.map +1 -0
  47. package/dist/durable-workflow.d.ts +176 -0
  48. package/dist/durable-workflow.d.ts.map +1 -0
  49. package/dist/durable-workflow.js +552 -0
  50. package/dist/durable-workflow.js.map +1 -0
  51. package/dist/every.d.ts +31 -2
  52. package/dist/every.d.ts.map +1 -1
  53. package/dist/every.js +63 -32
  54. package/dist/every.js.map +1 -1
  55. package/dist/graph/index.d.ts +8 -0
  56. package/dist/graph/index.d.ts.map +1 -0
  57. package/dist/graph/index.js +8 -0
  58. package/dist/graph/index.js.map +1 -0
  59. package/dist/graph/topological-sort.d.ts +121 -0
  60. package/dist/graph/topological-sort.d.ts.map +1 -0
  61. package/dist/graph/topological-sort.js +292 -0
  62. package/dist/graph/topological-sort.js.map +1 -0
  63. package/dist/index.d.ts +10 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +25 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/logger.d.ts +101 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +115 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/on.d.ts +35 -10
  72. package/dist/on.d.ts.map +1 -1
  73. package/dist/on.js +53 -19
  74. package/dist/on.js.map +1 -1
  75. package/dist/runtime.d.ts +169 -0
  76. package/dist/runtime.d.ts.map +1 -0
  77. package/dist/runtime.js +275 -0
  78. package/dist/runtime.js.map +1 -0
  79. package/dist/send.d.ts.map +1 -1
  80. package/dist/send.js +4 -3
  81. package/dist/send.js.map +1 -1
  82. package/dist/telemetry.d.ts +150 -0
  83. package/dist/telemetry.d.ts.map +1 -0
  84. package/dist/telemetry.js +388 -0
  85. package/dist/telemetry.js.map +1 -0
  86. package/dist/timer-registry.d.ts +77 -0
  87. package/dist/timer-registry.d.ts.map +1 -0
  88. package/dist/timer-registry.js +154 -0
  89. package/dist/timer-registry.js.map +1 -0
  90. package/dist/types.d.ts +105 -6
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/types.js +17 -1
  93. package/dist/types.js.map +1 -1
  94. package/dist/worker/durable-step.d.ts +481 -0
  95. package/dist/worker/durable-step.d.ts.map +1 -0
  96. package/dist/worker/durable-step.js +606 -0
  97. package/dist/worker/durable-step.js.map +1 -0
  98. package/dist/worker/index.d.ts +106 -0
  99. package/dist/worker/index.d.ts.map +1 -0
  100. package/dist/worker/index.js +124 -0
  101. package/dist/worker/index.js.map +1 -0
  102. package/dist/worker/state-adapter.d.ts +230 -0
  103. package/dist/worker/state-adapter.d.ts.map +1 -0
  104. package/dist/worker/state-adapter.js +409 -0
  105. package/dist/worker/state-adapter.js.map +1 -0
  106. package/dist/worker/topological-executor.d.ts +282 -0
  107. package/dist/worker/topological-executor.d.ts.map +1 -0
  108. package/dist/worker/topological-executor.js +396 -0
  109. package/dist/worker/topological-executor.js.map +1 -0
  110. package/dist/worker/workflow-builder.d.ts +286 -0
  111. package/dist/worker/workflow-builder.d.ts.map +1 -0
  112. package/dist/worker/workflow-builder.js +565 -0
  113. package/dist/worker/workflow-builder.js.map +1 -0
  114. package/dist/worker.d.ts +800 -0
  115. package/dist/worker.d.ts.map +1 -0
  116. package/dist/worker.js +2428 -0
  117. package/dist/worker.js.map +1 -0
  118. package/dist/workflow-builder.d.ts +287 -0
  119. package/dist/workflow-builder.d.ts.map +1 -0
  120. package/dist/workflow-builder.js +762 -0
  121. package/dist/workflow-builder.js.map +1 -0
  122. package/dist/workflow.d.ts +14 -30
  123. package/dist/workflow.d.ts.map +1 -1
  124. package/dist/workflow.js +136 -292
  125. package/dist/workflow.js.map +1 -1
  126. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  127. package/examples/02-content-moderation-cascade.ts +454 -0
  128. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  129. package/examples/04-database-persistence.ts +518 -0
  130. package/examples/README.md +173 -0
  131. package/package.json +21 -4
  132. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  133. package/src/__tests__/durable-workflow.test.ts +297 -0
  134. package/src/barrier.ts +507 -0
  135. package/src/cascade-context.ts +495 -0
  136. package/src/cascade-executor.ts +588 -0
  137. package/src/context.ts +51 -17
  138. package/src/cron-parser.ts +347 -0
  139. package/src/cron-scheduler.ts +239 -0
  140. package/src/database-context.ts +658 -0
  141. package/src/dependency-graph.ts +518 -0
  142. package/src/digital-objects-adapter.ts +351 -0
  143. package/src/durable-execution-cloudflare.ts +855 -0
  144. package/src/durable-execution.ts +1042 -0
  145. package/src/durable-workflow.ts +717 -0
  146. package/src/every.ts +104 -35
  147. package/src/graph/index.ts +19 -0
  148. package/src/graph/topological-sort.ts +412 -0
  149. package/src/index.ts +147 -0
  150. package/src/logger.ts +148 -0
  151. package/src/on.ts +81 -26
  152. package/src/runtime.ts +436 -0
  153. package/src/send.ts +4 -5
  154. package/src/telemetry.ts +577 -0
  155. package/src/timer-registry.ts +179 -0
  156. package/src/types.ts +146 -10
  157. package/src/worker/durable-step.ts +976 -0
  158. package/src/worker/index.ts +216 -0
  159. package/src/worker/state-adapter.ts +589 -0
  160. package/src/worker/topological-executor.ts +625 -0
  161. package/src/worker/workflow-builder.ts +871 -0
  162. package/src/worker.ts +2906 -0
  163. package/src/workflow-builder.ts +1068 -0
  164. package/src/workflow.ts +199 -355
  165. package/test/barrier-join.test.ts +442 -0
  166. package/test/barrier-unhandled-rejections.test.ts +359 -0
  167. package/test/cascade-context.test.ts +390 -0
  168. package/test/cascade-executor.test.ts +852 -0
  169. package/test/cron-parser.test.ts +314 -0
  170. package/test/cron-scheduler.test.ts +291 -0
  171. package/test/database-context.test.ts +770 -0
  172. package/test/db-provider-adapter.test.ts +862 -0
  173. package/test/dependency-graph.test.ts +512 -0
  174. package/test/durable-execution-cloudflare.test.ts +606 -0
  175. package/test/durable-execution-in-process.test.ts +286 -0
  176. package/test/durable-execution.test.ts +247 -0
  177. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  178. package/test/graph/topological-sort.test.ts +586 -0
  179. package/test/integration.test.ts +442 -0
  180. package/test/rpc-surface.test.ts +946 -0
  181. package/test/runtime.test.ts +262 -0
  182. package/test/schedule-timer-cleanup.test.ts +353 -0
  183. package/test/send-race-conditions.test.ts +400 -0
  184. package/test/type-safety-every.test.ts +303 -0
  185. package/test/worker/durable-cascade.test.ts +1117 -0
  186. package/test/worker/durable-step.test.ts +723 -0
  187. package/test/worker/topological-executor.test.ts +1240 -0
  188. package/test/worker/workflow-builder.test.ts +1067 -0
  189. package/test/worker.test.ts +608 -0
  190. package/test/workflow-builder.test.ts +1670 -0
  191. package/test/workflow-cron.test.ts +256 -0
  192. package/test/workflow-state-adapter.test.ts +923 -0
  193. package/test/workflow.test.ts +25 -22
  194. package/tsconfig.json +3 -1
  195. package/vitest.config.ts +38 -1
  196. package/vitest.workers.config.ts +44 -0
  197. package/wrangler.jsonc +22 -0
  198. package/.turbo/turbo-test.log +0 -7
  199. package/src/context.js +0 -83
  200. package/src/every.js +0 -267
  201. package/src/index.js +0 -71
  202. package/src/on.js +0 -79
  203. package/src/send.js +0 -111
  204. package/src/types.js +0 -4
  205. package/src/workflow.js +0 -455
  206. package/test/context.test.js +0 -116
  207. package/test/every.test.js +0 -282
  208. package/test/on.test.js +0 -80
  209. package/test/send.test.js +0 -89
  210. package/test/workflow.test.js +0 -224
  211. package/vitest.config.js +0 -7
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Worker Export Tests for ai-workflows
3
+ *
4
+ * Tests for the /worker export.
5
+ * These tests verify the WorkflowService WorkerEntrypoint and WorkflowServiceCore RpcTarget.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
9
+ import { WorkflowService, WorkflowServiceCore } from '../src/worker.js'
10
+ import type { WorkflowInstanceInfo } from '../src/worker.js'
11
+
12
+ describe('WorkflowServiceCore (RpcTarget)', () => {
13
+ let service: WorkflowServiceCore
14
+
15
+ beforeEach(() => {
16
+ service = new WorkflowServiceCore()
17
+ })
18
+
19
+ afterEach(() => {
20
+ // Clean up any created workflows
21
+ service.clear()
22
+ })
23
+
24
+ describe('constructor', () => {
25
+ it('creates a new WorkflowServiceCore instance', () => {
26
+ expect(service).toBeInstanceOf(WorkflowServiceCore)
27
+ })
28
+
29
+ it('has expected class name', () => {
30
+ expect(service.constructor.name).toBe('WorkflowServiceCore')
31
+ })
32
+ })
33
+
34
+ describe('workflow creation', () => {
35
+ describe('create()', () => {
36
+ it('creates a new workflow with generated ID', () => {
37
+ const info = service.create()
38
+
39
+ expect(info.id).toBeDefined()
40
+ expect(info.id).toMatch(/^wf-/)
41
+ expect(info.started).toBe(false)
42
+ })
43
+
44
+ it('creates a workflow with a custom name', () => {
45
+ const info = service.create('my-workflow')
46
+
47
+ expect(info.name).toBe('my-workflow')
48
+ expect(info.id).toBeDefined()
49
+ })
50
+
51
+ it('creates a workflow with initial context', () => {
52
+ const info = service.create('test-workflow', {
53
+ context: { userId: '123', role: 'admin' },
54
+ })
55
+
56
+ expect(info.state.context).toEqual({ userId: '123', role: 'admin' })
57
+ })
58
+
59
+ it('creates multiple independent workflows', () => {
60
+ const wf1 = service.create('workflow-1')
61
+ const wf2 = service.create('workflow-2')
62
+
63
+ expect(wf1.id).not.toBe(wf2.id)
64
+ expect(service.list()).toContain(wf1.id)
65
+ expect(service.list()).toContain(wf2.id)
66
+ })
67
+ })
68
+
69
+ describe('createWithSetup()', () => {
70
+ it('creates a workflow with event handlers', () => {
71
+ const info = service.createWithSetup(($) => {
72
+ $.on.Customer.created(() => {
73
+ // Handler
74
+ })
75
+ })
76
+
77
+ expect(info.id).toBeDefined()
78
+ expect(info.eventCount).toBe(1)
79
+ })
80
+
81
+ it('creates a workflow with schedule handlers', () => {
82
+ const info = service.createWithSetup(($) => {
83
+ $.every.minutes(5)(() => {
84
+ // Handler
85
+ })
86
+ })
87
+
88
+ expect(info.id).toBeDefined()
89
+ expect(info.scheduleCount).toBe(1)
90
+ })
91
+
92
+ it('creates a workflow with both event and schedule handlers', () => {
93
+ const info = service.createWithSetup(($) => {
94
+ $.on.Order.completed(() => {})
95
+ $.on.Payment.received(() => {})
96
+ $.every.hours(1)(() => {})
97
+ })
98
+
99
+ expect(info.eventCount).toBe(2)
100
+ expect(info.scheduleCount).toBe(1)
101
+ })
102
+ })
103
+ })
104
+
105
+ describe('workflow lifecycle', () => {
106
+ let workflowId: string
107
+
108
+ beforeEach(() => {
109
+ const info = service.create('lifecycle-test')
110
+ workflowId = info.id
111
+ })
112
+
113
+ describe('start()', () => {
114
+ it('starts a workflow', async () => {
115
+ await service.start(workflowId)
116
+ const info = service.get(workflowId)
117
+ expect(info?.started).toBe(true)
118
+ })
119
+
120
+ it('is idempotent (can be called multiple times)', async () => {
121
+ await service.start(workflowId)
122
+ await service.start(workflowId) // Should not throw
123
+ const info = service.get(workflowId)
124
+ expect(info?.started).toBe(true)
125
+ })
126
+
127
+ it('throws for non-existent workflow', async () => {
128
+ await expect(service.start('non-existent')).rejects.toThrow('not found')
129
+ })
130
+ })
131
+
132
+ describe('stop()', () => {
133
+ it('stops a running workflow', async () => {
134
+ await service.start(workflowId)
135
+ await service.stop(workflowId)
136
+ const info = service.get(workflowId)
137
+ expect(info?.started).toBe(false)
138
+ })
139
+
140
+ it('throws for non-existent workflow', async () => {
141
+ await expect(service.stop('non-existent')).rejects.toThrow('not found')
142
+ })
143
+ })
144
+
145
+ describe('destroy()', () => {
146
+ it('destroys a workflow and removes from registry', async () => {
147
+ await service.destroy(workflowId)
148
+ expect(service.has(workflowId)).toBe(false)
149
+ expect(service.get(workflowId)).toBeNull()
150
+ })
151
+
152
+ it('throws for non-existent workflow', async () => {
153
+ await expect(service.destroy('non-existent')).rejects.toThrow('not found')
154
+ })
155
+ })
156
+ })
157
+
158
+ describe('event emission', () => {
159
+ let workflowId: string
160
+
161
+ beforeEach(() => {
162
+ const info = service.createWithSetup(($) => {
163
+ $.on.Customer.created((data) => {
164
+ $.set('lastCustomer', data)
165
+ })
166
+ })
167
+ workflowId = info.id
168
+ })
169
+
170
+ describe('emit()', () => {
171
+ it('emits an event to a workflow', () => {
172
+ const eventId = service.emit(workflowId, 'Customer.created', {
173
+ name: 'John',
174
+ email: 'john@example.com',
175
+ })
176
+
177
+ expect(eventId).toBeDefined()
178
+ expect(typeof eventId).toBe('string')
179
+ })
180
+
181
+ it('throws for non-existent workflow', () => {
182
+ expect(() => {
183
+ service.emit('non-existent', 'Test.event', {})
184
+ }).toThrow('not found')
185
+ })
186
+ })
187
+ })
188
+
189
+ describe('state management', () => {
190
+ let workflowId: string
191
+
192
+ beforeEach(() => {
193
+ const info = service.create('state-test', {
194
+ context: { initial: 'value' },
195
+ })
196
+ workflowId = info.id
197
+ })
198
+
199
+ describe('getState()', () => {
200
+ it('returns the workflow state', () => {
201
+ const state = service.getState(workflowId)
202
+
203
+ expect(state.context).toBeDefined()
204
+ expect(state.context.initial).toBe('value')
205
+ expect(state.history).toEqual([])
206
+ })
207
+
208
+ it('throws for non-existent workflow', () => {
209
+ expect(() => service.getState('non-existent')).toThrow('not found')
210
+ })
211
+ })
212
+
213
+ describe('setState()', () => {
214
+ it('sets a value in workflow context', () => {
215
+ service.setState(workflowId, 'newKey', 'newValue')
216
+
217
+ const state = service.getState(workflowId)
218
+ expect(state.context.newKey).toBe('newValue')
219
+ })
220
+
221
+ it('throws for non-existent workflow', () => {
222
+ expect(() => service.setState('non-existent', 'key', 'value')).toThrow('not found')
223
+ })
224
+ })
225
+
226
+ describe('getValue()', () => {
227
+ it('gets a value from workflow context', () => {
228
+ service.setState(workflowId, 'testKey', 'testValue')
229
+ const value = service.getValue(workflowId, 'testKey')
230
+
231
+ expect(value).toBe('testValue')
232
+ })
233
+
234
+ it('returns undefined for non-existent key', () => {
235
+ const value = service.getValue(workflowId, 'nonExistent')
236
+ expect(value).toBeUndefined()
237
+ })
238
+
239
+ it('throws for non-existent workflow', () => {
240
+ expect(() => service.getValue('non-existent', 'key')).toThrow('not found')
241
+ })
242
+ })
243
+ })
244
+
245
+ describe('workflow info', () => {
246
+ describe('get()', () => {
247
+ it('returns workflow info', () => {
248
+ const created = service.create('info-test')
249
+ const info = service.get(created.id)
250
+
251
+ expect(info).not.toBeNull()
252
+ expect(info?.id).toBe(created.id)
253
+ expect(info?.name).toBe('info-test')
254
+ expect(info?.started).toBe(false)
255
+ })
256
+
257
+ it('returns null for non-existent workflow', () => {
258
+ const info = service.get('non-existent')
259
+ expect(info).toBeNull()
260
+ })
261
+ })
262
+
263
+ describe('list()', () => {
264
+ it('returns all workflow IDs', () => {
265
+ const wf1 = service.create('list-test-1')
266
+ const wf2 = service.create('list-test-2')
267
+
268
+ const ids = service.list()
269
+
270
+ expect(ids).toContain(wf1.id)
271
+ expect(ids).toContain(wf2.id)
272
+ })
273
+
274
+ it('returns empty array when no workflows', () => {
275
+ service.clear()
276
+ const ids = service.list()
277
+ expect(ids).toEqual([])
278
+ })
279
+ })
280
+
281
+ describe('has()', () => {
282
+ it('returns true for existing workflow', () => {
283
+ const info = service.create('has-test')
284
+ expect(service.has(info.id)).toBe(true)
285
+ })
286
+
287
+ it('returns false for non-existent workflow', () => {
288
+ expect(service.has('non-existent')).toBe(false)
289
+ })
290
+ })
291
+ })
292
+
293
+ describe('global event handlers', () => {
294
+ afterEach(() => {
295
+ service.clearGlobalEventHandlers()
296
+ })
297
+
298
+ describe('registerGlobalEvent()', () => {
299
+ it('registers a global event handler', () => {
300
+ service.registerGlobalEvent('Test', 'event', () => {})
301
+
302
+ const handlers = service.getGlobalEventHandlers()
303
+ expect(handlers.length).toBeGreaterThan(0)
304
+ expect(handlers.some((h) => h.noun === 'Test' && h.event === 'event')).toBe(true)
305
+ })
306
+ })
307
+
308
+ describe('getGlobalEventHandlers()', () => {
309
+ it('returns all registered global event handlers', () => {
310
+ service.registerGlobalEvent('A', 'event1', () => {})
311
+ service.registerGlobalEvent('B', 'event2', () => {})
312
+
313
+ const handlers = service.getGlobalEventHandlers()
314
+ expect(handlers.length).toBe(2)
315
+ })
316
+ })
317
+
318
+ describe('clearGlobalEventHandlers()', () => {
319
+ it('clears all global event handlers', () => {
320
+ service.registerGlobalEvent('Test', 'event', () => {})
321
+ service.clearGlobalEventHandlers()
322
+
323
+ const handlers = service.getGlobalEventHandlers()
324
+ expect(handlers.length).toBe(0)
325
+ })
326
+ })
327
+ })
328
+
329
+ describe('global schedule handlers', () => {
330
+ afterEach(() => {
331
+ service.clearGlobalScheduleHandlers()
332
+ })
333
+
334
+ describe('registerGlobalSchedule()', () => {
335
+ it('registers a global schedule handler', () => {
336
+ service.registerGlobalSchedule({ type: 'hour' }, () => {})
337
+
338
+ const handlers = service.getGlobalScheduleHandlers()
339
+ expect(handlers.length).toBe(1)
340
+ })
341
+ })
342
+
343
+ describe('getGlobalScheduleHandlers()', () => {
344
+ it('returns all registered global schedule handlers', () => {
345
+ service.registerGlobalSchedule({ type: 'minute' }, () => {})
346
+ service.registerGlobalSchedule({ type: 'hour' }, () => {})
347
+
348
+ const handlers = service.getGlobalScheduleHandlers()
349
+ expect(handlers.length).toBe(2)
350
+ })
351
+ })
352
+
353
+ describe('clearGlobalScheduleHandlers()', () => {
354
+ it('clears all global schedule handlers', () => {
355
+ service.registerGlobalSchedule({ type: 'hour' }, () => {})
356
+ service.clearGlobalScheduleHandlers()
357
+
358
+ const handlers = service.getGlobalScheduleHandlers()
359
+ expect(handlers.length).toBe(0)
360
+ })
361
+ })
362
+ })
363
+
364
+ describe('utilities', () => {
365
+ describe('parseEvent()', () => {
366
+ it('parses valid event string', () => {
367
+ const parsed = service.parseEvent('Customer.created')
368
+
369
+ expect(parsed).toEqual({ noun: 'Customer', event: 'created' })
370
+ })
371
+
372
+ it('returns null for invalid event string', () => {
373
+ expect(service.parseEvent('invalid')).toBeNull()
374
+ expect(service.parseEvent('too.many.parts')).toBeNull()
375
+ expect(service.parseEvent('')).toBeNull()
376
+ })
377
+ })
378
+
379
+ describe('toCron()', () => {
380
+ it('converts known pattern to cron', async () => {
381
+ const cron = await service.toCron('hour')
382
+ expect(cron).toBeDefined()
383
+ expect(cron).toBe('0 * * * *')
384
+ })
385
+
386
+ it('converts day pattern to cron', async () => {
387
+ const cron = await service.toCron('Monday')
388
+ expect(cron).toBeDefined()
389
+ expect(cron).toBe('0 0 * * 1')
390
+ })
391
+ })
392
+
393
+ describe('intervalToMs()', () => {
394
+ it('converts second interval to milliseconds', () => {
395
+ const ms = service.intervalToMs({ type: 'second', value: 10 })
396
+ expect(ms).toBe(10000)
397
+ })
398
+
399
+ it('converts minute interval to milliseconds', () => {
400
+ const ms = service.intervalToMs({ type: 'minute', value: 5 })
401
+ expect(ms).toBe(300000)
402
+ })
403
+
404
+ it('converts hour interval to milliseconds', () => {
405
+ const ms = service.intervalToMs({ type: 'hour', value: 2 })
406
+ expect(ms).toBe(7200000)
407
+ })
408
+ })
409
+
410
+ describe('formatInterval()', () => {
411
+ it('formats hour interval', () => {
412
+ const formatted = service.formatInterval({ type: 'hour' })
413
+ expect(typeof formatted).toBe('string')
414
+ })
415
+
416
+ it('formats minute interval with value', () => {
417
+ const formatted = service.formatInterval({ type: 'minute', value: 30 })
418
+ expect(typeof formatted).toBe('string')
419
+ })
420
+ })
421
+
422
+ describe('createTestContext()', () => {
423
+ it('creates a test context with emittedEvents tracking', () => {
424
+ const ctx = service.createTestContext()
425
+
426
+ expect(ctx.emittedEvents).toBeDefined()
427
+ expect(Array.isArray(ctx.emittedEvents)).toBe(true)
428
+ expect(typeof ctx.send).toBe('function')
429
+ expect(typeof ctx.log).toBe('function')
430
+ })
431
+
432
+ it('test context tracks sent events', () => {
433
+ const ctx = service.createTestContext()
434
+ ctx.send('Test.event', { value: 42 })
435
+
436
+ expect(ctx.emittedEvents.length).toBe(1)
437
+ expect(ctx.emittedEvents[0].event).toBe('Test.event')
438
+ })
439
+ })
440
+
441
+ describe('clear()', () => {
442
+ it('clears all workflows', () => {
443
+ service.create('test-1')
444
+ service.create('test-2')
445
+
446
+ service.clear()
447
+
448
+ expect(service.list()).toEqual([])
449
+ })
450
+ })
451
+ })
452
+ })
453
+
454
+ describe('WorkflowService (WorkerEntrypoint)', () => {
455
+ describe('class definition', () => {
456
+ it('exports WorkflowService class', () => {
457
+ expect(WorkflowService).toBeDefined()
458
+ expect(typeof WorkflowService).toBe('function')
459
+ })
460
+
461
+ it('WorkflowService has connect method in prototype', () => {
462
+ expect(typeof WorkflowService.prototype.connect).toBe('function')
463
+ })
464
+
465
+ it('has expected class name', () => {
466
+ expect(WorkflowService.name).toBe('WorkflowService')
467
+ })
468
+ })
469
+
470
+ describe('connect()', () => {
471
+ // Note: WorkerEntrypoint classes cannot be instantiated directly in tests.
472
+ // They require the Cloudflare Workers runtime context.
473
+ // We verify the connect method behavior by testing that:
474
+ // 1. The method exists on the prototype
475
+ // 2. The return type (WorkflowServiceCore) is properly constructable and functional
476
+
477
+ it('returns a WorkflowServiceCore instance', () => {
478
+ // Since we can't instantiate WorkflowService directly (requires Workers runtime),
479
+ // we verify that WorkflowServiceCore (the return type of connect()) works correctly
480
+ const core = new WorkflowServiceCore()
481
+ expect(core).toBeInstanceOf(WorkflowServiceCore)
482
+ })
483
+
484
+ it('returns service with all required methods', () => {
485
+ const core = new WorkflowServiceCore()
486
+
487
+ // Workflow creation
488
+ expect(typeof core.create).toBe('function')
489
+ expect(typeof core.createWithSetup).toBe('function')
490
+
491
+ // Lifecycle
492
+ expect(typeof core.start).toBe('function')
493
+ expect(typeof core.stop).toBe('function')
494
+ expect(typeof core.destroy).toBe('function')
495
+
496
+ // Events
497
+ expect(typeof core.emit).toBe('function')
498
+ expect(typeof core.sendGlobal).toBe('function')
499
+
500
+ // State
501
+ expect(typeof core.getState).toBe('function')
502
+ expect(typeof core.setState).toBe('function')
503
+ expect(typeof core.getValue).toBe('function')
504
+
505
+ // Info
506
+ expect(typeof core.get).toBe('function')
507
+ expect(typeof core.list).toBe('function')
508
+ expect(typeof core.has).toBe('function')
509
+
510
+ // Global handlers
511
+ expect(typeof core.registerGlobalEvent).toBe('function')
512
+ expect(typeof core.registerGlobalSchedule).toBe('function')
513
+
514
+ // Utilities
515
+ expect(typeof core.parseEvent).toBe('function')
516
+ expect(typeof core.toCron).toBe('function')
517
+ expect(typeof core.intervalToMs).toBe('function')
518
+ expect(typeof core.formatInterval).toBe('function')
519
+ expect(typeof core.createTestContext).toBe('function')
520
+ expect(typeof core.clear).toBe('function')
521
+ })
522
+
523
+ it('creates independent service instances', () => {
524
+ const core1 = new WorkflowServiceCore()
525
+ const core2 = new WorkflowServiceCore()
526
+
527
+ // Create workflow in core1
528
+ const wf = core1.create('independent-test')
529
+
530
+ // Both instances share the global registry, so wf should be visible in both
531
+ // This is intentional - workflows are shared state
532
+ expect(core1.has(wf.id)).toBe(true)
533
+ expect(core2.has(wf.id)).toBe(true)
534
+
535
+ // Clean up
536
+ core1.clear()
537
+ })
538
+ })
539
+ })
540
+
541
+ describe('Integration: Workflow Execution', () => {
542
+ let service: WorkflowServiceCore
543
+
544
+ beforeEach(() => {
545
+ service = new WorkflowServiceCore()
546
+ })
547
+
548
+ afterEach(() => {
549
+ service.clear()
550
+ service.clearGlobalEventHandlers()
551
+ service.clearGlobalScheduleHandlers()
552
+ })
553
+
554
+ it('creates and starts a workflow', async () => {
555
+ const info = service.create('integration-test')
556
+
557
+ await service.start(info.id)
558
+
559
+ const updated = service.get(info.id)
560
+ expect(updated?.started).toBe(true)
561
+ })
562
+
563
+ it('manages workflow state through lifecycle', async () => {
564
+ const info = service.create('state-lifecycle', {
565
+ context: { counter: 0 },
566
+ })
567
+
568
+ // Initial state
569
+ expect(service.getValue(info.id, 'counter')).toBe(0)
570
+
571
+ // Update state
572
+ service.setState(info.id, 'counter', 1)
573
+ expect(service.getValue(info.id, 'counter')).toBe(1)
574
+
575
+ // State persists across start/stop
576
+ await service.start(info.id)
577
+ expect(service.getValue(info.id, 'counter')).toBe(1)
578
+
579
+ await service.stop(info.id)
580
+ expect(service.getValue(info.id, 'counter')).toBe(1)
581
+ })
582
+
583
+ it('creates workflow with event handlers and emits events', () => {
584
+ const received: unknown[] = []
585
+
586
+ const info = service.createWithSetup(($) => {
587
+ $.on.Test.event((data) => {
588
+ received.push(data)
589
+ })
590
+ })
591
+
592
+ // Emit event
593
+ const eventId = service.emit(info.id, 'Test.event', { value: 42 })
594
+
595
+ expect(eventId).toBeDefined()
596
+ // Note: Event delivery is async, so we verify the eventId was returned
597
+ })
598
+
599
+ it('destroys workflow and cleans up', async () => {
600
+ const info = service.create('destroy-test')
601
+ await service.start(info.id)
602
+
603
+ await service.destroy(info.id)
604
+
605
+ expect(service.has(info.id)).toBe(false)
606
+ expect(service.get(info.id)).toBeNull()
607
+ })
608
+ })