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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +8 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-workflows",
|
|
3
|
-
"version": "2.
|
|
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
|
-
"
|
|
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": "^
|
|
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
|
-
|
|
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
|
+
})
|