@sonamu-kit/tasks 0.0.1

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 (280) hide show
  1. package/.swcrc +17 -0
  2. package/README.md +7 -0
  3. package/dist/backend.d.ts +107 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +3 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/chaos.test.d.ts +2 -0
  8. package/dist/chaos.test.d.ts.map +1 -0
  9. package/dist/chaos.test.js +92 -0
  10. package/dist/chaos.test.js.map +1 -0
  11. package/dist/client.d.ts +178 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +223 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/client.test.d.ts +2 -0
  16. package/dist/client.test.d.ts.map +1 -0
  17. package/dist/client.test.js +339 -0
  18. package/dist/client.test.js.map +1 -0
  19. package/dist/config.d.ts +22 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +23 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/config.test.d.ts +2 -0
  24. package/dist/config.test.d.ts.map +1 -0
  25. package/dist/config.test.js +24 -0
  26. package/dist/config.test.js.map +1 -0
  27. package/dist/core/duration.d.ts +22 -0
  28. package/dist/core/duration.d.ts.map +1 -0
  29. package/dist/core/duration.js +64 -0
  30. package/dist/core/duration.js.map +1 -0
  31. package/dist/core/duration.test.d.ts +2 -0
  32. package/dist/core/duration.test.d.ts.map +1 -0
  33. package/dist/core/duration.test.js +265 -0
  34. package/dist/core/duration.test.js.map +1 -0
  35. package/dist/core/error.d.ts +15 -0
  36. package/dist/core/error.d.ts.map +1 -0
  37. package/dist/core/error.js +25 -0
  38. package/dist/core/error.js.map +1 -0
  39. package/dist/core/error.test.d.ts +2 -0
  40. package/dist/core/error.test.d.ts.map +1 -0
  41. package/dist/core/error.test.js +63 -0
  42. package/dist/core/error.test.js.map +1 -0
  43. package/dist/core/json.d.ts +5 -0
  44. package/dist/core/json.d.ts.map +1 -0
  45. package/dist/core/json.js +3 -0
  46. package/dist/core/json.js.map +1 -0
  47. package/dist/core/result.d.ts +22 -0
  48. package/dist/core/result.d.ts.map +1 -0
  49. package/dist/core/result.js +22 -0
  50. package/dist/core/result.js.map +1 -0
  51. package/dist/core/result.test.d.ts +2 -0
  52. package/dist/core/result.test.d.ts.map +1 -0
  53. package/dist/core/result.test.js +19 -0
  54. package/dist/core/result.test.js.map +1 -0
  55. package/dist/core/retry.d.ts +21 -0
  56. package/dist/core/retry.d.ts.map +1 -0
  57. package/dist/core/retry.js +25 -0
  58. package/dist/core/retry.js.map +1 -0
  59. package/dist/core/retry.test.d.ts +2 -0
  60. package/dist/core/retry.test.d.ts.map +1 -0
  61. package/dist/core/retry.test.js +37 -0
  62. package/dist/core/retry.test.js.map +1 -0
  63. package/dist/core/schema.d.ts +57 -0
  64. package/dist/core/schema.d.ts.map +1 -0
  65. package/dist/core/schema.js +4 -0
  66. package/dist/core/schema.js.map +1 -0
  67. package/dist/core/step.d.ts +96 -0
  68. package/dist/core/step.d.ts.map +1 -0
  69. package/dist/core/step.js +78 -0
  70. package/dist/core/step.js.map +1 -0
  71. package/dist/core/step.test.d.ts +2 -0
  72. package/dist/core/step.test.d.ts.map +1 -0
  73. package/dist/core/step.test.js +356 -0
  74. package/dist/core/step.test.js.map +1 -0
  75. package/dist/core/workflow.d.ts +78 -0
  76. package/dist/core/workflow.d.ts.map +1 -0
  77. package/dist/core/workflow.js +46 -0
  78. package/dist/core/workflow.js.map +1 -0
  79. package/dist/core/workflow.test.d.ts +2 -0
  80. package/dist/core/workflow.test.d.ts.map +1 -0
  81. package/dist/core/workflow.test.js +172 -0
  82. package/dist/core/workflow.test.js.map +1 -0
  83. package/dist/database/backend.d.ts +60 -0
  84. package/dist/database/backend.d.ts.map +1 -0
  85. package/dist/database/backend.js +387 -0
  86. package/dist/database/backend.js.map +1 -0
  87. package/dist/database/backend.test.d.ts +2 -0
  88. package/dist/database/backend.test.d.ts.map +1 -0
  89. package/dist/database/backend.test.js +17 -0
  90. package/dist/database/backend.test.js.map +1 -0
  91. package/dist/database/backend.testsuite.d.ts +20 -0
  92. package/dist/database/backend.testsuite.d.ts.map +1 -0
  93. package/dist/database/backend.testsuite.js +1174 -0
  94. package/dist/database/backend.testsuite.js.map +1 -0
  95. package/dist/database/base.d.ts +12 -0
  96. package/dist/database/base.d.ts.map +1 -0
  97. package/dist/database/base.js +19 -0
  98. package/dist/database/base.js.map +1 -0
  99. package/dist/database/migrations/20251212000000_0_init.js +9 -0
  100. package/dist/database/migrations/20251212000000_0_init.js.map +1 -0
  101. package/dist/database/migrations/20251212000000_1_tables.js +88 -0
  102. package/dist/database/migrations/20251212000000_1_tables.js.map +1 -0
  103. package/dist/database/migrations/20251212000000_2_fk.js +48 -0
  104. package/dist/database/migrations/20251212000000_2_fk.js.map +1 -0
  105. package/dist/database/migrations/20251212000000_3_indexes.js +107 -0
  106. package/dist/database/migrations/20251212000000_3_indexes.js.map +1 -0
  107. package/dist/database/pubsub.d.ts +17 -0
  108. package/dist/database/pubsub.d.ts.map +1 -0
  109. package/dist/database/pubsub.js +70 -0
  110. package/dist/database/pubsub.js.map +1 -0
  111. package/dist/database/pubsub.test.d.ts +2 -0
  112. package/dist/database/pubsub.test.d.ts.map +1 -0
  113. package/dist/database/pubsub.test.js +86 -0
  114. package/dist/database/pubsub.test.js.map +1 -0
  115. package/dist/errors.d.ts +8 -0
  116. package/dist/errors.d.ts.map +1 -0
  117. package/dist/errors.js +21 -0
  118. package/dist/errors.js.map +1 -0
  119. package/dist/execution.d.ts +82 -0
  120. package/dist/execution.d.ts.map +1 -0
  121. package/dist/execution.js +182 -0
  122. package/dist/execution.js.map +1 -0
  123. package/dist/execution.test.d.ts +2 -0
  124. package/dist/execution.test.d.ts.map +1 -0
  125. package/dist/execution.test.js +556 -0
  126. package/dist/execution.test.js.map +1 -0
  127. package/dist/index.d.ts +8 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +6 -0
  130. package/dist/index.js.map +1 -0
  131. package/dist/internal.d.ts +12 -0
  132. package/dist/internal.d.ts.map +1 -0
  133. package/dist/internal.js +5 -0
  134. package/dist/internal.js.map +1 -0
  135. package/dist/practices/01-remote-workflow.d.ts +2 -0
  136. package/dist/practices/01-remote-workflow.d.ts.map +1 -0
  137. package/dist/practices/01-remote-workflow.js +69 -0
  138. package/dist/practices/01-remote-workflow.js.map +1 -0
  139. package/dist/practices/01-remote.d.ts +2 -0
  140. package/dist/practices/01-remote.d.ts.map +1 -0
  141. package/dist/practices/01-remote.js +87 -0
  142. package/dist/practices/01-remote.js.map +1 -0
  143. package/dist/practices/02-local.d.ts +2 -0
  144. package/dist/practices/02-local.d.ts.map +1 -0
  145. package/dist/practices/02-local.js +84 -0
  146. package/dist/practices/02-local.js.map +1 -0
  147. package/dist/practices/03-local-retry.d.ts +2 -0
  148. package/dist/practices/03-local-retry.d.ts.map +1 -0
  149. package/dist/practices/03-local-retry.js +85 -0
  150. package/dist/practices/03-local-retry.js.map +1 -0
  151. package/dist/practices/04-scheduler-dispose.d.ts +2 -0
  152. package/dist/practices/04-scheduler-dispose.d.ts.map +1 -0
  153. package/dist/practices/04-scheduler-dispose.js +65 -0
  154. package/dist/practices/04-scheduler-dispose.js.map +1 -0
  155. package/dist/practices/05-router.d.ts +2 -0
  156. package/dist/practices/05-router.d.ts.map +1 -0
  157. package/dist/practices/05-router.js +80 -0
  158. package/dist/practices/05-router.js.map +1 -0
  159. package/dist/registry.d.ts +33 -0
  160. package/dist/registry.d.ts.map +1 -0
  161. package/dist/registry.js +54 -0
  162. package/dist/registry.js.map +1 -0
  163. package/dist/registry.test.d.ts +2 -0
  164. package/dist/registry.test.d.ts.map +1 -0
  165. package/dist/registry.test.js +95 -0
  166. package/dist/registry.test.js.map +1 -0
  167. package/dist/scheduler.d.ts +22 -0
  168. package/dist/scheduler.d.ts.map +1 -0
  169. package/dist/scheduler.js +117 -0
  170. package/dist/scheduler.js.map +1 -0
  171. package/dist/tasks/index.d.ts +4 -0
  172. package/dist/tasks/index.d.ts.map +1 -0
  173. package/dist/tasks/index.js +5 -0
  174. package/dist/tasks/index.js.map +1 -0
  175. package/dist/tasks/local-task.d.ts +6 -0
  176. package/dist/tasks/local-task.d.ts.map +1 -0
  177. package/dist/tasks/local-task.js +95 -0
  178. package/dist/tasks/local-task.js.map +1 -0
  179. package/dist/tasks/remote-task.d.ts +11 -0
  180. package/dist/tasks/remote-task.d.ts.map +1 -0
  181. package/dist/tasks/remote-task.js +213 -0
  182. package/dist/tasks/remote-task.js.map +1 -0
  183. package/dist/tasks/shared.d.ts +8 -0
  184. package/dist/tasks/shared.d.ts.map +1 -0
  185. package/dist/tasks/shared.js +41 -0
  186. package/dist/tasks/shared.js.map +1 -0
  187. package/dist/testing/connection.d.ts +7 -0
  188. package/dist/testing/connection.d.ts.map +1 -0
  189. package/dist/testing/connection.js +38 -0
  190. package/dist/testing/connection.js.map +1 -0
  191. package/dist/types/config.d.ts +44 -0
  192. package/dist/types/config.d.ts.map +1 -0
  193. package/dist/types/config.js +3 -0
  194. package/dist/types/config.js.map +1 -0
  195. package/dist/types/context.d.ts +18 -0
  196. package/dist/types/context.d.ts.map +1 -0
  197. package/dist/types/context.js +4 -0
  198. package/dist/types/context.js.map +1 -0
  199. package/dist/types/events.d.ts +43 -0
  200. package/dist/types/events.d.ts.map +1 -0
  201. package/dist/types/events.js +3 -0
  202. package/dist/types/events.js.map +1 -0
  203. package/dist/types/index.d.ts +6 -0
  204. package/dist/types/index.d.ts.map +1 -0
  205. package/dist/types/index.js +3 -0
  206. package/dist/types/index.js.map +1 -0
  207. package/dist/types/task-items.d.ts +12 -0
  208. package/dist/types/task-items.d.ts.map +1 -0
  209. package/dist/types/task-items.js +3 -0
  210. package/dist/types/task-items.js.map +1 -0
  211. package/dist/types/utils.d.ts +4 -0
  212. package/dist/types/utils.d.ts.map +1 -0
  213. package/dist/types/utils.js +8 -0
  214. package/dist/types/utils.js.map +1 -0
  215. package/dist/worker.d.ts +61 -0
  216. package/dist/worker.d.ts.map +1 -0
  217. package/dist/worker.js +206 -0
  218. package/dist/worker.js.map +1 -0
  219. package/dist/worker.test.d.ts +2 -0
  220. package/dist/worker.test.d.ts.map +1 -0
  221. package/dist/worker.test.js +1163 -0
  222. package/dist/worker.test.js.map +1 -0
  223. package/dist/workflow.d.ts +44 -0
  224. package/dist/workflow.d.ts.map +1 -0
  225. package/dist/workflow.js +21 -0
  226. package/dist/workflow.js.map +1 -0
  227. package/dist/workflow.test.d.ts +2 -0
  228. package/dist/workflow.test.d.ts.map +1 -0
  229. package/dist/workflow.test.js +73 -0
  230. package/dist/workflow.test.js.map +1 -0
  231. package/nodemon.json +6 -0
  232. package/package.json +63 -0
  233. package/scripts/migrate.ts +11 -0
  234. package/src/backend.ts +133 -0
  235. package/src/chaos.test.ts +108 -0
  236. package/src/client.test.ts +297 -0
  237. package/src/client.ts +331 -0
  238. package/src/config.test.ts +23 -0
  239. package/src/config.ts +35 -0
  240. package/src/core/duration.test.ts +326 -0
  241. package/src/core/duration.ts +86 -0
  242. package/src/core/error.test.ts +77 -0
  243. package/src/core/error.ts +30 -0
  244. package/src/core/json.ts +2 -0
  245. package/src/core/result.test.ts +13 -0
  246. package/src/core/result.ts +29 -0
  247. package/src/core/retry.test.ts +41 -0
  248. package/src/core/retry.ts +29 -0
  249. package/src/core/schema.ts +74 -0
  250. package/src/core/step.test.ts +362 -0
  251. package/src/core/step.ts +152 -0
  252. package/src/core/workflow.test.ts +184 -0
  253. package/src/core/workflow.ts +127 -0
  254. package/src/database/backend.test.ts +16 -0
  255. package/src/database/backend.testsuite.ts +1376 -0
  256. package/src/database/backend.ts +655 -0
  257. package/src/database/base.ts +23 -0
  258. package/src/database/migrations/20251212000000_0_init.ts +10 -0
  259. package/src/database/migrations/20251212000000_1_tables.ts +54 -0
  260. package/src/database/migrations/20251212000000_2_fk.ts +46 -0
  261. package/src/database/migrations/20251212000000_3_indexes.ts +82 -0
  262. package/src/database/pubsub.test.ts +92 -0
  263. package/src/database/pubsub.ts +92 -0
  264. package/src/execution.test.ts +508 -0
  265. package/src/execution.ts +291 -0
  266. package/src/index.ts +7 -0
  267. package/src/internal.ts +11 -0
  268. package/src/practices/01-remote-workflow.ts +61 -0
  269. package/src/registry.test.ts +122 -0
  270. package/src/registry.ts +65 -0
  271. package/src/testing/connection.ts +44 -0
  272. package/src/worker.test.ts +1138 -0
  273. package/src/worker.ts +281 -0
  274. package/src/workflow.test.ts +68 -0
  275. package/src/workflow.ts +84 -0
  276. package/table_ddl.sql +60 -0
  277. package/templates/openworkflow.config.ts +22 -0
  278. package/tsconfig.json +40 -0
  279. package/tsconfig.test.json +4 -0
  280. package/vite.config.ts +13 -0
@@ -0,0 +1,1163 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
3
+ import { declareWorkflow, OpenWorkflow } from "./client.js";
4
+ import { BackendPostgres } from "./database/backend.js";
5
+ import { KNEX_GLOBAL_CONFIG } from "./testing/connection.js";
6
+ describe("Worker", ()=>{
7
+ let backend;
8
+ beforeEach(async ()=>{
9
+ backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
10
+ namespaceId: randomUUID(),
11
+ runMigrations: false
12
+ });
13
+ });
14
+ afterEach(async ()=>{
15
+ await backend.stop();
16
+ });
17
+ test("passes workflow input to handlers (known slow test)", async ()=>{
18
+ const client = new OpenWorkflow({
19
+ backend
20
+ });
21
+ const workflow = client.defineWorkflow({
22
+ name: "context"
23
+ }, ({ input })=>input);
24
+ const worker = client.newWorker();
25
+ const payload = {
26
+ value: 10
27
+ };
28
+ const handle = await workflow.run(payload);
29
+ await worker.tick();
30
+ const result = await handle.result();
31
+ expect(result).toEqual(payload);
32
+ });
33
+ test("processes workflow runs to completion (known slow test)", async ()=>{
34
+ const client = new OpenWorkflow({
35
+ backend
36
+ });
37
+ const workflow = client.defineWorkflow({
38
+ name: "process"
39
+ }, ({ input })=>input.value * 2);
40
+ const worker = client.newWorker();
41
+ const handle = await workflow.run({
42
+ value: 21
43
+ });
44
+ await worker.tick();
45
+ const result = await handle.result();
46
+ expect(result).toBe(42);
47
+ });
48
+ test("step.run reuses cached results (known slow test)", async ()=>{
49
+ const client = new OpenWorkflow({
50
+ backend
51
+ });
52
+ let executionCount = 0;
53
+ const workflow = client.defineWorkflow({
54
+ name: "cached-step"
55
+ }, async ({ step })=>{
56
+ const first = await step.run({
57
+ name: "once"
58
+ }, ()=>{
59
+ executionCount++;
60
+ return "value";
61
+ });
62
+ const second = await step.run({
63
+ name: "once"
64
+ }, ()=>{
65
+ executionCount++;
66
+ return "should-not-run";
67
+ });
68
+ return {
69
+ first,
70
+ second
71
+ };
72
+ });
73
+ const worker = client.newWorker();
74
+ const handle = await workflow.run();
75
+ await worker.tick();
76
+ const result = await handle.result();
77
+ expect(result).toEqual({
78
+ first: "value",
79
+ second: "value"
80
+ });
81
+ expect(executionCount).toBe(1);
82
+ });
83
+ test("marks workflow for retry when definition is missing", async ()=>{
84
+ const client = new OpenWorkflow({
85
+ backend
86
+ });
87
+ const workflowRun = await backend.createWorkflowRun({
88
+ workflowName: "missing",
89
+ version: null,
90
+ idempotencyKey: null,
91
+ config: {},
92
+ context: null,
93
+ input: null,
94
+ availableAt: null,
95
+ deadlineAt: null
96
+ });
97
+ const worker = client.newWorker();
98
+ await worker.tick();
99
+ const updated = await backend.getWorkflowRun({
100
+ workflowRunId: workflowRun.id
101
+ });
102
+ expect(updated?.status).toBe("pending");
103
+ expect(updated?.error).toBeDefined();
104
+ expect(updated?.availableAt).not.toBeNull();
105
+ });
106
+ test("retries failed workflows automatically (known slow test)", async ()=>{
107
+ const client = new OpenWorkflow({
108
+ backend
109
+ });
110
+ let attemptCount = 0;
111
+ const workflow = client.defineWorkflow({
112
+ name: "retry-test"
113
+ }, ()=>{
114
+ attemptCount++;
115
+ if (attemptCount < 2) {
116
+ throw new Error(`Attempt ${String(attemptCount)} failed`);
117
+ }
118
+ return {
119
+ success: true,
120
+ attempts: attemptCount
121
+ };
122
+ });
123
+ const worker = client.newWorker();
124
+ // run the workflow
125
+ const handle = await workflow.run();
126
+ // first attempt - will fail and reschedule
127
+ await worker.tick();
128
+ await sleep(100); // wait for worker to finish
129
+ expect(attemptCount).toBe(1);
130
+ await sleep(1100); // wait for backoff delay
131
+ // second attempt - will succeed
132
+ await worker.tick();
133
+ await sleep(100); // wait for worker to finish
134
+ expect(attemptCount).toBe(2);
135
+ const result = await handle.result();
136
+ expect(result).toEqual({
137
+ success: true,
138
+ attempts: 2
139
+ });
140
+ });
141
+ test("tick is a no-op when no work is available", async ()=>{
142
+ const client = new OpenWorkflow({
143
+ backend
144
+ });
145
+ client.defineWorkflow({
146
+ name: "noop"
147
+ }, ()=>null);
148
+ const worker = client.newWorker();
149
+ await worker.tick(); // no runs queued
150
+ });
151
+ test("handles step functions that return undefined (known slow test)", async ()=>{
152
+ const client = new OpenWorkflow({
153
+ backend
154
+ });
155
+ const workflow = client.defineWorkflow({
156
+ name: "undefined-steps"
157
+ }, async ({ step })=>{
158
+ await step.run({
159
+ name: "step-1"
160
+ }, ()=>{
161
+ return; // explicit undefined
162
+ });
163
+ await step.run({
164
+ name: "step-2"
165
+ }, ()=>{
166
+ // implicit undefined
167
+ });
168
+ return {
169
+ success: true
170
+ };
171
+ });
172
+ const worker = client.newWorker();
173
+ const handle = await workflow.run();
174
+ await worker.tick();
175
+ const result = await handle.result();
176
+ expect(result).toEqual({
177
+ success: true
178
+ });
179
+ });
180
+ test("executes steps synchronously within workflow (known slow test)", async ()=>{
181
+ const client = new OpenWorkflow({
182
+ backend
183
+ });
184
+ const executionOrder = [];
185
+ const workflow = client.defineWorkflow({
186
+ name: "sync-steps"
187
+ }, async ({ step })=>{
188
+ executionOrder.push("start");
189
+ await step.run({
190
+ name: "step1"
191
+ }, ()=>{
192
+ executionOrder.push("step1");
193
+ return 1;
194
+ });
195
+ executionOrder.push("between");
196
+ await step.run({
197
+ name: "step2"
198
+ }, ()=>{
199
+ executionOrder.push("step2");
200
+ return 2;
201
+ });
202
+ executionOrder.push("end");
203
+ return executionOrder;
204
+ });
205
+ const worker = client.newWorker();
206
+ const handle = await workflow.run();
207
+ await worker.tick();
208
+ const result = await handle.result();
209
+ expect(result).toEqual([
210
+ "start",
211
+ "step1",
212
+ "between",
213
+ "step2",
214
+ "end"
215
+ ]);
216
+ });
217
+ test("executes parallel steps with Promise.all (known slow test)", async ()=>{
218
+ const client = new OpenWorkflow({
219
+ backend
220
+ });
221
+ const executionTimes = {};
222
+ const workflow = client.defineWorkflow({
223
+ name: "parallel"
224
+ }, async ({ step })=>{
225
+ const start = Date.now();
226
+ const [a, b, c] = await Promise.all([
227
+ step.run({
228
+ name: "step-a"
229
+ }, ()=>{
230
+ executionTimes["step-a"] = Date.now() - start;
231
+ return "a";
232
+ }),
233
+ step.run({
234
+ name: "step-b"
235
+ }, ()=>{
236
+ executionTimes["step-b"] = Date.now() - start;
237
+ return "b";
238
+ }),
239
+ step.run({
240
+ name: "step-c"
241
+ }, ()=>{
242
+ executionTimes["step-c"] = Date.now() - start;
243
+ return "c";
244
+ })
245
+ ]);
246
+ return {
247
+ a,
248
+ b,
249
+ c
250
+ };
251
+ });
252
+ const worker = client.newWorker();
253
+ const handle = await workflow.run();
254
+ await worker.tick();
255
+ const result = await handle.result();
256
+ expect(result).toEqual({
257
+ a: "a",
258
+ b: "b",
259
+ c: "c"
260
+ });
261
+ // steps should execute at roughly the same time (within 100ms)
262
+ const times = Object.values(executionTimes);
263
+ const maxTime = Math.max(...times);
264
+ const minTime = Math.min(...times);
265
+ expect(maxTime - minTime).toBeLessThan(100);
266
+ });
267
+ test("respects worker concurrency limit", async ()=>{
268
+ const client = new OpenWorkflow({
269
+ backend
270
+ });
271
+ const workflow = client.defineWorkflow({
272
+ name: "concurrency-test"
273
+ }, ()=>{
274
+ return "done";
275
+ });
276
+ const worker = client.newWorker({
277
+ concurrency: 2
278
+ });
279
+ // create 5 workflow runs, though only 2 (concurrency limit) should be
280
+ // completed per tick
281
+ const handles = await Promise.all([
282
+ workflow.run(),
283
+ workflow.run(),
284
+ workflow.run(),
285
+ workflow.run(),
286
+ workflow.run()
287
+ ]);
288
+ await worker.tick();
289
+ await sleep(100);
290
+ let completed = 0;
291
+ for (const handle of handles){
292
+ const run = await backend.getWorkflowRun({
293
+ workflowRunId: handle.workflowRun.id
294
+ });
295
+ if (run?.status === "completed") completed++;
296
+ }
297
+ expect(completed).toBe(2);
298
+ });
299
+ test("worker starts, processes work, and stops gracefully (known slow test)", async ()=>{
300
+ const client = new OpenWorkflow({
301
+ backend
302
+ });
303
+ const workflow = client.defineWorkflow({
304
+ name: "lifecycle"
305
+ }, ()=>{
306
+ return "complete";
307
+ });
308
+ const worker = client.newWorker();
309
+ await worker.start();
310
+ const handle = await workflow.run();
311
+ await sleep(200);
312
+ await worker.stop();
313
+ const result = await handle.result();
314
+ expect(result).toBe("complete");
315
+ });
316
+ test("recovers from crashes during parallel step execution (known slow test)", async ()=>{
317
+ const client = new OpenWorkflow({
318
+ backend
319
+ });
320
+ let attemptCount = 0;
321
+ const workflow = client.defineWorkflow({
322
+ name: "crash-recovery"
323
+ }, async ({ step })=>{
324
+ attemptCount++;
325
+ const [a, b] = await Promise.all([
326
+ step.run({
327
+ name: "step-a"
328
+ }, ()=>{
329
+ if (attemptCount > 1) return "x"; // should not happen since "a" will be cached
330
+ return "a";
331
+ }),
332
+ step.run({
333
+ name: "step-b"
334
+ }, ()=>{
335
+ if (attemptCount === 1) throw new Error("Simulated crash");
336
+ return "b";
337
+ })
338
+ ]);
339
+ return {
340
+ a,
341
+ b,
342
+ attempts: attemptCount
343
+ };
344
+ });
345
+ const worker = client.newWorker();
346
+ const handle = await workflow.run();
347
+ // first attempt will fail
348
+ await worker.tick();
349
+ await sleep(100);
350
+ expect(attemptCount).toBe(1);
351
+ // wait for backoff
352
+ await sleep(1100);
353
+ // second attempt should succeed
354
+ await worker.tick();
355
+ await sleep(100);
356
+ const result = await handle.result();
357
+ expect(result).toEqual({
358
+ a: "a",
359
+ b: "b",
360
+ attempts: 2
361
+ });
362
+ expect(attemptCount).toBe(2);
363
+ });
364
+ test("reclaims workflow run when heartbeat stops (known slow test)", async ()=>{
365
+ const client = new OpenWorkflow({
366
+ backend
367
+ });
368
+ const workflow = client.defineWorkflow({
369
+ name: "heartbeat-test"
370
+ }, ()=>"done");
371
+ const handle = await workflow.run();
372
+ const workerId = randomUUID();
373
+ const claimed = await backend.claimWorkflowRun({
374
+ workerId,
375
+ leaseDurationMs: 50
376
+ });
377
+ expect(claimed).not.toBeNull();
378
+ // let lease expire before starting worker
379
+ await sleep(100);
380
+ // worker should be able to reclaim
381
+ const worker = client.newWorker();
382
+ await worker.tick();
383
+ const result = await handle.result();
384
+ expect(result).toBe("done");
385
+ });
386
+ test("tick() returns count of claimed workflows", async ()=>{
387
+ const client = new OpenWorkflow({
388
+ backend
389
+ });
390
+ const workflow = client.defineWorkflow({
391
+ name: "count-test"
392
+ }, ()=>"result");
393
+ // enqueue 3 workflows
394
+ await workflow.run();
395
+ await workflow.run();
396
+ await workflow.run();
397
+ const worker = client.newWorker({
398
+ concurrency: 5
399
+ });
400
+ // first tick should claim 3 workflows (all available)
401
+ const claimed = await worker.tick();
402
+ expect(claimed).toBe(3);
403
+ // second tick should claim 0 (all already claimed)
404
+ const claimedAgain = await worker.tick();
405
+ expect(claimedAgain).toBe(0);
406
+ await worker.stop();
407
+ });
408
+ test("tick() respects concurrency limit", async ()=>{
409
+ const client = new OpenWorkflow({
410
+ backend
411
+ });
412
+ const workflow = client.defineWorkflow({
413
+ name: "concurrency-test"
414
+ }, async ()=>{
415
+ await sleep(100);
416
+ return "done";
417
+ });
418
+ // enqueue 10 workflows
419
+ for(let i = 0; i < 10; i++){
420
+ await workflow.run();
421
+ }
422
+ const worker = client.newWorker({
423
+ concurrency: 3
424
+ });
425
+ // first tick should claim exactly 3 (concurrency limit)
426
+ const claimed = await worker.tick();
427
+ expect(claimed).toBe(3);
428
+ // second tick should claim 0 (all slots occupied)
429
+ const claimedAgain = await worker.tick();
430
+ expect(claimedAgain).toBe(0);
431
+ await worker.stop();
432
+ });
433
+ test("worker only sleeps between claims when no work is available (known slow test)", async ()=>{
434
+ const client = new OpenWorkflow({
435
+ backend
436
+ });
437
+ const workflow = client.defineWorkflow({
438
+ name: "adaptive-test"
439
+ }, async ({ step })=>{
440
+ await step.run({
441
+ name: "step-1"
442
+ }, ()=>"done");
443
+ return "complete";
444
+ });
445
+ // enqueue many workflows
446
+ const handles = [];
447
+ for(let i = 0; i < 20; i++){
448
+ handles.push(await workflow.run());
449
+ }
450
+ const worker = client.newWorker({
451
+ concurrency: 5
452
+ });
453
+ const startTime = Date.now();
454
+ await worker.start();
455
+ // wait for all workflows to complete
456
+ await Promise.all(handles.map((h)=>h.result()));
457
+ await worker.stop();
458
+ const duration = Date.now() - startTime;
459
+ // with this conditional sleep, all workflows should complete quickly
460
+ // without it (with 100ms sleep between ticks), it would take much longer
461
+ expect(duration).toBeLessThan(3000); // should complete in under 3 seconds
462
+ });
463
+ test("only failed steps re-execute on retry (known slow test)", async ()=>{
464
+ const client = new OpenWorkflow({
465
+ backend
466
+ });
467
+ const executionCounts = {
468
+ stepA: 0,
469
+ stepB: 0,
470
+ stepC: 0
471
+ };
472
+ const workflow = client.defineWorkflow({
473
+ name: "mixed-retry"
474
+ }, async ({ step })=>{
475
+ const a = await step.run({
476
+ name: "step-a"
477
+ }, ()=>{
478
+ executionCounts.stepA++;
479
+ return "a-result";
480
+ });
481
+ const b = await step.run({
482
+ name: "step-b"
483
+ }, ()=>{
484
+ executionCounts.stepB++;
485
+ if (executionCounts.stepB === 1) {
486
+ throw new Error("Step B fails on first attempt");
487
+ }
488
+ return "b-result";
489
+ });
490
+ const c = await step.run({
491
+ name: "step-c"
492
+ }, ()=>{
493
+ executionCounts.stepC++;
494
+ return "c-result";
495
+ });
496
+ return {
497
+ a,
498
+ b,
499
+ c
500
+ };
501
+ });
502
+ const worker = client.newWorker();
503
+ const handle = await workflow.run();
504
+ // first workflow attempt
505
+ // - step-a succeeds
506
+ // - step-b fails
507
+ // - step-c never runs (workflow fails at step-b)
508
+ await worker.tick();
509
+ await sleep(100);
510
+ expect(executionCounts.stepA).toBe(1);
511
+ expect(executionCounts.stepB).toBe(1);
512
+ expect(executionCounts.stepC).toBe(0);
513
+ // wait for backoff
514
+ await sleep(1100);
515
+ // second workflow attempt
516
+ // - step-a should be cached (not re-executed)
517
+ // - step-b should be re-executed (failed previously)
518
+ // - step-c should execute for first time
519
+ await worker.tick();
520
+ await sleep(100);
521
+ expect(executionCounts.stepA).toBe(1); // still 1, was cached
522
+ expect(executionCounts.stepB).toBe(2); // incremented, was retried
523
+ expect(executionCounts.stepC).toBe(1); // incremented, first execution
524
+ const result = await handle.result();
525
+ expect(result).toEqual({
526
+ a: "a-result",
527
+ b: "b-result",
528
+ c: "c-result"
529
+ });
530
+ });
531
+ test("step.sleep postpones workflow execution (known slow test)", async ()=>{
532
+ const client = new OpenWorkflow({
533
+ backend
534
+ });
535
+ let stepCount = 0;
536
+ const workflow = client.defineWorkflow({
537
+ name: "sleep-test"
538
+ }, async ({ step })=>{
539
+ const before = await step.run({
540
+ name: "before-sleep"
541
+ }, ()=>{
542
+ stepCount++;
543
+ return "before";
544
+ });
545
+ await step.sleep("pause", "100ms");
546
+ const after = await step.run({
547
+ name: "after-sleep"
548
+ }, ()=>{
549
+ stepCount++;
550
+ return "after";
551
+ });
552
+ return {
553
+ before,
554
+ after
555
+ };
556
+ });
557
+ const worker = client.newWorker();
558
+ const handle = await workflow.run();
559
+ // first execution - runs before-sleep, then sleeps
560
+ await worker.tick();
561
+ await sleep(50); // wait for processing
562
+ expect(stepCount).toBe(1);
563
+ // verify workflow was postponed with sleeping status
564
+ const slept = await backend.getWorkflowRun({
565
+ workflowRunId: handle.workflowRun.id
566
+ });
567
+ expect(slept?.status).toBe("sleeping");
568
+ expect(slept?.workerId).toBeNull(); // released during sleep
569
+ expect(slept?.availableAt).not.toBeNull();
570
+ if (!slept?.availableAt) throw new Error("availableAt should be set");
571
+ const delayMs = slept.availableAt.getTime() - Date.now();
572
+ expect(delayMs).toBeGreaterThan(0);
573
+ expect(delayMs).toBeLessThan(150); // should be ~100ms
574
+ // verify sleep step is in "running" state during sleep
575
+ const attempts = await backend.listStepAttempts({
576
+ workflowRunId: handle.workflowRun.id
577
+ });
578
+ const sleepStep = attempts.data.find((a)=>a.stepName === "pause");
579
+ expect(sleepStep?.status).toBe("running");
580
+ // wait for sleep duration
581
+ await sleep(150);
582
+ // second execution (after sleep)
583
+ await worker.tick();
584
+ await sleep(50); // wait for processing
585
+ expect(stepCount).toBe(2);
586
+ // verify sleep step is now "completed"
587
+ const refreshedAttempts = await backend.listStepAttempts({
588
+ workflowRunId: handle.workflowRun.id
589
+ });
590
+ const completedSleepStep = refreshedAttempts.data.find((a)=>a.stepName === "pause");
591
+ expect(completedSleepStep?.status).toBe("completed");
592
+ const result = await handle.result();
593
+ expect(result).toEqual({
594
+ before: "before",
595
+ after: "after"
596
+ });
597
+ });
598
+ test("step.sleep is cached on replay", async ()=>{
599
+ const client = new OpenWorkflow({
600
+ backend
601
+ });
602
+ let step1Count = 0;
603
+ let step2Count = 0;
604
+ const workflow = client.defineWorkflow({
605
+ name: "sleep-cache-test"
606
+ }, async ({ step })=>{
607
+ await step.run({
608
+ name: "step-1"
609
+ }, ()=>{
610
+ step1Count++;
611
+ return "one";
612
+ });
613
+ // this should only postpone once
614
+ await step.sleep("wait", "50ms");
615
+ await step.run({
616
+ name: "step-2"
617
+ }, ()=>{
618
+ step2Count++;
619
+ return "two";
620
+ });
621
+ return "done";
622
+ });
623
+ const worker = client.newWorker();
624
+ const handle = await workflow.run();
625
+ // first attempt: execute step-1, then sleep (step-2 not executed)
626
+ await worker.tick();
627
+ await sleep(50);
628
+ expect(step1Count).toBe(1);
629
+ expect(step2Count).toBe(0);
630
+ await sleep(100); // wait for sleep to complete
631
+ // second attempt: step-1 is cached (not re-executed), sleep is cached, step-2 executes
632
+ await worker.tick();
633
+ await sleep(50);
634
+ expect(step1Count).toBe(1); // still 1, was cached
635
+ expect(step2Count).toBe(1); // now 1, executed after sleep
636
+ const result = await handle.result();
637
+ expect(result).toBe("done");
638
+ });
639
+ test("step.sleep throws error for invalid duration format", async ()=>{
640
+ const client = new OpenWorkflow({
641
+ backend
642
+ });
643
+ const workflow = client.defineWorkflow({
644
+ name: "invalid-duration"
645
+ }, async ({ step })=>{
646
+ // @ts-expect-error - testing invalid duration
647
+ await step.sleep("bad", "invalid");
648
+ return "should-not-reach";
649
+ });
650
+ const worker = client.newWorker();
651
+ const handle = await workflow.run();
652
+ await worker.tick();
653
+ await sleep(100);
654
+ const failed = await backend.getWorkflowRun({
655
+ workflowRunId: handle.workflowRun.id
656
+ });
657
+ expect(failed?.status).toBe("pending"); // should be retrying
658
+ expect(failed?.error).toBeDefined();
659
+ expect(failed?.error?.message).toContain("Invalid duration format");
660
+ });
661
+ test("step.sleep handles multiple sequential sleeps (known slow test)", async ()=>{
662
+ const client = new OpenWorkflow({
663
+ backend
664
+ });
665
+ let executionCount = 0;
666
+ const workflow = client.defineWorkflow({
667
+ name: "sequential-sleeps"
668
+ }, async ({ step })=>{
669
+ executionCount++;
670
+ await step.run({
671
+ name: "step-1"
672
+ }, ()=>"one");
673
+ await step.sleep("sleep-1", "50ms");
674
+ await step.run({
675
+ name: "step-2"
676
+ }, ()=>"two");
677
+ await step.sleep("sleep-2", "50ms");
678
+ await step.run({
679
+ name: "step-3"
680
+ }, ()=>"three");
681
+ return "done";
682
+ });
683
+ const worker = client.newWorker();
684
+ const handle = await workflow.run();
685
+ // first execution: step-1, then sleep-1
686
+ await worker.tick();
687
+ await sleep(50);
688
+ expect(executionCount).toBe(1);
689
+ // verify first sleep is running
690
+ const attempts1 = await backend.listStepAttempts({
691
+ workflowRunId: handle.workflowRun.id
692
+ });
693
+ expect(attempts1.data.find((a)=>a.stepName === "sleep-1")?.status).toBe("running");
694
+ // wait for first sleep
695
+ await sleep(100);
696
+ // second execution: sleep-1 completed, step-2, then sleep-2
697
+ await worker.tick();
698
+ await sleep(50);
699
+ expect(executionCount).toBe(2);
700
+ // verify second sleep is running
701
+ const attempts2 = await backend.listStepAttempts({
702
+ workflowRunId: handle.workflowRun.id
703
+ });
704
+ expect(attempts2.data.find((a)=>a.stepName === "sleep-1")?.status).toBe("completed");
705
+ expect(attempts2.data.find((a)=>a.stepName === "sleep-2")?.status).toBe("running");
706
+ // wait for second sleep
707
+ await sleep(100);
708
+ // third execution: sleep-2 completed, step-3, complete
709
+ await worker.tick();
710
+ await sleep(50);
711
+ expect(executionCount).toBe(3);
712
+ const result = await handle.result();
713
+ expect(result).toBe("done");
714
+ // verify all steps completed
715
+ const finalAttempts = await backend.listStepAttempts({
716
+ workflowRunId: handle.workflowRun.id
717
+ });
718
+ expect(finalAttempts.data.length).toBe(5); // 3 regular steps + 2 sleeps
719
+ expect(finalAttempts.data.every((a)=>a.status === "completed")).toBe(true);
720
+ });
721
+ test("sleeping workflows can be claimed after availableAt", async ()=>{
722
+ const client = new OpenWorkflow({
723
+ backend
724
+ });
725
+ const workflow = client.defineWorkflow({
726
+ name: "sleeping-claim-test"
727
+ }, async ({ step })=>{
728
+ await step.run({
729
+ name: "before"
730
+ }, ()=>"before");
731
+ await step.sleep("wait", "100ms");
732
+ await step.run({
733
+ name: "after"
734
+ }, ()=>"after");
735
+ return "done";
736
+ });
737
+ const worker = client.newWorker();
738
+ const handle = await workflow.run();
739
+ // first execution - sleep
740
+ await worker.tick();
741
+ await sleep(50);
742
+ // verify workflow is in sleeping state
743
+ const sleeping = await backend.getWorkflowRun({
744
+ workflowRunId: handle.workflowRun.id
745
+ });
746
+ expect(sleeping?.status).toBe("sleeping");
747
+ expect(sleeping?.workerId).toBeNull();
748
+ // wait for sleep duration
749
+ await sleep(100);
750
+ // verify workflow can be claimed again
751
+ const claimed = await backend.claimWorkflowRun({
752
+ workerId: "test-worker",
753
+ leaseDurationMs: 30_000
754
+ });
755
+ expect(claimed?.id).toBe(handle.workflowRun.id);
756
+ expect(claimed?.status).toBe("running");
757
+ expect(claimed?.workerId).toBe("test-worker");
758
+ });
759
+ test("sleep is not skipped when worker crashes after creating sleep step but before marking workflow as sleeping (known slow test)", async ()=>{
760
+ const client = new OpenWorkflow({
761
+ backend
762
+ });
763
+ let executionCount = 0;
764
+ let beforeSleepCount = 0;
765
+ let afterSleepCount = 0;
766
+ const workflow = client.defineWorkflow({
767
+ name: "crash-during-sleep"
768
+ }, async ({ step })=>{
769
+ executionCount++;
770
+ await step.run({
771
+ name: "before-sleep"
772
+ }, ()=>{
773
+ beforeSleepCount++;
774
+ return "before";
775
+ });
776
+ // this sleep should NOT be skipped even if crash happens
777
+ await step.sleep("critical-pause", "200ms");
778
+ await step.run({
779
+ name: "after-sleep"
780
+ }, ()=>{
781
+ afterSleepCount++;
782
+ return "after";
783
+ });
784
+ return {
785
+ executionCount,
786
+ beforeSleepCount,
787
+ afterSleepCount
788
+ };
789
+ });
790
+ const handle = await workflow.run();
791
+ // first worker processes the workflow until sleep
792
+ const worker1 = client.newWorker();
793
+ await worker1.tick();
794
+ await sleep(100);
795
+ const workflowAfterFirst = await backend.getWorkflowRun({
796
+ workflowRunId: handle.workflowRun.id
797
+ });
798
+ expect(workflowAfterFirst?.status).toBe("sleeping");
799
+ const attemptsAfterFirst = await backend.listStepAttempts({
800
+ workflowRunId: handle.workflowRun.id
801
+ });
802
+ const sleepStep = attemptsAfterFirst.data.find((a)=>a.stepName === "critical-pause");
803
+ expect(sleepStep).toBeDefined();
804
+ expect(sleepStep?.kind).toBe("sleep");
805
+ expect(sleepStep?.status).toBe("running");
806
+ await sleep(50); // only 50ms of the 200ms sleep
807
+ // if there's a running sleep step, the workflow should be properly
808
+ // transitioned to sleeping
809
+ const worker2 = client.newWorker();
810
+ await worker2.tick();
811
+ // after-sleep step should NOT have executed yet
812
+ expect(afterSleepCount).toBe(0);
813
+ // wait for the full sleep duration to elapse then check to make sure
814
+ // workflow is claimable and resume
815
+ await sleep(200);
816
+ await worker2.tick();
817
+ await sleep(100);
818
+ expect(afterSleepCount).toBe(1);
819
+ const result = await handle.result();
820
+ expect(result.afterSleepCount).toBe(1);
821
+ });
822
+ test("version enables conditional code paths (known slow test)", async ()=>{
823
+ const client = new OpenWorkflow({
824
+ backend
825
+ });
826
+ const workflow = client.defineWorkflow({
827
+ name: "conditional-workflow",
828
+ version: "v2"
829
+ }, async ({ version, step })=>{
830
+ return version === "v1" ? await step.run({
831
+ name: "old-step"
832
+ }, ()=>"old-logic") : await step.run({
833
+ name: "new-step"
834
+ }, ()=>"new-logic");
835
+ });
836
+ const worker = client.newWorker();
837
+ const handle = await workflow.run();
838
+ await worker.tick();
839
+ const result = await handle.result();
840
+ expect(result).toBe("new-logic");
841
+ });
842
+ test("workflow version is null when not specified", async ()=>{
843
+ const client = new OpenWorkflow({
844
+ backend
845
+ });
846
+ const workflow = client.defineWorkflow({
847
+ name: "unversioned-workflow"
848
+ }, async ({ version, step })=>{
849
+ const result = await step.run({
850
+ name: "check-version"
851
+ }, ()=>{
852
+ return {
853
+ version
854
+ };
855
+ });
856
+ return result;
857
+ });
858
+ const worker = client.newWorker();
859
+ const handle = await workflow.run();
860
+ await worker.tick();
861
+ const result = await handle.result();
862
+ expect(result.version).toBeNull();
863
+ });
864
+ test("cancels a pending workflow", async ()=>{
865
+ const client = new OpenWorkflow({
866
+ backend
867
+ });
868
+ const workflow = client.defineWorkflow({
869
+ name: "cancel-pending"
870
+ }, async ({ step })=>{
871
+ await step.run({
872
+ name: "step-1"
873
+ }, ()=>"result");
874
+ return {
875
+ completed: true
876
+ };
877
+ });
878
+ const handle = await workflow.run();
879
+ // cancel before worker processes it
880
+ await handle.cancel();
881
+ const workflowRun = await backend.getWorkflowRun({
882
+ workflowRunId: handle.workflowRun.id
883
+ });
884
+ expect(workflowRun?.status).toBe("canceled");
885
+ expect(workflowRun?.finishedAt).not.toBeNull();
886
+ expect(workflowRun?.availableAt).toBeNull();
887
+ expect(workflowRun?.workerId).toBeNull();
888
+ });
889
+ test("cancels a sleeping workflow", async ()=>{
890
+ const client = new OpenWorkflow({
891
+ backend
892
+ });
893
+ const workflow = client.defineWorkflow({
894
+ name: "cancel-sleeping"
895
+ }, async ({ step })=>{
896
+ await step.sleep("sleep-1", "1h");
897
+ return {
898
+ completed: true
899
+ };
900
+ });
901
+ const worker = client.newWorker();
902
+ const handle = await workflow.run();
903
+ await worker.tick();
904
+ // cancel while sleeping
905
+ await handle.cancel();
906
+ const canceled = await backend.getWorkflowRun({
907
+ workflowRunId: handle.workflowRun.id
908
+ });
909
+ expect(canceled?.status).toBe("canceled");
910
+ expect(canceled?.finishedAt).not.toBeNull();
911
+ expect(canceled?.availableAt).toBeNull();
912
+ expect(canceled?.workerId).toBeNull();
913
+ });
914
+ test("cannot cancel a completed workflow", async ()=>{
915
+ const client = new OpenWorkflow({
916
+ backend
917
+ });
918
+ const workflow = client.defineWorkflow({
919
+ name: "cancel-completed"
920
+ }, ()=>({
921
+ completed: true
922
+ }));
923
+ const worker = client.newWorker();
924
+ const handle = await workflow.run();
925
+ await worker.tick();
926
+ const result = await handle.result();
927
+ expect(result.completed).toBe(true);
928
+ // try to cancel after success
929
+ await expect(handle.cancel()).rejects.toThrow(/Cannot cancel workflow run .* with status completed/);
930
+ });
931
+ test("cannot cancel a failed workflow", async ()=>{
932
+ const client = new OpenWorkflow({
933
+ backend
934
+ });
935
+ const workflow = client.defineWorkflow({
936
+ name: "cancel-failed"
937
+ }, ()=>{
938
+ throw new Error("intentional failure");
939
+ });
940
+ const worker = client.newWorker();
941
+ const handle = await workflow.run({
942
+ value: 1
943
+ }, {
944
+ deadlineAt: new Date()
945
+ });
946
+ await worker.tick();
947
+ // wait for it to fail due to deadline
948
+ await sleep(100);
949
+ const failed = await backend.getWorkflowRun({
950
+ workflowRunId: handle.workflowRun.id
951
+ });
952
+ expect(failed?.status).toBe("failed");
953
+ // try to cancel after failure
954
+ await expect(handle.cancel()).rejects.toThrow(/Cannot cancel workflow run .* with status failed/);
955
+ });
956
+ test("cannot cancel non-existent workflow", async ()=>{
957
+ await expect(backend.cancelWorkflowRun({
958
+ workflowRunId: "non-existent-id"
959
+ })).rejects.toThrow(/Workflow run non-existent-id does not exist/);
960
+ });
961
+ test("worker handles when canceled workflow during execution", async ()=>{
962
+ const client = new OpenWorkflow({
963
+ backend
964
+ });
965
+ let stepExecuted = false;
966
+ const workflow = client.defineWorkflow({
967
+ name: "cancel-during-execution"
968
+ }, async ({ step })=>{
969
+ await step.run({
970
+ name: "step-1"
971
+ }, async ()=>{
972
+ stepExecuted = true;
973
+ // simulate some work
974
+ await sleep(50);
975
+ return "result";
976
+ });
977
+ return {
978
+ completed: true
979
+ };
980
+ });
981
+ const worker = client.newWorker();
982
+ const handle = await workflow.run();
983
+ // start processing in the background
984
+ const tickPromise = worker.tick();
985
+ await sleep(25);
986
+ // cancel while step is executing
987
+ await handle.cancel();
988
+ // wait for tick to complete
989
+ await tickPromise;
990
+ // step should have been executed but workflow should be canceled
991
+ expect(stepExecuted).toBe(true);
992
+ const canceled = await backend.getWorkflowRun({
993
+ workflowRunId: handle.workflowRun.id
994
+ });
995
+ expect(canceled?.status).toBe("canceled");
996
+ });
997
+ test("result() rejects for canceled workflows", async ()=>{
998
+ const client = new OpenWorkflow({
999
+ backend
1000
+ });
1001
+ const workflow = client.defineWorkflow({
1002
+ name: "cancel-result"
1003
+ }, async ({ step })=>{
1004
+ await step.sleep("sleep-1", "1h");
1005
+ return {
1006
+ completed: true
1007
+ };
1008
+ });
1009
+ const handle = await workflow.run();
1010
+ await handle.cancel();
1011
+ await expect(handle.result()).rejects.toThrow(/Workflow cancel-result was canceled/);
1012
+ });
1013
+ describe("version matching", ()=>{
1014
+ test("worker matches workflow runs by version", async ()=>{
1015
+ const client = new OpenWorkflow({
1016
+ backend
1017
+ });
1018
+ client.defineWorkflow({
1019
+ name: "versioned-workflow",
1020
+ version: "v1"
1021
+ }, async ({ step })=>{
1022
+ return await step.run({
1023
+ name: "compute"
1024
+ }, ()=>"v1-result");
1025
+ });
1026
+ client.defineWorkflow({
1027
+ name: "versioned-workflow",
1028
+ version: "v2"
1029
+ }, async ({ step })=>{
1030
+ return await step.run({
1031
+ name: "compute"
1032
+ }, ()=>"v2-result");
1033
+ });
1034
+ const worker = client.newWorker({
1035
+ concurrency: 2
1036
+ });
1037
+ const v1Spec = declareWorkflow({
1038
+ name: "versioned-workflow",
1039
+ version: "v1"
1040
+ });
1041
+ const v2Spec = declareWorkflow({
1042
+ name: "versioned-workflow",
1043
+ version: "v2"
1044
+ });
1045
+ const handleV1 = await client.runWorkflow(v1Spec);
1046
+ const handleV2 = await client.runWorkflow(v2Spec);
1047
+ await worker.tick();
1048
+ await sleep(100); // wait for background execution
1049
+ const resultV1 = await handleV1.result();
1050
+ const resultV2 = await handleV2.result();
1051
+ expect(resultV1).toBe("v1-result");
1052
+ expect(resultV2).toBe("v2-result");
1053
+ });
1054
+ test("worker fails workflow run when version is not registered", async ()=>{
1055
+ const client = new OpenWorkflow({
1056
+ backend
1057
+ });
1058
+ client.defineWorkflow({
1059
+ name: "version-check",
1060
+ version: "v1"
1061
+ }, ()=>"v1-result");
1062
+ const worker = client.newWorker();
1063
+ const workflowRun = await backend.createWorkflowRun({
1064
+ workflowName: "version-check",
1065
+ version: "v2",
1066
+ idempotencyKey: null,
1067
+ config: {},
1068
+ context: null,
1069
+ input: null,
1070
+ availableAt: null,
1071
+ deadlineAt: null
1072
+ });
1073
+ await worker.tick();
1074
+ const updated = await backend.getWorkflowRun({
1075
+ workflowRunId: workflowRun.id
1076
+ });
1077
+ expect(updated?.status).toBe("pending");
1078
+ expect(updated?.error).toEqual({
1079
+ message: 'Workflow "version-check" (version: v2) is not registered'
1080
+ });
1081
+ });
1082
+ test("unversioned workflow does not match versioned run", async ()=>{
1083
+ const client = new OpenWorkflow({
1084
+ backend
1085
+ });
1086
+ client.defineWorkflow({
1087
+ name: "version-mismatch"
1088
+ }, ()=>"unversioned-result");
1089
+ const worker = client.newWorker();
1090
+ const workflowRun = await backend.createWorkflowRun({
1091
+ workflowName: "version-mismatch",
1092
+ version: "v1",
1093
+ idempotencyKey: null,
1094
+ config: {},
1095
+ context: null,
1096
+ input: null,
1097
+ availableAt: null,
1098
+ deadlineAt: null
1099
+ });
1100
+ await worker.tick();
1101
+ const updated = await backend.getWorkflowRun({
1102
+ workflowRunId: workflowRun.id
1103
+ });
1104
+ expect(updated?.status).toBe("pending");
1105
+ expect(updated?.error).toEqual({
1106
+ message: 'Workflow "version-mismatch" (version: v1) is not registered'
1107
+ });
1108
+ });
1109
+ test("versioned workflow does not match unversioned run", async ()=>{
1110
+ const client = new OpenWorkflow({
1111
+ backend
1112
+ });
1113
+ client.defineWorkflow({
1114
+ name: "version-required",
1115
+ version: "v1"
1116
+ }, ()=>"v1-result");
1117
+ const worker = client.newWorker();
1118
+ const workflowRun = await backend.createWorkflowRun({
1119
+ workflowName: "version-required",
1120
+ version: null,
1121
+ idempotencyKey: null,
1122
+ config: {},
1123
+ context: null,
1124
+ input: null,
1125
+ availableAt: null,
1126
+ deadlineAt: null
1127
+ });
1128
+ await worker.tick();
1129
+ const updated = await backend.getWorkflowRun({
1130
+ workflowRunId: workflowRun.id
1131
+ });
1132
+ expect(updated?.status).toBe("pending");
1133
+ expect(updated?.error).toEqual({
1134
+ message: 'Workflow "version-required" is not registered'
1135
+ });
1136
+ });
1137
+ test("workflow receives run's version, not registered version", async ()=>{
1138
+ // this test verifies that the version passed to the workflow function
1139
+ // is the one from the workflow run, not the registered workflow
1140
+ const client = new OpenWorkflow({
1141
+ backend
1142
+ });
1143
+ const workflow = client.defineWorkflow({
1144
+ name: "version-in-handler",
1145
+ version: "v1"
1146
+ }, async ({ version, step })=>{
1147
+ return await step.run({
1148
+ name: "get-version"
1149
+ }, ()=>version);
1150
+ });
1151
+ const worker = client.newWorker();
1152
+ const handle = await workflow.run();
1153
+ await worker.tick();
1154
+ const result = await handle.result();
1155
+ expect(result).toBe("v1");
1156
+ });
1157
+ });
1158
+ });
1159
+ function sleep(ms) {
1160
+ return new Promise((resolve)=>setTimeout(resolve, ms));
1161
+ }
1162
+
1163
+ //# sourceMappingURL=worker.test.js.map