@sonamu-kit/tasks 0.2.0 → 0.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 (215) hide show
  1. package/.oxlintrc.json +3 -0
  2. package/AGENTS.md +21 -0
  3. package/dist/backend.d.ts +126 -107
  4. package/dist/backend.d.ts.map +1 -1
  5. package/dist/backend.js +4 -1
  6. package/dist/backend.js.map +1 -1
  7. package/dist/client.d.ts +145 -132
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +219 -213
  10. package/dist/client.js.map +1 -1
  11. package/dist/config.d.ts +15 -8
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +22 -17
  14. package/dist/config.js.map +1 -1
  15. package/dist/core/duration.d.ts +5 -4
  16. package/dist/core/duration.d.ts.map +1 -1
  17. package/dist/core/duration.js +54 -59
  18. package/dist/core/duration.js.map +1 -1
  19. package/dist/core/error.d.ts +10 -7
  20. package/dist/core/error.d.ts.map +1 -1
  21. package/dist/core/error.js +21 -21
  22. package/dist/core/error.js.map +1 -1
  23. package/dist/core/json.d.ts +8 -3
  24. package/dist/core/json.d.ts.map +1 -1
  25. package/dist/core/result.d.ts +10 -14
  26. package/dist/core/result.d.ts.map +1 -1
  27. package/dist/core/result.js +21 -16
  28. package/dist/core/result.js.map +1 -1
  29. package/dist/core/retry.d.ts +37 -31
  30. package/dist/core/retry.d.ts.map +1 -1
  31. package/dist/core/retry.js +44 -51
  32. package/dist/core/retry.js.map +1 -1
  33. package/dist/core/schema.d.ts +57 -53
  34. package/dist/core/schema.d.ts.map +1 -1
  35. package/dist/core/step.d.ts +28 -78
  36. package/dist/core/step.d.ts.map +1 -1
  37. package/dist/core/step.js +53 -63
  38. package/dist/core/step.js.map +1 -1
  39. package/dist/core/workflow.d.ts +33 -61
  40. package/dist/core/workflow.d.ts.map +1 -1
  41. package/dist/core/workflow.js +31 -41
  42. package/dist/core/workflow.js.map +1 -1
  43. package/dist/database/backend.d.ts +53 -46
  44. package/dist/database/backend.d.ts.map +1 -1
  45. package/dist/database/backend.js +544 -577
  46. package/dist/database/backend.js.map +1 -1
  47. package/dist/database/base.js +48 -25
  48. package/dist/database/base.js.map +1 -1
  49. package/dist/database/migrations/20251212000000_0_init.d.ts +10 -0
  50. package/dist/database/migrations/20251212000000_0_init.d.ts.map +1 -0
  51. package/dist/database/migrations/20251212000000_0_init.js +8 -4
  52. package/dist/database/migrations/20251212000000_0_init.js.map +1 -1
  53. package/dist/database/migrations/20251212000000_1_tables.d.ts +10 -0
  54. package/dist/database/migrations/20251212000000_1_tables.d.ts.map +1 -0
  55. package/dist/database/migrations/20251212000000_1_tables.js +81 -83
  56. package/dist/database/migrations/20251212000000_1_tables.js.map +1 -1
  57. package/dist/database/migrations/20251212000000_2_fk.d.ts +10 -0
  58. package/dist/database/migrations/20251212000000_2_fk.d.ts.map +1 -0
  59. package/dist/database/migrations/20251212000000_2_fk.js +20 -43
  60. package/dist/database/migrations/20251212000000_2_fk.js.map +1 -1
  61. package/dist/database/migrations/20251212000000_3_indexes.d.ts +10 -0
  62. package/dist/database/migrations/20251212000000_3_indexes.d.ts.map +1 -0
  63. package/dist/database/migrations/20251212000000_3_indexes.js +88 -102
  64. package/dist/database/migrations/20251212000000_3_indexes.js.map +1 -1
  65. package/dist/database/pubsub.d.ts +7 -16
  66. package/dist/database/pubsub.d.ts.map +1 -1
  67. package/dist/database/pubsub.js +75 -73
  68. package/dist/database/pubsub.js.map +1 -1
  69. package/dist/execution.d.ts +20 -59
  70. package/dist/execution.d.ts.map +1 -1
  71. package/dist/execution.js +175 -188
  72. package/dist/execution.js.map +1 -1
  73. package/dist/index.d.ts +5 -8
  74. package/dist/index.js +5 -5
  75. package/dist/internal.d.ts +12 -13
  76. package/dist/internal.js +4 -4
  77. package/dist/registry.d.ts +33 -27
  78. package/dist/registry.d.ts.map +1 -1
  79. package/dist/registry.js +58 -49
  80. package/dist/registry.js.map +1 -1
  81. package/dist/worker.d.ts +57 -50
  82. package/dist/worker.d.ts.map +1 -1
  83. package/dist/worker.js +194 -199
  84. package/dist/worker.js.map +1 -1
  85. package/dist/workflow.d.ts +26 -30
  86. package/dist/workflow.d.ts.map +1 -1
  87. package/dist/workflow.js +20 -15
  88. package/dist/workflow.js.map +1 -1
  89. package/nodemon.json +1 -1
  90. package/package.json +17 -19
  91. package/src/backend.ts +25 -9
  92. package/src/chaos.test.ts +3 -1
  93. package/src/client.test.ts +2 -0
  94. package/src/client.ts +30 -8
  95. package/src/config.test.ts +1 -0
  96. package/src/config.ts +3 -2
  97. package/src/core/duration.test.ts +2 -1
  98. package/src/core/duration.ts +1 -1
  99. package/src/core/error.test.ts +1 -0
  100. package/src/core/error.ts +1 -1
  101. package/src/core/result.test.ts +1 -0
  102. package/src/core/retry.test.ts +3 -2
  103. package/src/core/retry.ts +1 -1
  104. package/src/core/schema.ts +2 -2
  105. package/src/core/step.test.ts +2 -1
  106. package/src/core/step.ts +4 -3
  107. package/src/core/workflow.test.ts +2 -1
  108. package/src/core/workflow.ts +4 -3
  109. package/src/database/backend.test.ts +1 -0
  110. package/src/database/backend.testsuite.ts +44 -40
  111. package/src/database/backend.ts +207 -25
  112. package/src/database/base.test.ts +41 -0
  113. package/src/database/base.ts +51 -2
  114. package/src/database/migrations/20251212000000_0_init.ts +2 -1
  115. package/src/database/migrations/20251212000000_1_tables.ts +2 -1
  116. package/src/database/migrations/20251212000000_2_fk.ts +2 -1
  117. package/src/database/migrations/20251212000000_3_indexes.ts +2 -1
  118. package/src/database/pubsub.test.ts +6 -3
  119. package/src/database/pubsub.ts +55 -33
  120. package/src/execution.test.ts +2 -0
  121. package/src/execution.ts +49 -10
  122. package/src/internal.ts +15 -15
  123. package/src/practices/01-remote-workflow.ts +1 -0
  124. package/src/registry.test.ts +1 -0
  125. package/src/registry.ts +1 -1
  126. package/src/testing/connection.ts +3 -1
  127. package/src/worker.test.ts +2 -0
  128. package/src/worker.ts +30 -9
  129. package/src/workflow.test.ts +1 -0
  130. package/src/workflow.ts +3 -3
  131. package/templates/openworkflow.config.ts +2 -1
  132. package/tsdown.config.ts +31 -0
  133. package/.swcrc +0 -17
  134. package/dist/chaos.test.d.ts +0 -2
  135. package/dist/chaos.test.d.ts.map +0 -1
  136. package/dist/chaos.test.js +0 -92
  137. package/dist/chaos.test.js.map +0 -1
  138. package/dist/client.test.d.ts +0 -2
  139. package/dist/client.test.d.ts.map +0 -1
  140. package/dist/client.test.js +0 -340
  141. package/dist/client.test.js.map +0 -1
  142. package/dist/config.test.d.ts +0 -2
  143. package/dist/config.test.d.ts.map +0 -1
  144. package/dist/config.test.js +0 -24
  145. package/dist/config.test.js.map +0 -1
  146. package/dist/core/duration.test.d.ts +0 -2
  147. package/dist/core/duration.test.d.ts.map +0 -1
  148. package/dist/core/duration.test.js +0 -265
  149. package/dist/core/duration.test.js.map +0 -1
  150. package/dist/core/error.test.d.ts +0 -2
  151. package/dist/core/error.test.d.ts.map +0 -1
  152. package/dist/core/error.test.js +0 -63
  153. package/dist/core/error.test.js.map +0 -1
  154. package/dist/core/json.js +0 -3
  155. package/dist/core/json.js.map +0 -1
  156. package/dist/core/result.test.d.ts +0 -2
  157. package/dist/core/result.test.d.ts.map +0 -1
  158. package/dist/core/result.test.js +0 -19
  159. package/dist/core/result.test.js.map +0 -1
  160. package/dist/core/retry.test.d.ts +0 -2
  161. package/dist/core/retry.test.d.ts.map +0 -1
  162. package/dist/core/retry.test.js +0 -198
  163. package/dist/core/retry.test.js.map +0 -1
  164. package/dist/core/schema.js +0 -4
  165. package/dist/core/schema.js.map +0 -1
  166. package/dist/core/step.test.d.ts +0 -2
  167. package/dist/core/step.test.d.ts.map +0 -1
  168. package/dist/core/step.test.js +0 -356
  169. package/dist/core/step.test.js.map +0 -1
  170. package/dist/core/workflow.test.d.ts +0 -2
  171. package/dist/core/workflow.test.d.ts.map +0 -1
  172. package/dist/core/workflow.test.js +0 -172
  173. package/dist/core/workflow.test.js.map +0 -1
  174. package/dist/database/backend.test.d.ts +0 -2
  175. package/dist/database/backend.test.d.ts.map +0 -1
  176. package/dist/database/backend.test.js +0 -19
  177. package/dist/database/backend.test.js.map +0 -1
  178. package/dist/database/backend.testsuite.d.ts +0 -20
  179. package/dist/database/backend.testsuite.d.ts.map +0 -1
  180. package/dist/database/backend.testsuite.js +0 -1280
  181. package/dist/database/backend.testsuite.js.map +0 -1
  182. package/dist/database/base.d.ts +0 -12
  183. package/dist/database/base.d.ts.map +0 -1
  184. package/dist/database/pubsub.test.d.ts +0 -2
  185. package/dist/database/pubsub.test.d.ts.map +0 -1
  186. package/dist/database/pubsub.test.js +0 -86
  187. package/dist/database/pubsub.test.js.map +0 -1
  188. package/dist/execution.test.d.ts +0 -2
  189. package/dist/execution.test.d.ts.map +0 -1
  190. package/dist/execution.test.js +0 -662
  191. package/dist/execution.test.js.map +0 -1
  192. package/dist/index.d.ts.map +0 -1
  193. package/dist/index.js.map +0 -1
  194. package/dist/internal.d.ts.map +0 -1
  195. package/dist/internal.js.map +0 -1
  196. package/dist/practices/01-remote-workflow.d.ts +0 -2
  197. package/dist/practices/01-remote-workflow.d.ts.map +0 -1
  198. package/dist/practices/01-remote-workflow.js +0 -70
  199. package/dist/practices/01-remote-workflow.js.map +0 -1
  200. package/dist/registry.test.d.ts +0 -2
  201. package/dist/registry.test.d.ts.map +0 -1
  202. package/dist/registry.test.js +0 -95
  203. package/dist/registry.test.js.map +0 -1
  204. package/dist/testing/connection.d.ts +0 -7
  205. package/dist/testing/connection.d.ts.map +0 -1
  206. package/dist/testing/connection.js +0 -39
  207. package/dist/testing/connection.js.map +0 -1
  208. package/dist/worker.test.d.ts +0 -2
  209. package/dist/worker.test.d.ts.map +0 -1
  210. package/dist/worker.test.js +0 -1164
  211. package/dist/worker.test.js.map +0 -1
  212. package/dist/workflow.test.d.ts +0 -2
  213. package/dist/workflow.test.d.ts.map +0 -1
  214. package/dist/workflow.test.js +0 -73
  215. package/dist/workflow.test.js.map +0 -1
@@ -1,1280 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
3
- /**
4
- * Runs the Backend test suite.
5
- * @param options - Test suite options
6
- */ export function testBackend(options) {
7
- const { setup, teardown } = options;
8
- describe("Backend", ()=>{
9
- let backend;
10
- beforeAll(async ()=>{
11
- backend = await setup();
12
- });
13
- afterAll(async ()=>{
14
- await teardown(backend);
15
- });
16
- describe("createWorkflowRun()", ()=>{
17
- test("creates a workflow run", async ()=>{
18
- const expected = {
19
- namespaceId: "",
20
- id: "",
21
- workflowName: randomUUID(),
22
- version: randomUUID(),
23
- status: "pending",
24
- idempotencyKey: randomUUID(),
25
- config: {
26
- key: "val"
27
- },
28
- context: {
29
- key: "val"
30
- },
31
- input: {
32
- key: "val"
33
- },
34
- output: null,
35
- error: null,
36
- attempts: 0,
37
- parentStepAttemptNamespaceId: null,
38
- parentStepAttemptId: null,
39
- workerId: null,
40
- availableAt: newDateInOneYear(),
41
- deadlineAt: newDateInOneYear(),
42
- startedAt: null,
43
- finishedAt: null,
44
- createdAt: new Date(),
45
- updatedAt: new Date()
46
- };
47
- // Create with all fields
48
- const created = await backend.createWorkflowRun({
49
- workflowName: expected.workflowName,
50
- version: expected.version,
51
- idempotencyKey: expected.idempotencyKey,
52
- input: expected.input,
53
- config: expected.config,
54
- context: expected.context,
55
- availableAt: expected.availableAt,
56
- deadlineAt: expected.deadlineAt
57
- });
58
- expect(created.namespaceId).toHaveLength(36);
59
- expect(created.id).toHaveLength(36);
60
- expect(deltaSeconds(created.availableAt)).toBeGreaterThan(1);
61
- expect(deltaSeconds(created.createdAt)).toBeLessThan(1);
62
- expect(deltaSeconds(created.updatedAt)).toBeLessThan(1);
63
- expected.namespaceId = created.namespaceId;
64
- expected.id = created.id;
65
- expected.availableAt = created.availableAt;
66
- expected.createdAt = created.createdAt;
67
- expected.updatedAt = created.updatedAt;
68
- expect(created).toEqual(expected);
69
- // Create with minimal fields
70
- const createdMin = await backend.createWorkflowRun({
71
- workflowName: expected.workflowName,
72
- version: null,
73
- idempotencyKey: null,
74
- input: null,
75
- config: {},
76
- context: null,
77
- availableAt: null,
78
- deadlineAt: null
79
- });
80
- expect(createdMin.version).toBeNull();
81
- expect(createdMin.idempotencyKey).toBeNull();
82
- expect(createdMin.input).toBeNull();
83
- expect(createdMin.context).toBeNull();
84
- expect(deltaSeconds(createdMin.availableAt)).toBeLessThan(1); // defaults to NOW()
85
- expect(createdMin.deadlineAt).toBeNull();
86
- });
87
- });
88
- describe("listWorkflowRuns()", ()=>{
89
- test("lists workflow runs ordered by creation time", async ()=>{
90
- const backend = await setup();
91
- const first = await createPendingWorkflowRun(backend);
92
- await sleep(10); // ensure timestamp difference
93
- const second = await createPendingWorkflowRun(backend);
94
- const listed = await backend.listWorkflowRuns({});
95
- const listedIds = listed.data.map((run)=>run.id);
96
- expect(listedIds).toEqual([
97
- first.id,
98
- second.id
99
- ]);
100
- await teardown(backend);
101
- });
102
- test("paginates workflow runs", async ()=>{
103
- const backend = await setup();
104
- const runs = [];
105
- for(let i = 0; i < 5; i++){
106
- runs.push(await createPendingWorkflowRun(backend));
107
- await sleep(10);
108
- }
109
- // p1
110
- const page1 = await backend.listWorkflowRuns({
111
- limit: 2
112
- });
113
- expect(page1.data).toHaveLength(2);
114
- expect(page1.data[0]?.id).toBe(runs[0]?.id);
115
- expect(page1.data[1]?.id).toBe(runs[1]?.id);
116
- expect(page1.pagination.next).not.toBeNull();
117
- expect(page1.pagination.prev).toBeNull();
118
- // p2
119
- const page2 = await backend.listWorkflowRuns({
120
- limit: 2,
121
- // biome-ignore lint/style/noNonNullAssertion: for test
122
- after: page1.pagination.next
123
- });
124
- expect(page2.data).toHaveLength(2);
125
- expect(page2.data[0]?.id).toBe(runs[2]?.id);
126
- expect(page2.data[1]?.id).toBe(runs[3]?.id);
127
- expect(page2.pagination.next).not.toBeNull();
128
- expect(page2.pagination.prev).not.toBeNull();
129
- // p3
130
- const page3 = await backend.listWorkflowRuns({
131
- limit: 2,
132
- // biome-ignore lint/style/noNonNullAssertion: for test
133
- after: page2.pagination.next
134
- });
135
- expect(page3.data).toHaveLength(1);
136
- expect(page3.data[0]?.id).toBe(runs[4]?.id);
137
- expect(page3.pagination.next).toBeNull();
138
- expect(page3.pagination.prev).not.toBeNull();
139
- // p2 again
140
- const page2Back = await backend.listWorkflowRuns({
141
- limit: 2,
142
- // biome-ignore lint/style/noNonNullAssertion: for test
143
- before: page3.pagination.prev
144
- });
145
- expect(page2Back.data).toHaveLength(2);
146
- expect(page2Back.data[0]?.id).toBe(runs[2]?.id);
147
- expect(page2Back.data[1]?.id).toBe(runs[3]?.id);
148
- expect(page2Back.pagination.next).toEqual(page2.pagination.next);
149
- expect(page2Back.pagination.prev).toEqual(page2.pagination.prev);
150
- await teardown(backend);
151
- });
152
- test("handles empty results", async ()=>{
153
- const backend = await setup();
154
- const listed = await backend.listWorkflowRuns({});
155
- expect(listed.data).toHaveLength(0);
156
- expect(listed.pagination.next).toBeNull();
157
- expect(listed.pagination.prev).toBeNull();
158
- await teardown(backend);
159
- });
160
- test("paginates correctly with id as tiebreaker when multiple items have the same created_at timestamp", async ()=>{
161
- const backend = await setup();
162
- const runs = [];
163
- for(let i = 0; i < 5; i++){
164
- runs.push(await createPendingWorkflowRun(backend));
165
- }
166
- runs.sort((a, b)=>{
167
- const timeDiff = a.createdAt.getTime() - b.createdAt.getTime();
168
- if (timeDiff !== 0) return timeDiff;
169
- return a.id.localeCompare(b.id);
170
- });
171
- const page1 = await backend.listWorkflowRuns({
172
- limit: 2
173
- });
174
- expect(page1.data).toHaveLength(2);
175
- expect(page1.data[0]?.id).toBe(runs[0]?.id);
176
- expect(page1.data[1]?.id).toBe(runs[1]?.id);
177
- expect(page1.pagination.next).not.toBeNull();
178
- const page2 = await backend.listWorkflowRuns({
179
- limit: 2,
180
- // biome-ignore lint/style/noNonNullAssertion: for test
181
- after: page1.pagination.next
182
- });
183
- expect(page2.data).toHaveLength(2);
184
- expect(page2.data[0]?.id).toBe(runs[2]?.id);
185
- expect(page2.data[1]?.id).toBe(runs[3]?.id);
186
- expect(page2.pagination.next).not.toBeNull();
187
- const page3 = await backend.listWorkflowRuns({
188
- limit: 2,
189
- // biome-ignore lint/style/noNonNullAssertion: for test
190
- after: page2.pagination.next
191
- });
192
- expect(page3.data).toHaveLength(1);
193
- expect(page3.data[0]?.id).toBe(runs[4]?.id);
194
- expect(page3.pagination.next).toBeNull();
195
- const page2Back = await backend.listWorkflowRuns({
196
- limit: 2,
197
- // biome-ignore lint/style/noNonNullAssertion: for test
198
- before: page3.pagination.prev
199
- });
200
- expect(page2Back.data).toHaveLength(2);
201
- expect(page2Back.data[0]?.id).toBe(runs[2]?.id);
202
- expect(page2Back.data[1]?.id).toBe(runs[3]?.id);
203
- await teardown(backend);
204
- });
205
- });
206
- describe("claimWorkflowRun()", ()=>{
207
- // because claims involve timing and leases, we create and teardown a new
208
- // namespaced backend instance for each test
209
- test("claims workflow runs and respects leases, reclaiming if lease expires", async ()=>{
210
- const backend = await setup();
211
- await createPendingWorkflowRun(backend);
212
- const firstLeaseMs = 30;
213
- const firstWorker = randomUUID();
214
- const claimed = await backend.claimWorkflowRun({
215
- workerId: firstWorker,
216
- leaseDurationMs: firstLeaseMs
217
- });
218
- expect(claimed?.status).toBe("running");
219
- expect(claimed?.workerId).toBe(firstWorker);
220
- expect(claimed?.attempts).toBe(1);
221
- expect(claimed?.startedAt).not.toBeNull();
222
- const secondWorker = randomUUID();
223
- const blocked = await backend.claimWorkflowRun({
224
- workerId: secondWorker,
225
- leaseDurationMs: 10
226
- });
227
- expect(blocked).toBeNull();
228
- await sleep(firstLeaseMs + 5); // small buffer for timing variability
229
- const reclaimed = await backend.claimWorkflowRun({
230
- workerId: secondWorker,
231
- leaseDurationMs: 10
232
- });
233
- expect(reclaimed?.id).toBe(claimed?.id);
234
- expect(reclaimed?.attempts).toBe(2);
235
- expect(reclaimed?.workerId).toBe(secondWorker);
236
- expect(reclaimed?.startedAt?.getTime()).toBe(claimed?.startedAt?.getTime());
237
- await teardown(backend);
238
- });
239
- test("prioritizes pending workflow runs over expired running ones", async ()=>{
240
- const backend = await setup();
241
- const running = await createPendingWorkflowRun(backend);
242
- const runningClaim = await backend.claimWorkflowRun({
243
- workerId: "worker-running",
244
- leaseDurationMs: 5
245
- });
246
- if (!runningClaim) throw new Error("expected claim");
247
- expect(runningClaim.id).toBe(running.id);
248
- await sleep(10); // wait for running's lease to expire
249
- // pending claimed first, even though running expired
250
- const pending = await createPendingWorkflowRun(backend);
251
- const claimedFirst = await backend.claimWorkflowRun({
252
- workerId: "worker-second",
253
- leaseDurationMs: 100
254
- });
255
- expect(claimedFirst?.id).toBe(pending.id);
256
- // running claimed second
257
- const claimedSecond = await backend.claimWorkflowRun({
258
- workerId: "worker-third",
259
- leaseDurationMs: 100
260
- });
261
- expect(claimedSecond?.id).toBe(running.id);
262
- await teardown(backend);
263
- });
264
- test("returns null when no workflow runs are available", async ()=>{
265
- const backend = await setup();
266
- const claimed = await backend.claimWorkflowRun({
267
- workerId: randomUUID(),
268
- leaseDurationMs: 10
269
- });
270
- expect(claimed).toBeNull();
271
- await teardown(backend);
272
- });
273
- });
274
- describe("extendWorkflowRunLease()", ()=>{
275
- test("extends the lease for running workflow runs", async ()=>{
276
- const workerId = randomUUID();
277
- await createPendingWorkflowRun(backend);
278
- const claimed = await backend.claimWorkflowRun({
279
- workerId,
280
- leaseDurationMs: 20
281
- });
282
- if (!claimed) throw new Error("Expected workflow run to be claimed"); // for type narrowing
283
- const previousExpiry = claimed.availableAt;
284
- const extended = await backend.extendWorkflowRunLease({
285
- workflowRunId: claimed.id,
286
- workerId,
287
- leaseDurationMs: 200
288
- });
289
- expect(extended.availableAt?.getTime()).toBeGreaterThan(previousExpiry?.getTime() ?? Infinity);
290
- });
291
- });
292
- describe("sleepWorkflowRun()", ()=>{
293
- test("sets a running workflow to sleeping status until a future time", async ()=>{
294
- const workerId = randomUUID();
295
- await createPendingWorkflowRun(backend);
296
- const claimed = await backend.claimWorkflowRun({
297
- workerId,
298
- leaseDurationMs: 100
299
- });
300
- if (!claimed) throw new Error("Expected workflow run to be claimed");
301
- const sleepUntil = new Date(Date.now() + 5000); // 5 seconds from now
302
- await backend.sleepWorkflowRun({
303
- workflowRunId: claimed.id,
304
- workerId,
305
- availableAt: sleepUntil
306
- });
307
- const fetched = await backend.getWorkflowRun({
308
- workflowRunId: claimed.id
309
- });
310
- expect(fetched).not.toBeNull();
311
- expect(fetched?.availableAt?.getTime()).toBe(sleepUntil.getTime());
312
- expect(fetched?.workerId).toBeNull();
313
- expect(fetched?.status).toBe("sleeping");
314
- });
315
- test("fails when trying to sleep a canceled workflow", async ()=>{
316
- const backend = await setup();
317
- // completed run
318
- let claimed = await createClaimedWorkflowRun(backend);
319
- await backend.completeWorkflowRun({
320
- workflowRunId: claimed.id,
321
- workerId: claimed.workerId ?? "",
322
- output: null
323
- });
324
- await expect(backend.sleepWorkflowRun({
325
- workflowRunId: claimed.id,
326
- workerId: claimed.workerId ?? "",
327
- availableAt: new Date(Date.now() + 60_000)
328
- })).rejects.toThrow("Failed to sleep workflow run");
329
- // failed run
330
- claimed = await createClaimedWorkflowRun(backend);
331
- await backend.failWorkflowRun({
332
- workflowRunId: claimed.id,
333
- workerId: claimed.workerId ?? "",
334
- error: {
335
- message: "failed"
336
- }
337
- });
338
- await expect(backend.sleepWorkflowRun({
339
- workflowRunId: claimed.id,
340
- workerId: claimed.workerId ?? "",
341
- availableAt: new Date(Date.now() + 60_000)
342
- })).rejects.toThrow("Failed to sleep workflow run");
343
- // canceled run
344
- claimed = await createClaimedWorkflowRun(backend);
345
- await backend.cancelWorkflowRun({
346
- workflowRunId: claimed.id
347
- });
348
- await expect(backend.sleepWorkflowRun({
349
- workflowRunId: claimed.id,
350
- workerId: claimed.workerId ?? "",
351
- availableAt: new Date(Date.now() + 60_000)
352
- })).rejects.toThrow("Failed to sleep workflow run");
353
- await teardown(backend);
354
- });
355
- });
356
- describe("completeWorkflowRun()", ()=>{
357
- test("marks running workflow runs as completed", async ()=>{
358
- const workerId = randomUUID();
359
- await createPendingWorkflowRun(backend);
360
- const claimed = await backend.claimWorkflowRun({
361
- workerId,
362
- leaseDurationMs: 20
363
- });
364
- if (!claimed) throw new Error("Expected workflow run to be claimed"); // for type narrowing
365
- const output = {
366
- ok: true
367
- };
368
- const completed = await backend.completeWorkflowRun({
369
- workflowRunId: claimed.id,
370
- workerId,
371
- output
372
- });
373
- expect(completed.status).toBe("completed");
374
- expect(completed.output).toEqual(output);
375
- expect(completed.error).toBeNull();
376
- expect(completed.finishedAt).not.toBeNull();
377
- expect(completed.availableAt).toBeNull();
378
- });
379
- });
380
- describe("failWorkflowRun()", ()=>{
381
- test("reschedules workflow runs with exponential backoff on first failure", async ()=>{
382
- const workerId = randomUUID();
383
- await createPendingWorkflowRun(backend);
384
- const claimed = await backend.claimWorkflowRun({
385
- workerId,
386
- leaseDurationMs: 20
387
- });
388
- if (!claimed) throw new Error("Expected workflow run to be claimed");
389
- const beforeFailTime = Date.now();
390
- const error = {
391
- message: "boom"
392
- };
393
- const failed = await backend.failWorkflowRun({
394
- workflowRunId: claimed.id,
395
- workerId,
396
- error
397
- });
398
- // rescheduled, not permanently failed
399
- expect(failed.status).toBe("pending");
400
- expect(failed.error).toEqual(error);
401
- expect(failed.output).toBeNull();
402
- expect(failed.finishedAt).toBeNull();
403
- expect(failed.workerId).toBeNull();
404
- expect(failed.startedAt).toBeNull(); // cleared on failure for retry
405
- expect(failed.availableAt).not.toBeNull();
406
- if (!failed.availableAt) throw new Error("Expected availableAt");
407
- const delayMs = failed.availableAt.getTime() - beforeFailTime;
408
- expect(delayMs).toBeGreaterThanOrEqual(900); // ~1s with some tolerance
409
- expect(delayMs).toBeLessThan(1500);
410
- });
411
- test("reschedules with increasing backoff on multiple failures (known slow test)", async ()=>{
412
- // this test needs isolated namespace
413
- const backend = await setup();
414
- await createPendingWorkflowRun(backend);
415
- // fail first attempt
416
- let workerId = randomUUID();
417
- let claimed = await backend.claimWorkflowRun({
418
- workerId,
419
- leaseDurationMs: 20
420
- });
421
- if (!claimed) throw new Error("Expected workflow run to be claimed");
422
- expect(claimed.attempts).toBe(1);
423
- const firstFailed = await backend.failWorkflowRun({
424
- workflowRunId: claimed.id,
425
- workerId,
426
- error: {
427
- message: "first failure"
428
- }
429
- });
430
- expect(firstFailed.status).toBe("pending");
431
- await sleep(1100); // wait for first backoff (~1s)
432
- // fail second attempt
433
- workerId = randomUUID();
434
- claimed = await backend.claimWorkflowRun({
435
- workerId,
436
- leaseDurationMs: 20
437
- });
438
- if (!claimed) throw new Error("Expected workflow run to be claimed");
439
- expect(claimed.attempts).toBe(2);
440
- const beforeSecondFail = Date.now();
441
- const secondFailed = await backend.failWorkflowRun({
442
- workflowRunId: claimed.id,
443
- workerId,
444
- error: {
445
- message: "second failure"
446
- }
447
- });
448
- expect(secondFailed.status).toBe("pending");
449
- // second attempt should have ~2s backoff (1s * 2^1)
450
- if (!secondFailed.availableAt) throw new Error("Expected availableAt");
451
- const delayMs = secondFailed.availableAt.getTime() - beforeSecondFail;
452
- expect(delayMs).toBeGreaterThanOrEqual(1900); // ~2s with some tolerance
453
- expect(delayMs).toBeLessThan(2500);
454
- await teardown(backend);
455
- });
456
- test("marks workflow run as failed when maxAttempts is reached", async ()=>{
457
- const backend = await setup();
458
- // retryPolicy에 maxAttempts: 2를 지정하여 생성
459
- const retryPolicy = {
460
- maxAttempts: 2,
461
- initialIntervalMs: 100
462
- };
463
- await backend.createWorkflowRun({
464
- workflowName: randomUUID(),
465
- version: null,
466
- idempotencyKey: null,
467
- input: null,
468
- config: {},
469
- context: null,
470
- availableAt: null,
471
- deadlineAt: null,
472
- retryPolicy
473
- });
474
- // 첫 번째 시도 - 실패하면 pending으로 스케줄링
475
- let workerId = randomUUID();
476
- let claimed = await backend.claimWorkflowRun({
477
- workerId,
478
- leaseDurationMs: 100
479
- });
480
- if (!claimed) throw new Error("Expected workflow run to be claimed");
481
- expect(claimed.attempts).toBe(1);
482
- const firstFailed = await backend.failWorkflowRun({
483
- workflowRunId: claimed.id,
484
- workerId,
485
- error: {
486
- message: "first failure"
487
- }
488
- });
489
- expect(firstFailed.status).toBe("pending"); // 아직 maxAttempts(2) 미달
490
- await sleep(150); // 100ms backoff 대기
491
- // 두 번째 시도 - maxAttempts에 도달하면 failed로 종료
492
- workerId = randomUUID();
493
- claimed = await backend.claimWorkflowRun({
494
- workerId,
495
- leaseDurationMs: 100
496
- });
497
- if (!claimed) throw new Error("Expected workflow run to be claimed");
498
- expect(claimed.attempts).toBe(2);
499
- const secondFailed = await backend.failWorkflowRun({
500
- workflowRunId: claimed.id,
501
- workerId,
502
- error: {
503
- message: "second failure"
504
- }
505
- });
506
- // maxAttempts에 도달했으므로 failed로 종료
507
- expect(secondFailed.status).toBe("failed");
508
- expect(secondFailed.availableAt).toBeNull();
509
- expect(secondFailed.finishedAt).not.toBeNull();
510
- await teardown(backend);
511
- });
512
- test("marks workflow run as failed immediately when forceComplete is true", async ()=>{
513
- const backend = await setup();
514
- await createPendingWorkflowRun(backend);
515
- const workerId = randomUUID();
516
- const claimed = await backend.claimWorkflowRun({
517
- workerId,
518
- leaseDurationMs: 100
519
- });
520
- if (!claimed) throw new Error("Expected workflow run to be claimed");
521
- // forceComplete: true로 호출하면 재시도 없이 즉시 failed
522
- const failed = await backend.failWorkflowRun({
523
- workflowRunId: claimed.id,
524
- workerId,
525
- error: {
526
- message: "forced failure"
527
- },
528
- forceComplete: true
529
- });
530
- expect(failed.status).toBe("failed");
531
- expect(failed.availableAt).toBeNull();
532
- expect(failed.finishedAt).not.toBeNull();
533
- await teardown(backend);
534
- });
535
- test("stores retryPolicy in config when creating workflow run", async ()=>{
536
- const backend = await setup();
537
- const retryPolicy = {
538
- maxAttempts: 10,
539
- initialIntervalMs: 500,
540
- backoffCoefficient: 1.5,
541
- maximumIntervalMs: 30000
542
- };
543
- const created = await backend.createWorkflowRun({
544
- workflowName: randomUUID(),
545
- version: null,
546
- idempotencyKey: null,
547
- input: null,
548
- config: {
549
- existingKey: "existingValue"
550
- },
551
- context: null,
552
- availableAt: null,
553
- deadlineAt: null,
554
- retryPolicy
555
- });
556
- // config에 retryPolicy가 저장되어 있는지 확인
557
- const config = created.config;
558
- expect(config.existingKey).toBe("existingValue");
559
- expect(config.retryPolicy).toEqual(retryPolicy);
560
- await teardown(backend);
561
- });
562
- });
563
- describe("createStepAttempt()", ()=>{
564
- test("creates a step attempt", async ()=>{
565
- const workflowRun = await createClaimedWorkflowRun(backend);
566
- const expected = {
567
- namespaceId: workflowRun.namespaceId,
568
- id: "",
569
- workflowRunId: workflowRun.id,
570
- stepName: randomUUID(),
571
- kind: "function",
572
- status: "running",
573
- config: {
574
- key: "val"
575
- },
576
- context: null,
577
- output: null,
578
- error: null,
579
- childWorkflowRunNamespaceId: null,
580
- childWorkflowRunId: null,
581
- startedAt: null,
582
- finishedAt: null,
583
- createdAt: new Date(),
584
- updatedAt: new Date()
585
- };
586
- const created = await backend.createStepAttempt({
587
- workflowRunId: expected.workflowRunId,
588
- // biome-ignore lint/style/noNonNullAssertion: for test
589
- workerId: workflowRun.workerId,
590
- stepName: expected.stepName,
591
- kind: expected.kind,
592
- config: expected.config,
593
- context: expected.context
594
- });
595
- expect(created.id).toHaveLength(36);
596
- expect(deltaSeconds(created.startedAt)).toBeLessThan(1);
597
- expect(deltaSeconds(created.createdAt)).toBeLessThan(1);
598
- expect(deltaSeconds(created.updatedAt)).toBeLessThan(1);
599
- expected.id = created.id;
600
- expected.startedAt = created.startedAt;
601
- expected.createdAt = created.createdAt;
602
- expected.updatedAt = created.updatedAt;
603
- expect(created).toEqual(expected);
604
- });
605
- });
606
- describe("getStepAttempt()", ()=>{
607
- test("returns a persisted step attempt", async ()=>{
608
- const claimed = await createClaimedWorkflowRun(backend);
609
- const created = await backend.createStepAttempt({
610
- workflowRunId: claimed.id,
611
- // biome-ignore lint/style/noNonNullAssertion: for test
612
- workerId: claimed.workerId,
613
- stepName: randomUUID(),
614
- kind: "function",
615
- config: {},
616
- context: null
617
- });
618
- const got = await backend.getStepAttempt({
619
- stepAttemptId: created.id
620
- });
621
- expect(got).toEqual(created);
622
- });
623
- });
624
- describe("listStepAttempts()", ()=>{
625
- beforeAll(async ()=>{
626
- backend = await setup();
627
- });
628
- afterAll(async ()=>{
629
- await teardown(backend);
630
- });
631
- test("lists step attempts ordered by creation time", async ()=>{
632
- const claimed = await createClaimedWorkflowRun(backend);
633
- const first = await backend.createStepAttempt({
634
- workflowRunId: claimed.id,
635
- // biome-ignore lint/style/noNonNullAssertion: for test
636
- workerId: claimed.workerId,
637
- stepName: randomUUID(),
638
- kind: "function",
639
- config: {},
640
- context: null
641
- });
642
- await backend.completeStepAttempt({
643
- workflowRunId: claimed.id,
644
- stepAttemptId: first.id,
645
- // biome-ignore lint/style/noNonNullAssertion: for test
646
- workerId: claimed.workerId,
647
- output: {
648
- ok: true
649
- }
650
- });
651
- await sleep(10); // ensure timestamp difference
652
- const second = await backend.createStepAttempt({
653
- workflowRunId: claimed.id,
654
- // biome-ignore lint/style/noNonNullAssertion: for test
655
- workerId: claimed.workerId,
656
- stepName: randomUUID(),
657
- kind: "function",
658
- config: {},
659
- context: null
660
- });
661
- const listed = await backend.listStepAttempts({
662
- workflowRunId: claimed.id
663
- });
664
- const listedStepNames = listed.data.map((step)=>step.stepName);
665
- expect(listedStepNames).toEqual([
666
- first.stepName,
667
- second.stepName
668
- ]);
669
- });
670
- test("paginates step attempts", async ()=>{
671
- const claimed = await createClaimedWorkflowRun(backend);
672
- for(let i = 0; i < 5; i++){
673
- await backend.createStepAttempt({
674
- workflowRunId: claimed.id,
675
- // biome-ignore lint/style/noNonNullAssertion: for test
676
- workerId: claimed.workerId,
677
- stepName: `step-${String(i)}`,
678
- kind: "function",
679
- config: {},
680
- context: null
681
- });
682
- await sleep(10); // ensure createdAt differs
683
- }
684
- // p1
685
- const page1 = await backend.listStepAttempts({
686
- workflowRunId: claimed.id,
687
- limit: 2
688
- });
689
- expect(page1.data).toHaveLength(2);
690
- expect(page1.data[0]?.stepName).toBe("step-0");
691
- expect(page1.data[1]?.stepName).toBe("step-1");
692
- expect(page1.pagination.next).not.toBeNull();
693
- expect(page1.pagination.prev).toBeNull();
694
- // p2
695
- const page2 = await backend.listStepAttempts({
696
- workflowRunId: claimed.id,
697
- limit: 2,
698
- // biome-ignore lint/style/noNonNullAssertion: for test
699
- after: page1.pagination.next
700
- });
701
- expect(page2.data).toHaveLength(2);
702
- expect(page2.data[0]?.stepName).toBe("step-2");
703
- expect(page2.data[1]?.stepName).toBe("step-3");
704
- expect(page2.pagination.next).not.toBeNull();
705
- expect(page2.pagination.prev).not.toBeNull();
706
- // p3
707
- const page3 = await backend.listStepAttempts({
708
- workflowRunId: claimed.id,
709
- limit: 2,
710
- // biome-ignore lint/style/noNonNullAssertion: for test
711
- after: page2.pagination.next
712
- });
713
- expect(page3.data).toHaveLength(1);
714
- expect(page3.data[0]?.stepName).toBe("step-4");
715
- expect(page3.pagination.next).toBeNull();
716
- expect(page3.pagination.prev).not.toBeNull();
717
- // p2 again
718
- const page2Back = await backend.listStepAttempts({
719
- workflowRunId: claimed.id,
720
- limit: 2,
721
- // biome-ignore lint/style/noNonNullAssertion: for test
722
- before: page3.pagination.prev
723
- });
724
- expect(page2Back.data).toHaveLength(2);
725
- expect(page2Back.data[0]?.stepName).toBe("step-2");
726
- expect(page2Back.data[1]?.stepName).toBe("step-3");
727
- expect(page2Back.pagination.next).toEqual(page2.pagination.next);
728
- expect(page2Back.pagination.prev).toEqual(page2.pagination.prev);
729
- });
730
- test("handles empty results", async ()=>{
731
- const claimed = await createClaimedWorkflowRun(backend);
732
- const listed = await backend.listStepAttempts({
733
- workflowRunId: claimed.id
734
- });
735
- expect(listed.data).toHaveLength(0);
736
- expect(listed.pagination.next).toBeNull();
737
- expect(listed.pagination.prev).toBeNull();
738
- });
739
- test("handles exact limit match", async ()=>{
740
- const claimed = await createClaimedWorkflowRun(backend);
741
- await backend.createStepAttempt({
742
- workflowRunId: claimed.id,
743
- // biome-ignore lint/style/noNonNullAssertion: for test
744
- workerId: claimed.workerId,
745
- stepName: "step-1",
746
- kind: "function",
747
- config: {},
748
- context: null
749
- });
750
- const listed = await backend.listStepAttempts({
751
- workflowRunId: claimed.id,
752
- limit: 1
753
- });
754
- expect(listed.data).toHaveLength(1);
755
- expect(listed.pagination.next).toBeNull();
756
- expect(listed.pagination.prev).toBeNull();
757
- });
758
- });
759
- describe("completeStepAttempt()", ()=>{
760
- beforeAll(async ()=>{
761
- backend = await setup();
762
- });
763
- afterAll(async ()=>{
764
- await teardown(backend);
765
- });
766
- test("marks running step attempts as completed", async ()=>{
767
- const claimed = await createClaimedWorkflowRun(backend);
768
- const created = await backend.createStepAttempt({
769
- workflowRunId: claimed.id,
770
- // biome-ignore lint/style/noNonNullAssertion: for test
771
- workerId: claimed.workerId,
772
- stepName: randomUUID(),
773
- kind: "function",
774
- config: {},
775
- context: null
776
- });
777
- const output = {
778
- foo: "bar"
779
- };
780
- const completed = await backend.completeStepAttempt({
781
- workflowRunId: claimed.id,
782
- stepAttemptId: created.id,
783
- // biome-ignore lint/style/noNonNullAssertion: for test
784
- workerId: claimed.workerId,
785
- output
786
- });
787
- expect(completed.status).toBe("completed");
788
- expect(completed.output).toEqual(output);
789
- expect(completed.error).toBeNull();
790
- expect(completed.finishedAt).not.toBeNull();
791
- const fetched = await backend.getStepAttempt({
792
- stepAttemptId: created.id
793
- });
794
- expect(fetched?.status).toBe("completed");
795
- expect(fetched?.output).toEqual(output);
796
- expect(fetched?.error).toBeNull();
797
- expect(fetched?.finishedAt).not.toBeNull();
798
- });
799
- test("throws when workflow is not running", async ()=>{
800
- const backend = await setup();
801
- await createPendingWorkflowRun(backend);
802
- // create a step attempt by first claiming the workflow
803
- const claimed = await backend.claimWorkflowRun({
804
- workerId: randomUUID(),
805
- leaseDurationMs: 100
806
- });
807
- if (!claimed) throw new Error("Failed to claim workflow run");
808
- const stepAttempt = await backend.createStepAttempt({
809
- workflowRunId: claimed.id,
810
- // biome-ignore lint/style/noNonNullAssertion: for test
811
- workerId: claimed.workerId,
812
- stepName: randomUUID(),
813
- kind: "function",
814
- config: {},
815
- context: null
816
- });
817
- // complete the workflow so it's no longer running
818
- await backend.completeWorkflowRun({
819
- workflowRunId: claimed.id,
820
- // biome-ignore lint/style/noNonNullAssertion: for test
821
- workerId: claimed.workerId,
822
- output: null
823
- });
824
- // try to complete the step attempt
825
- await expect(backend.completeStepAttempt({
826
- workflowRunId: claimed.id,
827
- stepAttemptId: stepAttempt.id,
828
- // biome-ignore lint/style/noNonNullAssertion: for test
829
- workerId: claimed.workerId,
830
- output: {
831
- foo: "bar"
832
- }
833
- })).rejects.toThrow("Failed to mark step attempt completed");
834
- await teardown(backend);
835
- });
836
- test("throws when step attempt does not exist", async ()=>{
837
- const backend = await setup();
838
- const claimed = await createClaimedWorkflowRun(backend);
839
- await expect(backend.completeStepAttempt({
840
- workflowRunId: claimed.id,
841
- stepAttemptId: randomUUID(),
842
- // biome-ignore lint/style/noNonNullAssertion: for test
843
- workerId: claimed.workerId,
844
- output: {
845
- foo: "bar"
846
- }
847
- })).rejects.toThrow("Failed to mark step attempt completed");
848
- await teardown(backend);
849
- });
850
- });
851
- describe("failStepAttempt()", ()=>{
852
- beforeAll(async ()=>{
853
- backend = await setup();
854
- });
855
- afterAll(async ()=>{
856
- await teardown(backend);
857
- });
858
- test("marks running step attempts as failed", async ()=>{
859
- const claimed = await createClaimedWorkflowRun(backend);
860
- const created = await backend.createStepAttempt({
861
- workflowRunId: claimed.id,
862
- // biome-ignore lint/style/noNonNullAssertion: for test
863
- workerId: claimed.workerId,
864
- stepName: randomUUID(),
865
- kind: "function",
866
- config: {},
867
- context: null
868
- });
869
- const error = {
870
- message: "nope"
871
- };
872
- const failed = await backend.failStepAttempt({
873
- workflowRunId: claimed.id,
874
- stepAttemptId: created.id,
875
- // biome-ignore lint/style/noNonNullAssertion: for test
876
- workerId: claimed.workerId,
877
- error
878
- });
879
- expect(failed.status).toBe("failed");
880
- expect(failed.error).toEqual(error);
881
- expect(failed.output).toBeNull();
882
- expect(failed.finishedAt).not.toBeNull();
883
- const fetched = await backend.getStepAttempt({
884
- stepAttemptId: created.id
885
- });
886
- expect(fetched?.status).toBe("failed");
887
- expect(fetched?.error).toEqual(error);
888
- expect(fetched?.output).toBeNull();
889
- expect(fetched?.finishedAt).not.toBeNull();
890
- });
891
- test("throws when workflow is not running", async ()=>{
892
- const backend = await setup();
893
- await createPendingWorkflowRun(backend);
894
- // create a step attempt by first claiming the workflow
895
- const claimed = await backend.claimWorkflowRun({
896
- workerId: randomUUID(),
897
- leaseDurationMs: 100
898
- });
899
- if (!claimed) throw new Error("Failed to claim workflow run");
900
- const stepAttempt = await backend.createStepAttempt({
901
- workflowRunId: claimed.id,
902
- // biome-ignore lint/style/noNonNullAssertion: for test
903
- workerId: claimed.workerId,
904
- stepName: randomUUID(),
905
- kind: "function",
906
- config: {},
907
- context: null
908
- });
909
- // complete the workflow so it's no longer running
910
- await backend.completeWorkflowRun({
911
- workflowRunId: claimed.id,
912
- // biome-ignore lint/style/noNonNullAssertion: for test
913
- workerId: claimed.workerId,
914
- output: null
915
- });
916
- // try to fail the step attempt
917
- await expect(backend.failStepAttempt({
918
- workflowRunId: claimed.id,
919
- stepAttemptId: stepAttempt.id,
920
- // biome-ignore lint/style/noNonNullAssertion: for test
921
- workerId: claimed.workerId,
922
- error: {
923
- message: "nope"
924
- }
925
- })).rejects.toThrow("Failed to mark step attempt failed");
926
- await teardown(backend);
927
- });
928
- test("throws when step attempt does not exist", async ()=>{
929
- const backend = await setup();
930
- const claimed = await createClaimedWorkflowRun(backend);
931
- await expect(backend.failStepAttempt({
932
- workflowRunId: claimed.id,
933
- stepAttemptId: randomUUID(),
934
- // biome-ignore lint/style/noNonNullAssertion: for test
935
- workerId: claimed.workerId,
936
- error: {
937
- message: "nope"
938
- }
939
- })).rejects.toThrow("Failed to mark step attempt failed");
940
- await teardown(backend);
941
- });
942
- });
943
- describe("deadline_at", ()=>{
944
- beforeAll(async ()=>{
945
- backend = await setup();
946
- });
947
- afterAll(async ()=>{
948
- await teardown(backend);
949
- });
950
- test("creates a workflow run with a deadline", async ()=>{
951
- const deadline = new Date(Date.now() + 60_000); // in 1 minute
952
- const created = await backend.createWorkflowRun({
953
- workflowName: randomUUID(),
954
- version: null,
955
- idempotencyKey: null,
956
- input: null,
957
- config: {},
958
- context: null,
959
- availableAt: null,
960
- deadlineAt: deadline
961
- });
962
- expect(created.deadlineAt).not.toBeNull();
963
- expect(created.deadlineAt?.getTime()).toBe(deadline.getTime());
964
- });
965
- test("does not claim workflow runs past their deadline", async ()=>{
966
- const backend = await setup();
967
- const pastDeadline = new Date(Date.now() - 1000);
968
- await backend.createWorkflowRun({
969
- workflowName: randomUUID(),
970
- version: null,
971
- idempotencyKey: null,
972
- input: null,
973
- config: {},
974
- context: null,
975
- availableAt: null,
976
- deadlineAt: pastDeadline
977
- });
978
- const claimed = await backend.claimWorkflowRun({
979
- workerId: randomUUID(),
980
- leaseDurationMs: 1000
981
- });
982
- expect(claimed).toBeNull();
983
- await teardown(backend);
984
- });
985
- test("marks deadline-expired workflow runs as failed when claiming", async ()=>{
986
- const backend = await setup();
987
- const pastDeadline = new Date(Date.now() - 1000);
988
- const created = await backend.createWorkflowRun({
989
- workflowName: randomUUID(),
990
- version: null,
991
- idempotencyKey: null,
992
- input: null,
993
- config: {},
994
- context: null,
995
- availableAt: null,
996
- deadlineAt: pastDeadline
997
- });
998
- // attempt to claim triggers deadline check
999
- const claimed = await backend.claimWorkflowRun({
1000
- workerId: randomUUID(),
1001
- leaseDurationMs: 1000
1002
- });
1003
- expect(claimed).toBeNull();
1004
- // verify it was marked as failed
1005
- const failed = await backend.getWorkflowRun({
1006
- workflowRunId: created.id
1007
- });
1008
- expect(failed?.status).toBe("failed");
1009
- expect(failed?.error).toEqual({
1010
- message: "Workflow run deadline exceeded"
1011
- });
1012
- expect(failed?.finishedAt).not.toBeNull();
1013
- expect(failed?.availableAt).toBeNull();
1014
- await teardown(backend);
1015
- });
1016
- test("does not reschedule failed workflow runs if next retry would exceed deadline", async ()=>{
1017
- const backend = await setup();
1018
- const deadline = new Date(Date.now() + 500); // 500ms from now
1019
- const created = await backend.createWorkflowRun({
1020
- workflowName: randomUUID(),
1021
- version: null,
1022
- idempotencyKey: null,
1023
- input: null,
1024
- config: {},
1025
- context: null,
1026
- availableAt: null,
1027
- deadlineAt: deadline
1028
- });
1029
- const workerId = randomUUID();
1030
- const claimed = await backend.claimWorkflowRun({
1031
- workerId,
1032
- leaseDurationMs: 100
1033
- });
1034
- expect(claimed).not.toBeNull();
1035
- // should mark as permanently failed since retry backoff (1s) would exceed deadline (500ms)
1036
- const failed = await backend.failWorkflowRun({
1037
- workflowRunId: created.id,
1038
- workerId,
1039
- error: {
1040
- message: "test error"
1041
- }
1042
- });
1043
- expect(failed.status).toBe("failed");
1044
- expect(failed.availableAt).toBeNull();
1045
- expect(failed.finishedAt).not.toBeNull();
1046
- expect(failed.startedAt).toBeNull(); // cleared on permanent failure
1047
- await teardown(backend);
1048
- });
1049
- test("reschedules failed workflow runs if retry would complete before deadline", async ()=>{
1050
- const backend = await setup();
1051
- const deadline = new Date(Date.now() + 5000); // in 5 seconds
1052
- const created = await backend.createWorkflowRun({
1053
- workflowName: randomUUID(),
1054
- version: null,
1055
- idempotencyKey: null,
1056
- input: null,
1057
- config: {},
1058
- context: null,
1059
- availableAt: null,
1060
- deadlineAt: deadline
1061
- });
1062
- const workerId = randomUUID();
1063
- const claimed = await backend.claimWorkflowRun({
1064
- workerId,
1065
- leaseDurationMs: 100
1066
- });
1067
- expect(claimed).not.toBeNull();
1068
- // should reschedule since retry backoff (1s) is before deadline (5s
1069
- const failed = await backend.failWorkflowRun({
1070
- workflowRunId: created.id,
1071
- workerId,
1072
- error: {
1073
- message: "test error"
1074
- }
1075
- });
1076
- expect(failed.status).toBe("pending");
1077
- expect(failed.availableAt).not.toBeNull();
1078
- expect(failed.finishedAt).toBeNull();
1079
- await teardown(backend);
1080
- });
1081
- });
1082
- describe("cancelWorkflowRun()", ()=>{
1083
- test("cancels a pending workflow run", async ()=>{
1084
- const backend = await setup();
1085
- const created = await createPendingWorkflowRun(backend);
1086
- expect(created.status).toBe("pending");
1087
- const canceled = await backend.cancelWorkflowRun({
1088
- workflowRunId: created.id
1089
- });
1090
- expect(canceled.status).toBe("canceled");
1091
- expect(canceled.workerId).toBeNull();
1092
- expect(canceled.availableAt).toBeNull();
1093
- expect(canceled.finishedAt).not.toBeNull();
1094
- expect(deltaSeconds(canceled.finishedAt)).toBeLessThan(1);
1095
- await teardown(backend);
1096
- });
1097
- test("cancels a running workflow run", async ()=>{
1098
- const backend = await setup();
1099
- const created = await createClaimedWorkflowRun(backend);
1100
- expect(created.status).toBe("running");
1101
- expect(created.workerId).not.toBeNull();
1102
- const canceled = await backend.cancelWorkflowRun({
1103
- workflowRunId: created.id
1104
- });
1105
- expect(canceled.status).toBe("canceled");
1106
- expect(canceled.workerId).toBeNull();
1107
- expect(canceled.availableAt).toBeNull();
1108
- expect(canceled.finishedAt).not.toBeNull();
1109
- await teardown(backend);
1110
- });
1111
- test("cancels a sleeping workflow run", async ()=>{
1112
- const backend = await setup();
1113
- const claimed = await createClaimedWorkflowRun(backend);
1114
- // put workflow to sleep
1115
- const sleepUntil = new Date(Date.now() + 60_000); // 1 minute from now
1116
- const sleeping = await backend.sleepWorkflowRun({
1117
- workflowRunId: claimed.id,
1118
- workerId: claimed.workerId ?? "",
1119
- availableAt: sleepUntil
1120
- });
1121
- expect(sleeping.status).toBe("sleeping");
1122
- const canceled = await backend.cancelWorkflowRun({
1123
- workflowRunId: sleeping.id
1124
- });
1125
- expect(canceled.status).toBe("canceled");
1126
- expect(canceled.workerId).toBeNull();
1127
- expect(canceled.availableAt).toBeNull();
1128
- expect(canceled.finishedAt).not.toBeNull();
1129
- await teardown(backend);
1130
- });
1131
- test("throws error when canceling a completed workflow run", async ()=>{
1132
- const backend = await setup();
1133
- const claimed = await createClaimedWorkflowRun(backend);
1134
- // mark as completed
1135
- await backend.completeWorkflowRun({
1136
- workflowRunId: claimed.id,
1137
- workerId: claimed.workerId ?? "",
1138
- output: {
1139
- result: "success"
1140
- }
1141
- });
1142
- await expect(backend.cancelWorkflowRun({
1143
- workflowRunId: claimed.id
1144
- })).rejects.toThrow(/Cannot cancel workflow run .* with status completed/);
1145
- await teardown(backend);
1146
- });
1147
- test("throws error when canceling a failed workflow run", async ()=>{
1148
- const backend = await setup();
1149
- // create with deadline that's already passed to make it fail
1150
- const workflowWithDeadline = await backend.createWorkflowRun({
1151
- workflowName: randomUUID(),
1152
- version: null,
1153
- idempotencyKey: null,
1154
- input: null,
1155
- config: {},
1156
- context: null,
1157
- availableAt: null,
1158
- deadlineAt: new Date(Date.now() - 1000)
1159
- });
1160
- // try to claim it, which should mark it as failed due to deadline
1161
- const claimed = await backend.claimWorkflowRun({
1162
- workerId: randomUUID(),
1163
- leaseDurationMs: 100
1164
- });
1165
- // if claim succeeds, manually fail it
1166
- if (claimed?.workerId) {
1167
- await backend.failWorkflowRun({
1168
- workflowRunId: claimed.id,
1169
- workerId: claimed.workerId,
1170
- error: {
1171
- message: "test error"
1172
- }
1173
- });
1174
- }
1175
- // get a workflow that's definitely failed
1176
- const failedRun = await backend.getWorkflowRun({
1177
- workflowRunId: workflowWithDeadline.id
1178
- });
1179
- if (failedRun?.status === "failed") {
1180
- await expect(backend.cancelWorkflowRun({
1181
- workflowRunId: failedRun.id
1182
- })).rejects.toThrow(/Cannot cancel workflow run .* with status failed/);
1183
- }
1184
- await teardown(backend);
1185
- });
1186
- test("is idempotent when canceling an already canceled workflow run", async ()=>{
1187
- const backend = await setup();
1188
- const created = await createPendingWorkflowRun(backend);
1189
- const firstCancel = await backend.cancelWorkflowRun({
1190
- workflowRunId: created.id
1191
- });
1192
- expect(firstCancel.status).toBe("canceled");
1193
- const secondCancel = await backend.cancelWorkflowRun({
1194
- workflowRunId: created.id
1195
- });
1196
- expect(secondCancel.status).toBe("canceled");
1197
- expect(secondCancel.id).toBe(firstCancel.id);
1198
- await teardown(backend);
1199
- });
1200
- test("throws error when canceling a non-existent workflow run", async ()=>{
1201
- const backend = await setup();
1202
- const nonExistentId = randomUUID();
1203
- await expect(backend.cancelWorkflowRun({
1204
- workflowRunId: nonExistentId
1205
- })).rejects.toThrow(`Workflow run ${nonExistentId} does not exist`);
1206
- await teardown(backend);
1207
- });
1208
- test("canceled workflow is not claimed by workers", async ()=>{
1209
- const backend = await setup();
1210
- const created = await createPendingWorkflowRun(backend);
1211
- // cancel the workflow
1212
- await backend.cancelWorkflowRun({
1213
- workflowRunId: created.id
1214
- });
1215
- // try to claim work
1216
- const claimed = await backend.claimWorkflowRun({
1217
- workerId: randomUUID(),
1218
- leaseDurationMs: 100
1219
- });
1220
- // should not claim the canceled workflow
1221
- expect(claimed).toBeNull();
1222
- await teardown(backend);
1223
- });
1224
- });
1225
- });
1226
- }
1227
- /**
1228
- * Create a pending workflow run for tests.
1229
- * @param b - Backend
1230
- * @returns Created workflow run
1231
- */ async function createPendingWorkflowRun(b) {
1232
- return await b.createWorkflowRun({
1233
- workflowName: randomUUID(),
1234
- version: null,
1235
- idempotencyKey: null,
1236
- input: null,
1237
- config: {},
1238
- context: null,
1239
- availableAt: null,
1240
- deadlineAt: null
1241
- });
1242
- }
1243
- /**
1244
- * Create and claim a workflow run for tests.
1245
- * @param b - Backend
1246
- * @returns Claimed workflow run
1247
- */ async function createClaimedWorkflowRun(b) {
1248
- await createPendingWorkflowRun(b);
1249
- const claimed = await b.claimWorkflowRun({
1250
- workerId: randomUUID(),
1251
- leaseDurationMs: 100
1252
- });
1253
- if (!claimed) throw new Error("Failed to claim workflow run");
1254
- return claimed;
1255
- }
1256
- /**
1257
- * Get delta in seconds from now.
1258
- * @param date - Date to compare
1259
- * @returns Delta in seconds
1260
- */ function deltaSeconds(date) {
1261
- if (!date) return Infinity;
1262
- return Math.abs((Date.now() - date.getTime()) / 1000);
1263
- }
1264
- /**
1265
- * Create a Date one year in the future.
1266
- * @returns Future Date
1267
- */ function newDateInOneYear() {
1268
- const d = new Date();
1269
- d.setFullYear(d.getFullYear() + 1);
1270
- return d;
1271
- }
1272
- /**
1273
- * Sleep for a given duration.
1274
- * @param ms - Milliseconds to sleep
1275
- * @returns Promise resolved after sleeping
1276
- */ function sleep(ms) {
1277
- return new Promise((resolve)=>setTimeout(resolve, ms));
1278
- }
1279
-
1280
- //# sourceMappingURL=backend.testsuite.js.map