ai-workflows 2.1.3 → 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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-workflows",
3
- "version": "2.1.3",
3
+ "version": "2.3.0",
4
4
  "description": "Event-driven workflows with state machine support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,11 +9,36 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
+ },
13
+ "./worker": {
14
+ "import": "./dist/worker/index.js",
15
+ "types": "./dist/worker/index.d.ts"
16
+ },
17
+ "./durable-execution": {
18
+ "import": "./dist/durable-execution.js",
19
+ "types": "./dist/durable-execution.d.ts"
12
20
  }
13
21
  },
14
- "dependencies": {},
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsc --watch",
25
+ "test": "vitest run",
26
+ "test:coverage": "vitest run --coverage",
27
+ "test:workers": "vitest --config vitest.workers.config.ts",
28
+ "test:workers:coverage": "vitest run --config vitest.workers.config.ts --coverage",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "eslint .",
31
+ "clean": "rm -rf dist"
32
+ },
33
+ "dependencies": {
34
+ "@org.ai/types": "2.3.0",
35
+ "digital-objects": "1.1.0"
36
+ },
15
37
  "devDependencies": {
16
- "vitest": "^2.1.0"
38
+ "@cloudflare/vitest-pool-workers": "^0.8.0",
39
+ "@cloudflare/workers-types": "^4.20250109.0",
40
+ "vitest": "^2.1.0",
41
+ "wrangler": "^4.0.0"
17
42
  },
18
43
  "keywords": [
19
44
  "ai",
@@ -22,13 +47,5 @@
22
47
  "state-machine",
23
48
  "primitives"
24
49
  ],
25
- "license": "MIT",
26
- "scripts": {
27
- "build": "tsc",
28
- "dev": "tsc --watch",
29
- "test": "vitest",
30
- "typecheck": "tsc --noEmit",
31
- "lint": "eslint .",
32
- "clean": "rm -rf dist"
33
- }
34
- }
50
+ "license": "MIT"
51
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Tests for digital-objects adapter
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest'
6
+ import { createMemoryProvider, type DigitalObjectsProvider } from 'digital-objects'
7
+ import {
8
+ createDigitalObjectsAdapter,
9
+ createSimpleAdapter,
10
+ type DigitalObjectsDatabaseContext,
11
+ } from '../digital-objects-adapter.js'
12
+ import type { DatabaseContext } from '../types.js'
13
+
14
+ describe('createDigitalObjectsAdapter', () => {
15
+ let provider: DigitalObjectsProvider
16
+ let adapter: DigitalObjectsDatabaseContext
17
+
18
+ beforeEach(async () => {
19
+ provider = createMemoryProvider()
20
+ adapter = await createDigitalObjectsAdapter(provider)
21
+ })
22
+
23
+ describe('auto-definition', () => {
24
+ it('should define workflow nouns', async () => {
25
+ const workflowNoun = await provider.getNoun('Workflow')
26
+ expect(workflowNoun).not.toBeNull()
27
+ expect(workflowNoun?.name).toBe('Workflow')
28
+
29
+ const artifactNoun = await provider.getNoun('Artifact')
30
+ expect(artifactNoun).not.toBeNull()
31
+
32
+ const scheduleNoun = await provider.getNoun('Schedule')
33
+ expect(scheduleNoun).not.toBeNull()
34
+
35
+ const cascadeNoun = await provider.getNoun('Cascade')
36
+ expect(cascadeNoun).not.toBeNull()
37
+ })
38
+
39
+ it('should define workflow verbs', async () => {
40
+ const emitVerb = await provider.getVerb('emit')
41
+ expect(emitVerb).not.toBeNull()
42
+ expect(emitVerb?.name).toBe('emit')
43
+
44
+ const executeVerb = await provider.getVerb('execute')
45
+ expect(executeVerb).not.toBeNull()
46
+
47
+ const completeVerb = await provider.getVerb('complete')
48
+ expect(completeVerb).not.toBeNull()
49
+
50
+ const stepVerb = await provider.getVerb('step')
51
+ expect(stepVerb).not.toBeNull()
52
+ })
53
+
54
+ it('should skip definition if already defined', async () => {
55
+ // Create another adapter - should not throw
56
+ const adapter2 = await createDigitalObjectsAdapter(provider)
57
+ expect(adapter2).toBeDefined()
58
+ })
59
+
60
+ it('should skip auto-definition when disabled', async () => {
61
+ const freshProvider = createMemoryProvider()
62
+ await createDigitalObjectsAdapter(freshProvider, { autoDefine: true })
63
+
64
+ // Nouns should be defined
65
+ const workflowNoun = await freshProvider.getNoun('Workflow')
66
+ expect(workflowNoun).not.toBeNull()
67
+ })
68
+ })
69
+
70
+ describe('recordEvent', () => {
71
+ it('should record events as Actions', async () => {
72
+ await adapter.recordEvent('Customer.created', { name: 'John', email: 'john@example.com' })
73
+
74
+ const actions = await provider.listActions({ verb: 'emit' })
75
+ expect(actions.length).toBe(1)
76
+ expect(actions[0]?.data).toMatchObject({
77
+ event: 'Customer.created',
78
+ data: { name: 'John', email: 'john@example.com' },
79
+ })
80
+ })
81
+
82
+ it('should associate events with workflow when workflowId is set', async () => {
83
+ const adapterWithWorkflow = await createDigitalObjectsAdapter(provider, {
84
+ workflowId: 'wf-123',
85
+ })
86
+
87
+ await adapterWithWorkflow.recordEvent('Order.completed', { orderId: 'order-1' })
88
+
89
+ const actions = await provider.listActions({ verb: 'emit', subject: 'wf-123' })
90
+ expect(actions.length).toBe(1)
91
+ })
92
+ })
93
+
94
+ describe('createAction', () => {
95
+ it('should create actions with proper verb', async () => {
96
+ await adapter.createAction({
97
+ actor: 'user-1',
98
+ object: 'document-1',
99
+ action: 'edit',
100
+ status: 'pending',
101
+ metadata: { changes: ['title'] },
102
+ })
103
+
104
+ const actions = await provider.listActions({ verb: 'edit' })
105
+ expect(actions.length).toBe(1)
106
+ expect(actions[0]?.subject).toBe('user-1')
107
+ expect(actions[0]?.object).toBe('document-1')
108
+ })
109
+ })
110
+
111
+ describe('completeAction', () => {
112
+ it('should record completion as an action', async () => {
113
+ // First create an action
114
+ const action = await provider.perform('process', 'worker', 'task-1', {})
115
+
116
+ // Complete it
117
+ await adapter.completeAction(action.id, { success: true, output: 'done' })
118
+
119
+ // Verify completion action was recorded
120
+ const completions = await provider.listActions({ verb: 'complete' })
121
+ expect(completions.length).toBe(1)
122
+ expect(completions[0]?.object).toBe(action.id)
123
+ expect(completions[0]?.data).toMatchObject({
124
+ result: { success: true, output: 'done' },
125
+ })
126
+ })
127
+ })
128
+
129
+ describe('artifacts', () => {
130
+ it('should store artifacts as Things', async () => {
131
+ await adapter.storeArtifact({
132
+ key: 'ast-hash-123',
133
+ type: 'ast',
134
+ sourceHash: 'abc123',
135
+ content: { type: 'Program', body: [] },
136
+ metadata: { language: 'javascript' },
137
+ })
138
+
139
+ const thing = await provider.get('ast-hash-123')
140
+ expect(thing).not.toBeNull()
141
+ expect(thing?.noun).toBe('Artifact')
142
+ expect(thing?.data).toMatchObject({
143
+ key: 'ast-hash-123',
144
+ type: 'ast',
145
+ sourceHash: 'abc123',
146
+ })
147
+ })
148
+
149
+ it('should retrieve artifact content', async () => {
150
+ await adapter.storeArtifact({
151
+ key: 'compiled-module',
152
+ type: 'esm',
153
+ sourceHash: 'def456',
154
+ content: 'export default function() {}',
155
+ })
156
+
157
+ const content = await adapter.getArtifact('compiled-module')
158
+ expect(content).toBe('export default function() {}')
159
+ })
160
+
161
+ it('should return null for non-existent artifact', async () => {
162
+ const content = await adapter.getArtifact('does-not-exist')
163
+ expect(content).toBeNull()
164
+ })
165
+
166
+ it('should include workflowId when set', async () => {
167
+ const adapterWithWorkflow = await createDigitalObjectsAdapter(provider, {
168
+ workflowId: 'wf-456',
169
+ })
170
+
171
+ await adapterWithWorkflow.storeArtifact({
172
+ key: 'workflow-artifact',
173
+ type: 'bundle',
174
+ sourceHash: 'ghi789',
175
+ content: { bundled: true },
176
+ })
177
+
178
+ const thing = await provider.get('workflow-artifact')
179
+ expect(thing?.data).toHaveProperty('workflowId', 'wf-456')
180
+ })
181
+ })
182
+
183
+ describe('extended methods', () => {
184
+ it('should provide access to provider', () => {
185
+ expect(adapter.provider).toBe(provider)
186
+ })
187
+
188
+ it('should get workflow by id', async () => {
189
+ await provider.create(
190
+ 'Workflow',
191
+ {
192
+ name: 'test-workflow',
193
+ status: 'running',
194
+ context: { userId: '123' },
195
+ },
196
+ 'wf-test'
197
+ )
198
+
199
+ const workflow = await adapter.getWorkflow('wf-test')
200
+ expect(workflow).not.toBeNull()
201
+ expect(workflow?.data).toMatchObject({ name: 'test-workflow' })
202
+ })
203
+
204
+ it('should update workflow', async () => {
205
+ await provider.create(
206
+ 'Workflow',
207
+ {
208
+ name: 'test-workflow',
209
+ status: 'running',
210
+ context: {},
211
+ },
212
+ 'wf-update'
213
+ )
214
+
215
+ const updated = await adapter.updateWorkflow('wf-update', {
216
+ status: 'completed',
217
+ context: { result: 'done' },
218
+ })
219
+
220
+ expect(updated.data).toMatchObject({
221
+ status: 'completed',
222
+ context: { result: 'done' },
223
+ })
224
+ })
225
+
226
+ it('should list workflow actions', async () => {
227
+ const adapterWithWorkflow = await createDigitalObjectsAdapter(provider, {
228
+ workflowId: 'wf-actions',
229
+ })
230
+
231
+ await adapterWithWorkflow.recordEvent('Event1', {})
232
+ await adapterWithWorkflow.recordEvent('Event2', {})
233
+
234
+ const actions = await adapterWithWorkflow.listWorkflowActions('wf-actions')
235
+ expect(actions.length).toBe(2)
236
+ })
237
+
238
+ it('should list actions by verb', async () => {
239
+ await adapter.recordEvent('Event1', {})
240
+ await adapter.recordEvent('Event2', {})
241
+
242
+ const actions = await adapter.listActionsByVerb('emit')
243
+ expect(actions.length).toBe(2)
244
+ })
245
+ })
246
+ })
247
+
248
+ describe('createSimpleAdapter', () => {
249
+ it('should return only DatabaseContext interface', async () => {
250
+ const provider = createMemoryProvider()
251
+ const simple = await createSimpleAdapter(provider)
252
+
253
+ // Should have all DatabaseContext methods
254
+ expect(typeof simple.recordEvent).toBe('function')
255
+ expect(typeof simple.createAction).toBe('function')
256
+ expect(typeof simple.completeAction).toBe('function')
257
+ expect(typeof simple.storeArtifact).toBe('function')
258
+ expect(typeof simple.getArtifact).toBe('function')
259
+
260
+ // Should NOT have extended methods
261
+ expect((simple as DigitalObjectsDatabaseContext).provider).toBeUndefined()
262
+ expect((simple as DigitalObjectsDatabaseContext).getWorkflow).toBeUndefined()
263
+ })
264
+
265
+ it('should work with workflow options', async () => {
266
+ const provider = createMemoryProvider()
267
+ const simple = await createSimpleAdapter(provider, { workflowId: 'wf-simple' })
268
+
269
+ await simple.recordEvent('Test.event', { data: 'test' })
270
+
271
+ const actions = await provider.listActions({ subject: 'wf-simple' })
272
+ expect(actions.length).toBe(1)
273
+ })
274
+ })
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Tests for DurableWorkflow
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
6
+ import { createMemoryProvider, type DigitalObjectsProvider } from 'digital-objects'
7
+ import {
8
+ DurableWorkflow,
9
+ createDurableWorkflow,
10
+ type DurableWorkflowState,
11
+ } from '../durable-workflow.js'
12
+
13
+ describe('DurableWorkflow', () => {
14
+ let provider: DigitalObjectsProvider
15
+
16
+ beforeEach(() => {
17
+ provider = createMemoryProvider()
18
+ })
19
+
20
+ afterEach(async () => {
21
+ // Clean up any workflows
22
+ await provider.close?.()
23
+ })
24
+
25
+ describe('initialization', () => {
26
+ it('should create a new workflow instance', async () => {
27
+ const workflow = new DurableWorkflow(provider)
28
+
29
+ await workflow.initialize('test-workflow', ($) => {
30
+ $.on.Test.event(async () => {})
31
+ })
32
+
33
+ // Verify workflow was created in digital-objects
34
+ const thing = await provider.get<DurableWorkflowState>(workflow.id)
35
+ expect(thing).not.toBeNull()
36
+ expect(thing?.noun).toBe('Workflow')
37
+ expect(thing?.data.name).toBe('test-workflow')
38
+ expect(thing?.data.status).toBe('running')
39
+ })
40
+
41
+ it('should use provided instance ID', async () => {
42
+ const workflow = new DurableWorkflow(provider, { instanceId: 'my-workflow-id' })
43
+
44
+ await workflow.initialize('named-workflow', ($) => {})
45
+
46
+ expect(workflow.id).toBe('my-workflow-id')
47
+
48
+ const thing = await provider.get('my-workflow-id')
49
+ expect(thing).not.toBeNull()
50
+ })
51
+
52
+ it('should register event handlers', async () => {
53
+ const workflow = new DurableWorkflow(provider)
54
+
55
+ await workflow.initialize('event-workflow', ($) => {
56
+ $.on.Customer.created(async () => {})
57
+ $.on.Order.completed(async () => {})
58
+ })
59
+
60
+ const thing = await provider.get<DurableWorkflowState>(workflow.id)
61
+ expect(thing?.data.registeredEvents).toContain('Customer.created')
62
+ expect(thing?.data.registeredEvents).toContain('Order.completed')
63
+ })
64
+
65
+ it('should register schedule handlers', async () => {
66
+ const workflow = new DurableWorkflow(provider)
67
+
68
+ await workflow.initialize('schedule-workflow', ($) => {
69
+ $.every.minutes(30)(async () => {})
70
+ })
71
+
72
+ const thing = await provider.get<DurableWorkflowState>(workflow.id)
73
+ expect(thing?.data.registeredSchedules).toContain('minute:30')
74
+ })
75
+
76
+ it('should reject double initialization', async () => {
77
+ const workflow = new DurableWorkflow(provider)
78
+
79
+ await workflow.initialize('test', ($) => {})
80
+
81
+ await expect(workflow.initialize('test2', ($) => {})).rejects.toThrow(
82
+ 'Workflow already initialized'
83
+ )
84
+ })
85
+
86
+ it('should initialize with context', async () => {
87
+ const workflow = new DurableWorkflow(provider, {
88
+ context: { userId: 'user-123', tenant: 'acme' },
89
+ })
90
+
91
+ await workflow.initialize('context-workflow', ($) => {})
92
+
93
+ const thing = await provider.get<DurableWorkflowState>(workflow.id)
94
+ expect(thing?.data.context).toMatchObject({
95
+ userId: 'user-123',
96
+ tenant: 'acme',
97
+ })
98
+ })
99
+ })
100
+
101
+ describe('state management', () => {
102
+ it('should get current state', async () => {
103
+ const workflow = new DurableWorkflow(provider, {
104
+ context: { counter: 0 },
105
+ })
106
+
107
+ await workflow.initialize('state-workflow', ($) => {
108
+ $.state.counter = 1
109
+ })
110
+
111
+ const state = workflow.getState()
112
+ expect(state.context.counter).toBe(1)
113
+ })
114
+
115
+ it('should persist state changes', async () => {
116
+ const workflow = new DurableWorkflow(provider)
117
+
118
+ await workflow.initialize('persist-workflow', ($) => {
119
+ $.set('key1', 'value1')
120
+ $.set('key2', { nested: true })
121
+ })
122
+
123
+ // Wait for async persist
124
+ await new Promise((r) => setTimeout(r, 10))
125
+
126
+ const thing = await provider.get<DurableWorkflowState>(workflow.id)
127
+ expect(thing?.data.context.key1).toBe('value1')
128
+ expect(thing?.data.context.key2).toMatchObject({ nested: true })
129
+ })
130
+
131
+ it('should support $.get and $.set', async () => {
132
+ const workflow = new DurableWorkflow(provider)
133
+
134
+ let capturedValue: unknown
135
+
136
+ await workflow.initialize('getset-workflow', ($) => {
137
+ $.set('myKey', 'myValue')
138
+ capturedValue = $.get('myKey')
139
+ })
140
+
141
+ expect(capturedValue).toBe('myValue')
142
+ })
143
+ })
144
+
145
+ describe('event handling', () => {
146
+ it('should send events and trigger handlers', async () => {
147
+ const workflow = new DurableWorkflow(provider)
148
+ const handler = vi.fn()
149
+
150
+ await workflow.initialize('event-workflow', ($) => {
151
+ $.on.Test.triggered(handler)
152
+ })
153
+
154
+ await workflow.start()
155
+ await workflow.send('Test.triggered', { value: 42 })
156
+
157
+ expect(handler).toHaveBeenCalledTimes(1)
158
+ expect(handler).toHaveBeenCalledWith(
159
+ expect.objectContaining({ value: 42 }),
160
+ expect.anything()
161
+ )
162
+
163
+ await workflow.destroy()
164
+ })
165
+
166
+ it('should record events as Actions', async () => {
167
+ const workflow = new DurableWorkflow(provider)
168
+
169
+ await workflow.initialize('action-workflow', ($) => {
170
+ $.on.Event.happened(async () => {})
171
+ })
172
+
173
+ await workflow.start()
174
+ await workflow.send('Event.happened', { data: 'test' })
175
+
176
+ // Wait for async persistence
177
+ await new Promise((r) => setTimeout(r, 10))
178
+
179
+ const actions = await provider.listActions({ verb: 'emit' })
180
+ expect(actions.length).toBeGreaterThan(0)
181
+
182
+ await workflow.destroy()
183
+ })
184
+
185
+ it('should throw for uninitialized workflow', async () => {
186
+ const workflow = new DurableWorkflow(provider)
187
+
188
+ await expect(workflow.send('Test.event', {})).rejects.toThrow('Workflow not initialized')
189
+ })
190
+ })
191
+
192
+ describe('history', () => {
193
+ it('should track history entries', async () => {
194
+ const workflow = new DurableWorkflow(provider)
195
+
196
+ await workflow.initialize('history-workflow', ($) => {
197
+ $.on.Step.one(async () => {})
198
+ })
199
+
200
+ await workflow.start()
201
+ await workflow.send('Step.one', {})
202
+
203
+ const state = workflow.getState()
204
+ expect(state.history.length).toBeGreaterThan(0)
205
+ expect(state.history.some((h) => h.name === 'workflow.started')).toBe(true)
206
+
207
+ await workflow.destroy()
208
+ })
209
+
210
+ it('should persist history as Actions', async () => {
211
+ const workflow = new DurableWorkflow(provider)
212
+
213
+ await workflow.initialize('persist-history', ($) => {
214
+ $.log('Test log message')
215
+ })
216
+
217
+ // Wait for async persistence
218
+ await new Promise((r) => setTimeout(r, 10))
219
+
220
+ const actions = await provider.listActions({ verb: 'action', subject: workflow.id })
221
+ expect(actions.some((a) => a.data?.name === 'log')).toBe(true)
222
+
223
+ await workflow.destroy()
224
+ })
225
+ })
226
+
227
+ describe('lifecycle', () => {
228
+ it('should start and stop workflow', async () => {
229
+ const workflow = new DurableWorkflow(provider)
230
+
231
+ await workflow.initialize('lifecycle-workflow', ($) => {})
232
+
233
+ await workflow.start()
234
+
235
+ let thing = await provider.get<DurableWorkflowState>(workflow.id)
236
+ expect(thing?.data.status).toBe('running')
237
+
238
+ await workflow.stop()
239
+
240
+ thing = await provider.get<DurableWorkflowState>(workflow.id)
241
+ expect(thing?.data.status).toBe('paused')
242
+ })
243
+
244
+ it('should destroy workflow', async () => {
245
+ const workflow = new DurableWorkflow(provider)
246
+
247
+ await workflow.initialize('destroy-workflow', ($) => {})
248
+ await workflow.start()
249
+ await workflow.destroy()
250
+
251
+ const thing = await provider.get<DurableWorkflowState>(workflow.id)
252
+ expect(thing?.data.status).toBe('completed')
253
+ })
254
+
255
+ it('should reject start before initialization', async () => {
256
+ const workflow = new DurableWorkflow(provider)
257
+
258
+ await expect(workflow.start()).rejects.toThrow('Workflow not initialized')
259
+ })
260
+ })
261
+
262
+ describe('recovery', () => {
263
+ it('should restore workflow from existing state', async () => {
264
+ // Create initial workflow
265
+ const workflow1 = new DurableWorkflow(provider, { instanceId: 'recovery-test' })
266
+
267
+ await workflow1.initialize('recoverable', ($) => {
268
+ $.set('data', 'original')
269
+ })
270
+
271
+ await workflow1.start()
272
+ await workflow1.stop()
273
+
274
+ // Create new workflow with same ID - should restore
275
+ const workflow2 = new DurableWorkflow(provider, { instanceId: 'recovery-test' })
276
+
277
+ await workflow2.initialize('recoverable', ($) => {})
278
+
279
+ const state = workflow2.getState()
280
+ expect(state.context.data).toBe('original')
281
+
282
+ await workflow2.destroy()
283
+ })
284
+ })
285
+
286
+ describe('createDurableWorkflow factory', () => {
287
+ it('should create workflow instance', () => {
288
+ const workflow = createDurableWorkflow(provider)
289
+ expect(workflow).toBeInstanceOf(DurableWorkflow)
290
+ })
291
+
292
+ it('should pass options', () => {
293
+ const workflow = createDurableWorkflow(provider, { instanceId: 'factory-test' })
294
+ expect(workflow.id).toBe('factory-test')
295
+ })
296
+ })
297
+ })