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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -1
- package/README.md +305 -184
- package/dist/barrier.d.ts +159 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +377 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -8
- 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/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.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/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -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 +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +53 -19
- 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 +77 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +154 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +105 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -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 +136 -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 +21 -4
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +507 -0
- package/src/cascade-context.ts +495 -0
- package/src/cascade-executor.ts +588 -0
- package/src/context.ts +51 -17
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/dependency-graph.ts +518 -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/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +412 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +81 -26
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +179 -0
- package/src/types.ts +146 -10
- 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 +199 -355
- package/test/barrier-join.test.ts +442 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +852 -0
- 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/dependency-graph.test.ts +512 -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/graph/topological-sort.test.ts +586 -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 +353 -0
- package/test/send-race-conditions.test.ts +400 -0
- package/test/type-safety-every.test.ts +303 -0
- 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 -7
- 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,946 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Surface Tests (RED Phase)
|
|
3
|
+
*
|
|
4
|
+
* Failing tests that define the expected interface for the full RPC surface
|
|
5
|
+
* of WorkflowService and WorkflowServiceCore.
|
|
6
|
+
*
|
|
7
|
+
* These tests are written in the RED phase of TDD - they define the expected
|
|
8
|
+
* behavior before implementation. They should fail initially.
|
|
9
|
+
*
|
|
10
|
+
* ## RPC Surface Categories
|
|
11
|
+
* 1. Workflow Creation and Registration
|
|
12
|
+
* 2. Workflow Lifecycle Management
|
|
13
|
+
* 3. Event Emission and Handling
|
|
14
|
+
* 4. State Management (in-memory and persisted)
|
|
15
|
+
* 5. Query and List Operations
|
|
16
|
+
* 6. Batch Operations
|
|
17
|
+
* 7. Workflow Introspection
|
|
18
|
+
* 8. Error Handling and Recovery
|
|
19
|
+
* 9. Metrics and Observability
|
|
20
|
+
* 10. Serialization/Deserialization for RPC
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
24
|
+
import { WorkflowService, WorkflowServiceCore } from '../src/worker.js'
|
|
25
|
+
import type { WorkflowInstanceInfo } from '../src/worker.js'
|
|
26
|
+
|
|
27
|
+
describe('RPC Surface: WorkflowServiceCore', () => {
|
|
28
|
+
let service: WorkflowServiceCore
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
service = new WorkflowServiceCore()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
service.clear()
|
|
36
|
+
service.clearGlobalEventHandlers()
|
|
37
|
+
service.clearGlobalScheduleHandlers()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ===========================================================================
|
|
41
|
+
// 1. Workflow Creation and Registration
|
|
42
|
+
// ===========================================================================
|
|
43
|
+
describe('Workflow Creation RPC Methods', () => {
|
|
44
|
+
describe('createFromDefinition()', () => {
|
|
45
|
+
it('creates a workflow from a serializable definition object', () => {
|
|
46
|
+
const definition = {
|
|
47
|
+
name: 'test-workflow',
|
|
48
|
+
events: [
|
|
49
|
+
{ noun: 'Customer', event: 'created' },
|
|
50
|
+
{ noun: 'Order', event: 'placed' },
|
|
51
|
+
],
|
|
52
|
+
schedules: [{ type: 'hour' as const }],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const info = service.createFromDefinition(definition)
|
|
56
|
+
|
|
57
|
+
expect(info.id).toBeDefined()
|
|
58
|
+
expect(info.name).toBe('test-workflow')
|
|
59
|
+
expect(info.eventCount).toBe(2)
|
|
60
|
+
expect(info.scheduleCount).toBe(1)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('validates definition schema before creation', () => {
|
|
64
|
+
const invalidDefinition = {
|
|
65
|
+
name: '', // Invalid: empty name
|
|
66
|
+
events: 'not-an-array', // Invalid: should be array
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(() => {
|
|
70
|
+
service.createFromDefinition(invalidDefinition as unknown)
|
|
71
|
+
}).toThrow(/invalid.*definition/i)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('clone()', () => {
|
|
76
|
+
it('clones an existing workflow with a new ID', () => {
|
|
77
|
+
const original = service.create('original-workflow', {
|
|
78
|
+
context: { key: 'value' },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const cloned = service.clone(original.id, 'cloned-workflow')
|
|
82
|
+
|
|
83
|
+
expect(cloned.id).not.toBe(original.id)
|
|
84
|
+
expect(cloned.name).toBe('cloned-workflow')
|
|
85
|
+
expect(cloned.state.context).toEqual({ key: 'value' })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('throws for non-existent workflow', () => {
|
|
89
|
+
expect(() => {
|
|
90
|
+
service.clone('non-existent', 'new-name')
|
|
91
|
+
}).toThrow('not found')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('import()', () => {
|
|
96
|
+
it('imports a workflow from a serialized state snapshot', async () => {
|
|
97
|
+
// Create and export a workflow
|
|
98
|
+
const original = service.create('export-test', {
|
|
99
|
+
context: { data: 'important' },
|
|
100
|
+
})
|
|
101
|
+
service.setState(original.id, 'progress', 50)
|
|
102
|
+
const exported = await service.export(original.id)
|
|
103
|
+
|
|
104
|
+
// Clear and import
|
|
105
|
+
service.clear()
|
|
106
|
+
const imported = await service.import(exported)
|
|
107
|
+
|
|
108
|
+
expect(imported.name).toBe('export-test')
|
|
109
|
+
expect(service.getValue(imported.id, 'progress')).toBe(50)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('export()', () => {
|
|
114
|
+
it('exports a workflow to a serializable format', async () => {
|
|
115
|
+
const info = service.create('export-test', {
|
|
116
|
+
context: { userId: '123' },
|
|
117
|
+
})
|
|
118
|
+
service.setState(info.id, 'step', 'processing')
|
|
119
|
+
|
|
120
|
+
const exported = await service.export(info.id)
|
|
121
|
+
|
|
122
|
+
expect(exported).toMatchObject({
|
|
123
|
+
name: 'export-test',
|
|
124
|
+
state: expect.objectContaining({
|
|
125
|
+
context: expect.objectContaining({ userId: '123', step: 'processing' }),
|
|
126
|
+
}),
|
|
127
|
+
version: expect.any(Number),
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('throws for non-existent workflow', async () => {
|
|
132
|
+
await expect(service.export('non-existent')).rejects.toThrow('not found')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ===========================================================================
|
|
138
|
+
// 2. Workflow Lifecycle Management
|
|
139
|
+
// ===========================================================================
|
|
140
|
+
describe('Lifecycle RPC Methods', () => {
|
|
141
|
+
describe('pause()', () => {
|
|
142
|
+
it('pauses a running workflow', async () => {
|
|
143
|
+
const info = service.create('pause-test')
|
|
144
|
+
await service.start(info.id)
|
|
145
|
+
|
|
146
|
+
await service.pause(info.id)
|
|
147
|
+
|
|
148
|
+
const updated = service.get(info.id)
|
|
149
|
+
expect(updated?.started).toBe(false)
|
|
150
|
+
// Paused workflows should retain their state
|
|
151
|
+
expect(service.getState(info.id).context).toBeDefined()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('records pause in workflow history', async () => {
|
|
155
|
+
const info = service.create('pause-history-test')
|
|
156
|
+
await service.start(info.id)
|
|
157
|
+
await service.pause(info.id)
|
|
158
|
+
|
|
159
|
+
const state = service.getState(info.id)
|
|
160
|
+
expect(state.history).toContainEqual(
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
type: 'lifecycle',
|
|
163
|
+
action: 'paused',
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('resume()', () => {
|
|
170
|
+
it('resumes a paused workflow', async () => {
|
|
171
|
+
const info = service.create('resume-test')
|
|
172
|
+
await service.start(info.id)
|
|
173
|
+
await service.pause(info.id)
|
|
174
|
+
|
|
175
|
+
await service.resume(info.id)
|
|
176
|
+
|
|
177
|
+
const updated = service.get(info.id)
|
|
178
|
+
expect(updated?.started).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('restart()', () => {
|
|
183
|
+
it('restarts a workflow from the beginning', async () => {
|
|
184
|
+
const info = service.create('restart-test', {
|
|
185
|
+
context: { counter: 0 },
|
|
186
|
+
})
|
|
187
|
+
await service.start(info.id)
|
|
188
|
+
service.setState(info.id, 'counter', 10)
|
|
189
|
+
|
|
190
|
+
await service.restart(info.id)
|
|
191
|
+
|
|
192
|
+
// After restart, state should be reset but workflow should be running
|
|
193
|
+
const state = service.getState(info.id)
|
|
194
|
+
expect(state.context.counter).toBe(0)
|
|
195
|
+
expect(service.get(info.id)?.started).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('getStatus()', () => {
|
|
200
|
+
it('returns detailed workflow status', () => {
|
|
201
|
+
const info = service.create('status-test')
|
|
202
|
+
|
|
203
|
+
const status = service.getStatus(info.id)
|
|
204
|
+
|
|
205
|
+
expect(status).toMatchObject({
|
|
206
|
+
id: info.id,
|
|
207
|
+
name: 'status-test',
|
|
208
|
+
started: false,
|
|
209
|
+
paused: false,
|
|
210
|
+
eventsPending: 0,
|
|
211
|
+
lastActivity: expect.any(Date),
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// ===========================================================================
|
|
218
|
+
// 3. Event Emission and Handling
|
|
219
|
+
// ===========================================================================
|
|
220
|
+
describe('Event RPC Methods', () => {
|
|
221
|
+
describe('emitBatch()', () => {
|
|
222
|
+
it('emits multiple events in a single RPC call', () => {
|
|
223
|
+
const info = service.createWithSetup(($) => {
|
|
224
|
+
$.on.Customer.created(() => {})
|
|
225
|
+
$.on.Order.placed(() => {})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const eventIds = service.emitBatch(info.id, [
|
|
229
|
+
{ event: 'Customer.created', data: { id: '1' } },
|
|
230
|
+
{ event: 'Order.placed', data: { id: '2' } },
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
expect(eventIds).toHaveLength(2)
|
|
234
|
+
expect(eventIds.every((id) => typeof id === 'string')).toBe(true)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('is atomic - all events succeed or none', () => {
|
|
238
|
+
const info = service.create('batch-atomic-test')
|
|
239
|
+
|
|
240
|
+
// Emitting to a workflow without handlers should still return event IDs
|
|
241
|
+
// But attempting to batch emit to non-existent workflow should fail all
|
|
242
|
+
expect(() => {
|
|
243
|
+
service.emitBatch('non-existent', [
|
|
244
|
+
{ event: 'Test.event1', data: {} },
|
|
245
|
+
{ event: 'Test.event2', data: {} },
|
|
246
|
+
])
|
|
247
|
+
}).toThrow('not found')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('emitWithDelay()', () => {
|
|
252
|
+
it('schedules an event for future emission', async () => {
|
|
253
|
+
const info = service.createWithSetup(($) => {
|
|
254
|
+
$.on.Test.delayed(() => {})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const scheduledId = await service.emitWithDelay(
|
|
258
|
+
info.id,
|
|
259
|
+
'Test.delayed',
|
|
260
|
+
{ value: 42 },
|
|
261
|
+
1000 // 1 second delay
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
expect(scheduledId).toBeDefined()
|
|
265
|
+
// The event should not have been processed yet
|
|
266
|
+
const pending = service.getPendingEvents(info.id)
|
|
267
|
+
expect(pending).toContainEqual(
|
|
268
|
+
expect.objectContaining({
|
|
269
|
+
event: 'Test.delayed',
|
|
270
|
+
scheduledFor: expect.any(Date),
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('supports cancellation of scheduled events', async () => {
|
|
276
|
+
const info = service.create('cancel-test')
|
|
277
|
+
const scheduledId = await service.emitWithDelay(info.id, 'Test.event', {}, 10000)
|
|
278
|
+
|
|
279
|
+
const cancelled = await service.cancelScheduledEvent(info.id, scheduledId)
|
|
280
|
+
|
|
281
|
+
expect(cancelled).toBe(true)
|
|
282
|
+
const pending = service.getPendingEvents(info.id)
|
|
283
|
+
expect(pending.find((e) => e.id === scheduledId)).toBeUndefined()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('subscribeToEvents()', () => {
|
|
288
|
+
it('returns event stream for a workflow', async () => {
|
|
289
|
+
const info = service.create('subscribe-test')
|
|
290
|
+
const events: Array<{ event: string; data: unknown }> = []
|
|
291
|
+
|
|
292
|
+
const subscription = service.subscribeToEvents(info.id, (event, data) => {
|
|
293
|
+
events.push({ event, data })
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
service.emit(info.id, 'Test.event', { value: 1 })
|
|
297
|
+
service.emit(info.id, 'Test.event', { value: 2 })
|
|
298
|
+
|
|
299
|
+
// Wait for events to be processed
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
301
|
+
|
|
302
|
+
expect(events).toHaveLength(2)
|
|
303
|
+
subscription.unsubscribe()
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('getEventHistory()', () => {
|
|
308
|
+
it('returns history of emitted events', () => {
|
|
309
|
+
const info = service.create('history-test')
|
|
310
|
+
service.emit(info.id, 'Test.event1', { a: 1 })
|
|
311
|
+
service.emit(info.id, 'Test.event2', { b: 2 })
|
|
312
|
+
|
|
313
|
+
const history = service.getEventHistory(info.id)
|
|
314
|
+
|
|
315
|
+
expect(history).toHaveLength(2)
|
|
316
|
+
expect(history[0]).toMatchObject({
|
|
317
|
+
event: 'Test.event1',
|
|
318
|
+
data: { a: 1 },
|
|
319
|
+
timestamp: expect.any(Date),
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('supports pagination', () => {
|
|
324
|
+
const info = service.create('paginated-history-test')
|
|
325
|
+
for (let i = 0; i < 10; i++) {
|
|
326
|
+
service.emit(info.id, 'Test.event', { i })
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const page1 = service.getEventHistory(info.id, { limit: 5, offset: 0 })
|
|
330
|
+
const page2 = service.getEventHistory(info.id, { limit: 5, offset: 5 })
|
|
331
|
+
|
|
332
|
+
expect(page1).toHaveLength(5)
|
|
333
|
+
expect(page2).toHaveLength(5)
|
|
334
|
+
expect(page1[0].data).not.toEqual(page2[0].data)
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// ===========================================================================
|
|
340
|
+
// 4. State Management
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
describe('State RPC Methods', () => {
|
|
343
|
+
describe('getStateSnapshot()', () => {
|
|
344
|
+
it('returns immutable snapshot of current state', () => {
|
|
345
|
+
const info = service.create('snapshot-test', {
|
|
346
|
+
context: { mutable: 'value' },
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const snapshot = service.getStateSnapshot(info.id)
|
|
350
|
+
|
|
351
|
+
// Modifying snapshot should not affect actual state
|
|
352
|
+
snapshot.context.mutable = 'changed'
|
|
353
|
+
expect(service.getValue(info.id, 'mutable')).toBe('value')
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('mergeState()', () => {
|
|
358
|
+
it('deep merges state updates', () => {
|
|
359
|
+
const info = service.create('merge-test', {
|
|
360
|
+
context: {
|
|
361
|
+
user: { name: 'John', age: 30 },
|
|
362
|
+
settings: { theme: 'dark' },
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
service.mergeState(info.id, {
|
|
367
|
+
user: { age: 31, email: 'john@example.com' },
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const state = service.getState(info.id)
|
|
371
|
+
expect(state.context).toEqual({
|
|
372
|
+
user: { name: 'John', age: 31, email: 'john@example.com' },
|
|
373
|
+
settings: { theme: 'dark' },
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
describe('deleteValue()', () => {
|
|
379
|
+
it('removes a key from workflow context', () => {
|
|
380
|
+
const info = service.create('delete-test', {
|
|
381
|
+
context: { keep: 'this', remove: 'this' },
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
service.deleteValue(info.id, 'remove')
|
|
385
|
+
|
|
386
|
+
const state = service.getState(info.id)
|
|
387
|
+
expect(state.context.keep).toBe('this')
|
|
388
|
+
expect(state.context.remove).toBeUndefined()
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe('hasValue()', () => {
|
|
393
|
+
it('checks if a key exists in context', () => {
|
|
394
|
+
const info = service.create('has-test', {
|
|
395
|
+
context: { exists: true },
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
expect(service.hasValue(info.id, 'exists')).toBe(true)
|
|
399
|
+
expect(service.hasValue(info.id, 'missing')).toBe(false)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
describe('getValues()', () => {
|
|
404
|
+
it('returns multiple values in a single call', () => {
|
|
405
|
+
const info = service.create('multi-get-test', {
|
|
406
|
+
context: { a: 1, b: 2, c: 3 },
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const values = service.getValues(info.id, ['a', 'c', 'missing'])
|
|
410
|
+
|
|
411
|
+
expect(values).toEqual({ a: 1, c: 3, missing: undefined })
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('setValues()', () => {
|
|
416
|
+
it('sets multiple values in a single call', () => {
|
|
417
|
+
const info = service.create('multi-set-test')
|
|
418
|
+
|
|
419
|
+
service.setValues(info.id, { x: 10, y: 20, z: 30 })
|
|
420
|
+
|
|
421
|
+
expect(service.getValue(info.id, 'x')).toBe(10)
|
|
422
|
+
expect(service.getValue(info.id, 'y')).toBe(20)
|
|
423
|
+
expect(service.getValue(info.id, 'z')).toBe(30)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// ===========================================================================
|
|
429
|
+
// 5. Query and List Operations
|
|
430
|
+
// ===========================================================================
|
|
431
|
+
describe('Query RPC Methods', () => {
|
|
432
|
+
describe('listByName()', () => {
|
|
433
|
+
it('lists workflows by name pattern', () => {
|
|
434
|
+
service.create('order-workflow-1')
|
|
435
|
+
service.create('order-workflow-2')
|
|
436
|
+
service.create('customer-workflow-1')
|
|
437
|
+
|
|
438
|
+
const orderWorkflows = service.listByName('order-*')
|
|
439
|
+
|
|
440
|
+
expect(orderWorkflows).toHaveLength(2)
|
|
441
|
+
expect(orderWorkflows.every((id) => id.includes('order'))).toBe(false) // IDs don't contain name
|
|
442
|
+
// Verify by getting the info
|
|
443
|
+
for (const id of orderWorkflows) {
|
|
444
|
+
const info = service.get(id)
|
|
445
|
+
expect(info?.name).toMatch(/^order-workflow/)
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
describe('listByStatus()', () => {
|
|
451
|
+
it('lists workflows by running status', async () => {
|
|
452
|
+
const wf1 = service.create('running-1')
|
|
453
|
+
const wf2 = service.create('running-2')
|
|
454
|
+
const wf3 = service.create('stopped-1')
|
|
455
|
+
|
|
456
|
+
await service.start(wf1.id)
|
|
457
|
+
await service.start(wf2.id)
|
|
458
|
+
// wf3 not started
|
|
459
|
+
|
|
460
|
+
const running = service.listByStatus('running')
|
|
461
|
+
const stopped = service.listByStatus('stopped')
|
|
462
|
+
|
|
463
|
+
expect(running).toHaveLength(2)
|
|
464
|
+
expect(stopped).toHaveLength(1)
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
describe('count()', () => {
|
|
469
|
+
it('returns total count of workflows', () => {
|
|
470
|
+
service.create('count-1')
|
|
471
|
+
service.create('count-2')
|
|
472
|
+
service.create('count-3')
|
|
473
|
+
|
|
474
|
+
expect(service.count()).toBe(3)
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
describe('find()', () => {
|
|
479
|
+
it('finds workflows matching a predicate', () => {
|
|
480
|
+
service.create('findable-1', { context: { type: 'order' } })
|
|
481
|
+
service.create('findable-2', { context: { type: 'customer' } })
|
|
482
|
+
service.create('findable-3', { context: { type: 'order' } })
|
|
483
|
+
|
|
484
|
+
const orderWorkflows = service.find((info) => info.state.context.type === 'order')
|
|
485
|
+
|
|
486
|
+
expect(orderWorkflows).toHaveLength(2)
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
describe('getAll()', () => {
|
|
491
|
+
it('returns all workflow info objects', () => {
|
|
492
|
+
service.create('all-1')
|
|
493
|
+
service.create('all-2')
|
|
494
|
+
|
|
495
|
+
const all = service.getAll()
|
|
496
|
+
|
|
497
|
+
expect(all).toHaveLength(2)
|
|
498
|
+
expect(all[0]).toMatchObject({
|
|
499
|
+
id: expect.any(String),
|
|
500
|
+
name: expect.any(String),
|
|
501
|
+
started: false,
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
// ===========================================================================
|
|
508
|
+
// 6. Batch Operations
|
|
509
|
+
// ===========================================================================
|
|
510
|
+
describe('Batch RPC Methods', () => {
|
|
511
|
+
describe('startBatch()', () => {
|
|
512
|
+
it('starts multiple workflows in a single call', async () => {
|
|
513
|
+
const wf1 = service.create('batch-start-1')
|
|
514
|
+
const wf2 = service.create('batch-start-2')
|
|
515
|
+
const wf3 = service.create('batch-start-3')
|
|
516
|
+
|
|
517
|
+
const results = await service.startBatch([wf1.id, wf2.id, wf3.id])
|
|
518
|
+
|
|
519
|
+
expect(results.successful).toHaveLength(3)
|
|
520
|
+
expect(results.failed).toHaveLength(0)
|
|
521
|
+
expect(service.get(wf1.id)?.started).toBe(true)
|
|
522
|
+
expect(service.get(wf2.id)?.started).toBe(true)
|
|
523
|
+
expect(service.get(wf3.id)?.started).toBe(true)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('reports partial failures', async () => {
|
|
527
|
+
const wf1 = service.create('batch-partial-1')
|
|
528
|
+
|
|
529
|
+
const results = await service.startBatch([wf1.id, 'non-existent'])
|
|
530
|
+
|
|
531
|
+
expect(results.successful).toContain(wf1.id)
|
|
532
|
+
expect(results.failed).toContainEqual({
|
|
533
|
+
id: 'non-existent',
|
|
534
|
+
error: expect.stringMatching(/not found/i),
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
describe('stopBatch()', () => {
|
|
540
|
+
it('stops multiple workflows in a single call', async () => {
|
|
541
|
+
const wf1 = service.create('batch-stop-1')
|
|
542
|
+
const wf2 = service.create('batch-stop-2')
|
|
543
|
+
await service.start(wf1.id)
|
|
544
|
+
await service.start(wf2.id)
|
|
545
|
+
|
|
546
|
+
const results = await service.stopBatch([wf1.id, wf2.id])
|
|
547
|
+
|
|
548
|
+
expect(results.successful).toHaveLength(2)
|
|
549
|
+
expect(service.get(wf1.id)?.started).toBe(false)
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
describe('destroyBatch()', () => {
|
|
554
|
+
it('destroys multiple workflows in a single call', async () => {
|
|
555
|
+
const wf1 = service.create('batch-destroy-1')
|
|
556
|
+
const wf2 = service.create('batch-destroy-2')
|
|
557
|
+
|
|
558
|
+
const results = await service.destroyBatch([wf1.id, wf2.id])
|
|
559
|
+
|
|
560
|
+
expect(results.successful).toHaveLength(2)
|
|
561
|
+
expect(service.has(wf1.id)).toBe(false)
|
|
562
|
+
expect(service.has(wf2.id)).toBe(false)
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
describe('emitToAll()', () => {
|
|
567
|
+
it('emits an event to all workflows', () => {
|
|
568
|
+
service.createWithSetup(($) => {
|
|
569
|
+
$.on.Global.broadcast(() => {})
|
|
570
|
+
})
|
|
571
|
+
service.createWithSetup(($) => {
|
|
572
|
+
$.on.Global.broadcast(() => {})
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
const results = service.emitToAll('Global.broadcast', { message: 'hello' })
|
|
576
|
+
|
|
577
|
+
expect(results.eventIds).toHaveLength(2)
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
// ===========================================================================
|
|
583
|
+
// 7. Workflow Introspection
|
|
584
|
+
// ===========================================================================
|
|
585
|
+
describe('Introspection RPC Methods', () => {
|
|
586
|
+
describe('getDefinition()', () => {
|
|
587
|
+
it('returns the workflow definition', () => {
|
|
588
|
+
const info = service.createWithSetup(($) => {
|
|
589
|
+
$.on.Customer.created(() => {})
|
|
590
|
+
$.on.Order.placed(() => {})
|
|
591
|
+
$.every.hour(() => {})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const definition = service.getDefinition(info.id)
|
|
595
|
+
|
|
596
|
+
expect(definition.events).toContainEqual({ noun: 'Customer', event: 'created' })
|
|
597
|
+
expect(definition.events).toContainEqual({ noun: 'Order', event: 'placed' })
|
|
598
|
+
expect(definition.schedules).toHaveLength(1)
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
describe('getRegisteredEvents()', () => {
|
|
603
|
+
it('returns list of events the workflow handles', () => {
|
|
604
|
+
const info = service.createWithSetup(($) => {
|
|
605
|
+
$.on.Customer.created(() => {})
|
|
606
|
+
$.on.Customer.updated(() => {})
|
|
607
|
+
$.on.Order.placed(() => {})
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
const events = service.getRegisteredEvents(info.id)
|
|
611
|
+
|
|
612
|
+
expect(events).toContain('Customer.created')
|
|
613
|
+
expect(events).toContain('Customer.updated')
|
|
614
|
+
expect(events).toContain('Order.placed')
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
describe('getRegisteredSchedules()', () => {
|
|
619
|
+
it('returns list of schedules the workflow handles', () => {
|
|
620
|
+
const info = service.createWithSetup(($) => {
|
|
621
|
+
$.every.hour(() => {})
|
|
622
|
+
$.every.minutes(30)(() => {})
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
const schedules = service.getRegisteredSchedules(info.id)
|
|
626
|
+
|
|
627
|
+
expect(schedules).toHaveLength(2)
|
|
628
|
+
expect(schedules).toContainEqual(expect.objectContaining({ type: 'hour' }))
|
|
629
|
+
expect(schedules).toContainEqual(expect.objectContaining({ type: 'minute', value: 30 }))
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
describe('canHandle()', () => {
|
|
634
|
+
it('checks if a workflow can handle a specific event', () => {
|
|
635
|
+
const info = service.createWithSetup(($) => {
|
|
636
|
+
$.on.Customer.created(() => {})
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
expect(service.canHandle(info.id, 'Customer.created')).toBe(true)
|
|
640
|
+
expect(service.canHandle(info.id, 'Order.placed')).toBe(false)
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
// ===========================================================================
|
|
646
|
+
// 8. Error Handling and Recovery
|
|
647
|
+
// ===========================================================================
|
|
648
|
+
describe('Error and Recovery RPC Methods', () => {
|
|
649
|
+
describe('getErrors()', () => {
|
|
650
|
+
it('returns list of errors that occurred in workflow', async () => {
|
|
651
|
+
const info = service.createWithSetup(($) => {
|
|
652
|
+
$.on.Test.error(() => {
|
|
653
|
+
throw new Error('Intentional error')
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// Emit event that causes error
|
|
658
|
+
service.emit(info.id, 'Test.error', {})
|
|
659
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
660
|
+
|
|
661
|
+
const errors = service.getErrors(info.id)
|
|
662
|
+
|
|
663
|
+
expect(errors).toContainEqual(
|
|
664
|
+
expect.objectContaining({
|
|
665
|
+
message: 'Intentional error',
|
|
666
|
+
event: 'Test.error',
|
|
667
|
+
timestamp: expect.any(Date),
|
|
668
|
+
})
|
|
669
|
+
)
|
|
670
|
+
})
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
describe('clearErrors()', () => {
|
|
674
|
+
it('clears error history for a workflow', async () => {
|
|
675
|
+
const info = service.createWithSetup(($) => {
|
|
676
|
+
$.on.Test.error(() => {
|
|
677
|
+
throw new Error('Error')
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
service.emit(info.id, 'Test.error', {})
|
|
682
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
683
|
+
|
|
684
|
+
service.clearErrors(info.id)
|
|
685
|
+
|
|
686
|
+
expect(service.getErrors(info.id)).toHaveLength(0)
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
describe('retry()', () => {
|
|
691
|
+
it('retries the last failed operation', async () => {
|
|
692
|
+
let attempts = 0
|
|
693
|
+
const info = service.createWithSetup(($) => {
|
|
694
|
+
$.on.Test.retry(() => {
|
|
695
|
+
attempts++
|
|
696
|
+
if (attempts < 2) {
|
|
697
|
+
throw new Error('Retry me')
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
service.emit(info.id, 'Test.retry', {})
|
|
703
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
704
|
+
|
|
705
|
+
const result = await service.retry(info.id)
|
|
706
|
+
|
|
707
|
+
expect(result.success).toBe(true)
|
|
708
|
+
expect(attempts).toBe(2)
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
describe('setErrorHandler()', () => {
|
|
713
|
+
it('sets a global error handler for workflow', () => {
|
|
714
|
+
const errors: Error[] = []
|
|
715
|
+
const info = service.createWithSetup(($) => {
|
|
716
|
+
$.on.Test.error(() => {
|
|
717
|
+
throw new Error('Caught error')
|
|
718
|
+
})
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
service.setErrorHandler(info.id, (error) => {
|
|
722
|
+
errors.push(error)
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
service.emit(info.id, 'Test.error', {})
|
|
726
|
+
|
|
727
|
+
// Error handler should be called
|
|
728
|
+
expect(errors).toHaveLength(1)
|
|
729
|
+
expect(errors[0].message).toBe('Caught error')
|
|
730
|
+
})
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
// ===========================================================================
|
|
735
|
+
// 9. Metrics and Observability
|
|
736
|
+
// ===========================================================================
|
|
737
|
+
describe('Metrics RPC Methods', () => {
|
|
738
|
+
describe('getMetrics()', () => {
|
|
739
|
+
it('returns workflow execution metrics', () => {
|
|
740
|
+
const info = service.create('metrics-test')
|
|
741
|
+
service.emit(info.id, 'Test.event1', {})
|
|
742
|
+
service.emit(info.id, 'Test.event2', {})
|
|
743
|
+
|
|
744
|
+
const metrics = service.getMetrics(info.id)
|
|
745
|
+
|
|
746
|
+
expect(metrics).toMatchObject({
|
|
747
|
+
eventsProcessed: expect.any(Number),
|
|
748
|
+
eventsEmitted: expect.any(Number),
|
|
749
|
+
errorCount: expect.any(Number),
|
|
750
|
+
uptime: expect.any(Number),
|
|
751
|
+
lastEventAt: expect.any(Date),
|
|
752
|
+
})
|
|
753
|
+
})
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
describe('getAggregateMetrics()', () => {
|
|
757
|
+
it('returns aggregate metrics across all workflows', () => {
|
|
758
|
+
service.create('agg-1')
|
|
759
|
+
service.create('agg-2')
|
|
760
|
+
service.emit(service.list()[0], 'Test.event', {})
|
|
761
|
+
|
|
762
|
+
const metrics = service.getAggregateMetrics()
|
|
763
|
+
|
|
764
|
+
expect(metrics).toMatchObject({
|
|
765
|
+
totalWorkflows: 2,
|
|
766
|
+
runningWorkflows: 0,
|
|
767
|
+
totalEventsProcessed: expect.any(Number),
|
|
768
|
+
totalErrors: expect.any(Number),
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
describe('resetMetrics()', () => {
|
|
774
|
+
it('resets metrics for a workflow', () => {
|
|
775
|
+
const info = service.create('reset-metrics-test')
|
|
776
|
+
service.emit(info.id, 'Test.event', {})
|
|
777
|
+
|
|
778
|
+
service.resetMetrics(info.id)
|
|
779
|
+
|
|
780
|
+
const metrics = service.getMetrics(info.id)
|
|
781
|
+
expect(metrics.eventsProcessed).toBe(0)
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
// ===========================================================================
|
|
787
|
+
// 10. Serialization for RPC
|
|
788
|
+
// ===========================================================================
|
|
789
|
+
describe('RPC Serialization', () => {
|
|
790
|
+
describe('toJSON()', () => {
|
|
791
|
+
it('serializes workflow info to JSON-safe format', () => {
|
|
792
|
+
const info = service.create('json-test', {
|
|
793
|
+
context: { date: new Date(), func: () => {} },
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
const json = service.toJSON(info.id)
|
|
797
|
+
const parsed = JSON.parse(JSON.stringify(json))
|
|
798
|
+
|
|
799
|
+
expect(parsed.id).toBe(info.id)
|
|
800
|
+
expect(parsed.name).toBe('json-test')
|
|
801
|
+
// Functions should be excluded
|
|
802
|
+
expect(parsed.state.context.func).toBeUndefined()
|
|
803
|
+
// Dates should be serialized
|
|
804
|
+
expect(typeof parsed.state.context.date).toBe('string')
|
|
805
|
+
})
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
describe('describe()', () => {
|
|
809
|
+
it('returns RPC interface description', () => {
|
|
810
|
+
const description = service.describe()
|
|
811
|
+
|
|
812
|
+
expect(description.methods).toContain('create')
|
|
813
|
+
expect(description.methods).toContain('start')
|
|
814
|
+
expect(description.methods).toContain('emit')
|
|
815
|
+
expect(description.version).toBeDefined()
|
|
816
|
+
})
|
|
817
|
+
})
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
// =============================================================================
|
|
822
|
+
// WorkflowService WorkerEntrypoint Tests
|
|
823
|
+
// =============================================================================
|
|
824
|
+
describe('RPC Surface: WorkflowService (WorkerEntrypoint)', () => {
|
|
825
|
+
describe('connect() with options', () => {
|
|
826
|
+
it('accepts configuration options', () => {
|
|
827
|
+
// This tests that connect() can accept optional configuration
|
|
828
|
+
// Note: We can't fully test WorkerEntrypoint without Workers runtime
|
|
829
|
+
expect(typeof WorkflowService.prototype.connect).toBe('function')
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
describe('RPC method signatures', () => {
|
|
834
|
+
it('all methods are callable via RPC', () => {
|
|
835
|
+
const service = new WorkflowServiceCore()
|
|
836
|
+
|
|
837
|
+
// Verify all expected RPC methods exist
|
|
838
|
+
const expectedMethods = [
|
|
839
|
+
// Creation
|
|
840
|
+
'create',
|
|
841
|
+
'createWithSetup',
|
|
842
|
+
'createFromDefinition',
|
|
843
|
+
'clone',
|
|
844
|
+
'import',
|
|
845
|
+
'export',
|
|
846
|
+
// Lifecycle
|
|
847
|
+
'start',
|
|
848
|
+
'stop',
|
|
849
|
+
'pause',
|
|
850
|
+
'resume',
|
|
851
|
+
'restart',
|
|
852
|
+
'destroy',
|
|
853
|
+
'getStatus',
|
|
854
|
+
// Events
|
|
855
|
+
'emit',
|
|
856
|
+
'emitBatch',
|
|
857
|
+
'emitWithDelay',
|
|
858
|
+
'cancelScheduledEvent',
|
|
859
|
+
'getPendingEvents',
|
|
860
|
+
'subscribeToEvents',
|
|
861
|
+
'getEventHistory',
|
|
862
|
+
'sendGlobal',
|
|
863
|
+
// State
|
|
864
|
+
'getState',
|
|
865
|
+
'setState',
|
|
866
|
+
'getValue',
|
|
867
|
+
'getStateSnapshot',
|
|
868
|
+
'mergeState',
|
|
869
|
+
'deleteValue',
|
|
870
|
+
'hasValue',
|
|
871
|
+
'getValues',
|
|
872
|
+
'setValues',
|
|
873
|
+
// Query
|
|
874
|
+
'get',
|
|
875
|
+
'list',
|
|
876
|
+
'has',
|
|
877
|
+
'listByName',
|
|
878
|
+
'listByStatus',
|
|
879
|
+
'count',
|
|
880
|
+
'find',
|
|
881
|
+
'getAll',
|
|
882
|
+
// Batch
|
|
883
|
+
'startBatch',
|
|
884
|
+
'stopBatch',
|
|
885
|
+
'destroyBatch',
|
|
886
|
+
'emitToAll',
|
|
887
|
+
// Introspection
|
|
888
|
+
'getDefinition',
|
|
889
|
+
'getRegisteredEvents',
|
|
890
|
+
'getRegisteredSchedules',
|
|
891
|
+
'canHandle',
|
|
892
|
+
// Errors
|
|
893
|
+
'getErrors',
|
|
894
|
+
'clearErrors',
|
|
895
|
+
'retry',
|
|
896
|
+
'setErrorHandler',
|
|
897
|
+
// Metrics
|
|
898
|
+
'getMetrics',
|
|
899
|
+
'getAggregateMetrics',
|
|
900
|
+
'resetMetrics',
|
|
901
|
+
// Utilities
|
|
902
|
+
'parseEvent',
|
|
903
|
+
'toCron',
|
|
904
|
+
'intervalToMs',
|
|
905
|
+
'formatInterval',
|
|
906
|
+
'createTestContext',
|
|
907
|
+
'clear',
|
|
908
|
+
'toJSON',
|
|
909
|
+
'describe',
|
|
910
|
+
// Global handlers
|
|
911
|
+
'registerGlobalEvent',
|
|
912
|
+
'registerGlobalSchedule',
|
|
913
|
+
'getGlobalEventHandlers',
|
|
914
|
+
'getGlobalScheduleHandlers',
|
|
915
|
+
'clearGlobalEventHandlers',
|
|
916
|
+
'clearGlobalScheduleHandlers',
|
|
917
|
+
// State persistence
|
|
918
|
+
'hasStatePersistence',
|
|
919
|
+
'getStateAdapter',
|
|
920
|
+
'persistState',
|
|
921
|
+
'loadPersistedState',
|
|
922
|
+
'saveCheckpoint',
|
|
923
|
+
'getCheckpoint',
|
|
924
|
+
'updateStateWithVersion',
|
|
925
|
+
'queryByStatus',
|
|
926
|
+
'queryByIds',
|
|
927
|
+
'deletePersistedState',
|
|
928
|
+
'listPersistedWorkflows',
|
|
929
|
+
'createSnapshot',
|
|
930
|
+
'restoreSnapshot',
|
|
931
|
+
'getSnapshots',
|
|
932
|
+
// WorkflowBuilder
|
|
933
|
+
'registerWorkflow',
|
|
934
|
+
]
|
|
935
|
+
|
|
936
|
+
for (const method of expectedMethods) {
|
|
937
|
+
expect(
|
|
938
|
+
typeof (service as Record<string, unknown>)[method],
|
|
939
|
+
`Method ${method} should exist`
|
|
940
|
+
).toBe('function')
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
service.clear()
|
|
944
|
+
})
|
|
945
|
+
})
|
|
946
|
+
})
|