ai-workflows 2.1.3 → 2.4.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +14 -1
- package/README.md +2 -0
- package/dist/barrier.d.ts +6 -0
- package/dist/barrier.d.ts.map +1 -1
- package/dist/barrier.js +45 -7
- package/dist/barrier.js.map +1 -1
- package/dist/cascade-context.d.ts.map +1 -1
- package/dist/cascade-context.js +25 -25
- package/dist/cascade-context.js.map +1 -1
- package/dist/cascade-executor.d.ts.map +1 -1
- package/dist/cascade-executor.js +1 -1
- package/dist/cascade-executor.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -7
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- package/dist/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.js.map +1 -0
- package/dist/graph/topological-sort.d.ts.map +1 -1
- package/dist/graph/topological-sort.js +5 -5
- package/dist/graph/topological-sort.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +3 -3
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +25 -0
- package/dist/timer-registry.d.ts.map +1 -1
- package/dist/timer-registry.js +42 -8
- package/dist/timer-registry.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +132 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +30 -13
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +48 -7
- package/src/cascade-context.ts +36 -29
- package/src/cascade-executor.ts +3 -2
- package/src/context.ts +41 -12
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/graph/topological-sort.ts +6 -8
- package/src/index.ts +69 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +8 -9
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +44 -10
- package/src/types.ts +32 -17
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +188 -351
- package/test/barrier-join.test.ts +32 -24
- package/test/cascade-executor.test.ts +9 -16
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +30 -21
- package/test/send-race-conditions.test.ts +30 -40
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -169
- package/LICENSE +0 -21
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- 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
|
+
})
|