openworkflow 0.4.1 → 0.6.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 (220) hide show
  1. package/README.md +43 -345
  2. package/dist/backend-test/backend.testsuite.d.ts +20 -0
  3. package/dist/backend-test/backend.testsuite.d.ts.map +1 -0
  4. package/dist/{core → backend-test}/backend.testsuite.js +191 -59
  5. package/dist/backend-test/index.d.ts +2 -0
  6. package/dist/backend-test/index.d.ts.map +1 -0
  7. package/dist/backend-test/index.js +1 -0
  8. package/dist/{core/backend.d.ts → backend.d.ts} +7 -5
  9. package/dist/backend.d.ts.map +1 -0
  10. package/dist/{core/backend.js → backend.js} +0 -1
  11. package/dist/backend.testsuite.d.ts +20 -0
  12. package/dist/backend.testsuite.d.ts.map +1 -0
  13. package/dist/{core/backend-test-suite.js → backend.testsuite.js} +301 -171
  14. package/dist/bin/openworkflow.d.ts +3 -0
  15. package/dist/bin/openworkflow.d.ts.map +1 -0
  16. package/dist/bin/openworkflow.js +43 -0
  17. package/dist/chaos.test.d.ts +2 -0
  18. package/dist/chaos.test.d.ts.map +1 -0
  19. package/dist/chaos.test.js +88 -0
  20. package/dist/client.d.ts +141 -0
  21. package/dist/client.d.ts.map +1 -0
  22. package/dist/{sdk/sdk.js → client.js} +43 -71
  23. package/dist/client.test.d.ts +2 -0
  24. package/dist/client.test.d.ts.map +1 -0
  25. package/dist/{sdk/sdk.test.js → client.test.js} +130 -14
  26. package/dist/core/duration.d.ts +4 -2
  27. package/dist/core/duration.d.ts.map +1 -1
  28. package/dist/core/duration.js +3 -2
  29. package/dist/core/duration.test.js +0 -1
  30. package/dist/core/error.d.ts +14 -0
  31. package/dist/core/error.d.ts.map +1 -0
  32. package/dist/core/error.js +17 -0
  33. package/dist/core/error.test.d.ts +2 -0
  34. package/dist/core/error.test.d.ts.map +1 -0
  35. package/dist/core/error.test.js +60 -0
  36. package/dist/core/json.js +0 -1
  37. package/dist/core/result.d.ts +14 -4
  38. package/dist/core/result.d.ts.map +1 -1
  39. package/dist/core/result.js +10 -1
  40. package/dist/core/result.test.js +2 -2
  41. package/dist/core/retry.d.ts +0 -9
  42. package/dist/core/retry.d.ts.map +1 -1
  43. package/dist/core/retry.js +0 -15
  44. package/dist/core/schema.js +0 -1
  45. package/dist/core/step.d.ts +1 -32
  46. package/dist/core/step.d.ts.map +1 -1
  47. package/dist/core/step.js +0 -36
  48. package/dist/core/step.test.js +1 -75
  49. package/dist/core/workflow.d.ts +2 -47
  50. package/dist/core/workflow.d.ts.map +1 -1
  51. package/dist/core/workflow.js +0 -45
  52. package/dist/core/workflow.test.js +1 -104
  53. package/dist/driver.d.ts +116 -0
  54. package/dist/driver.d.ts.map +1 -0
  55. package/dist/driver.js +1 -0
  56. package/dist/{execution/execution.d.ts → execution.d.ts} +4 -26
  57. package/dist/execution.d.ts.map +1 -0
  58. package/dist/{execution/execution.js → execution.js} +4 -5
  59. package/dist/execution.test.d.ts.map +1 -0
  60. package/dist/{execution/execution.test.js → execution.test.js} +4 -5
  61. package/dist/factory.d.ts +74 -0
  62. package/dist/factory.d.ts.map +1 -0
  63. package/dist/factory.js +72 -0
  64. package/dist/index.d.ts +6 -9
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -5
  67. package/dist/internal.d.ts +7 -0
  68. package/dist/internal.d.ts.map +1 -0
  69. package/dist/internal.js +2 -0
  70. package/dist/node-sqlite/backend.d.ts +52 -0
  71. package/dist/node-sqlite/backend.d.ts.map +1 -0
  72. package/dist/node-sqlite/backend.js +673 -0
  73. package/dist/node-sqlite/index.d.ts +11 -0
  74. package/dist/node-sqlite/index.d.ts.map +1 -0
  75. package/dist/node-sqlite/index.js +7 -0
  76. package/dist/node-sqlite/sqlite.d.ts +60 -0
  77. package/dist/node-sqlite/sqlite.d.ts.map +1 -0
  78. package/dist/{backend-sqlite → node-sqlite}/sqlite.js +20 -3
  79. package/dist/postgres/backend.d.ts +44 -0
  80. package/dist/postgres/backend.d.ts.map +1 -0
  81. package/dist/postgres/backend.js +534 -0
  82. package/dist/postgres/backend.test.d.ts +2 -0
  83. package/dist/postgres/backend.test.d.ts.map +1 -0
  84. package/dist/postgres/backend.test.js +19 -0
  85. package/dist/postgres/driver.d.ts +81 -0
  86. package/dist/postgres/driver.d.ts.map +1 -0
  87. package/dist/postgres/driver.js +63 -0
  88. package/dist/postgres/index.d.ts +11 -0
  89. package/dist/postgres/index.d.ts.map +1 -0
  90. package/dist/postgres/index.js +7 -0
  91. package/dist/postgres/internal.d.ts +2 -0
  92. package/dist/postgres/internal.d.ts.map +1 -0
  93. package/dist/postgres/internal.js +1 -0
  94. package/dist/postgres/postgres.d.ts +42 -0
  95. package/dist/postgres/postgres.d.ts.map +1 -0
  96. package/dist/postgres/postgres.js +233 -0
  97. package/dist/postgres/postgres.test.d.ts +2 -0
  98. package/dist/postgres/postgres.test.d.ts.map +1 -0
  99. package/dist/postgres/postgres.test.js +45 -0
  100. package/dist/postgres/scripts/db-migrate.d.ts +2 -0
  101. package/dist/postgres/scripts/db-migrate.d.ts.map +1 -0
  102. package/dist/postgres/scripts/db-migrate.js +4 -0
  103. package/dist/postgres/scripts/db-reset.d.ts +2 -0
  104. package/dist/postgres/scripts/db-reset.d.ts.map +1 -0
  105. package/dist/postgres/scripts/db-reset.js +5 -0
  106. package/dist/postgres/scripts/squawk.d.ts +2 -0
  107. package/dist/postgres/scripts/squawk.d.ts.map +1 -0
  108. package/dist/postgres/scripts/squawk.js +16 -0
  109. package/dist/postgres/vitest.global-setup.d.ts +3 -0
  110. package/dist/postgres/vitest.global-setup.d.ts.map +1 -0
  111. package/dist/postgres/vitest.global-setup.js +7 -0
  112. package/dist/postgres.d.ts +2 -0
  113. package/dist/postgres.d.ts.map +1 -0
  114. package/dist/postgres.js +1 -0
  115. package/dist/registry.d.ts +27 -0
  116. package/dist/registry.d.ts.map +1 -0
  117. package/dist/registry.js +48 -0
  118. package/dist/registry.test.d.ts +2 -0
  119. package/dist/registry.test.d.ts.map +1 -0
  120. package/dist/registry.test.js +109 -0
  121. package/dist/{backend-sqlite → sqlite}/backend.d.ts +8 -4
  122. package/dist/sqlite/backend.d.ts.map +1 -0
  123. package/dist/{backend-sqlite → sqlite}/backend.js +35 -9
  124. package/dist/sqlite/backend.test.d.ts +2 -0
  125. package/dist/sqlite/backend.test.d.ts.map +1 -0
  126. package/dist/sqlite/backend.test.js +50 -0
  127. package/dist/sqlite/driver.d.ts +79 -0
  128. package/dist/sqlite/driver.d.ts.map +1 -0
  129. package/dist/sqlite/driver.js +62 -0
  130. package/dist/sqlite/index.d.ts +13 -0
  131. package/dist/sqlite/index.d.ts.map +1 -0
  132. package/dist/sqlite/index.js +11 -0
  133. package/dist/sqlite/internal.d.ts +2 -0
  134. package/dist/sqlite/internal.d.ts.map +1 -0
  135. package/dist/sqlite/internal.js +1 -0
  136. package/dist/{backend-sqlite → sqlite}/sqlite.d.ts +18 -2
  137. package/dist/sqlite/sqlite.d.ts.map +1 -0
  138. package/dist/sqlite/sqlite.js +246 -0
  139. package/dist/sqlite/sqlite.test.d.ts +2 -0
  140. package/dist/sqlite/sqlite.test.d.ts.map +1 -0
  141. package/dist/sqlite/sqlite.test.js +171 -0
  142. package/dist/sqlite.d.ts +2 -0
  143. package/dist/sqlite.d.ts.map +1 -0
  144. package/dist/sqlite.js +1 -0
  145. package/dist/tsconfig.tsbuildinfo +1 -1
  146. package/dist/{worker/worker.d.ts → worker.d.ts} +11 -4
  147. package/dist/worker.d.ts.map +1 -0
  148. package/dist/{worker/worker.js → worker.js} +20 -11
  149. package/dist/{worker/worker.test.d.ts.map → worker.test.d.ts.map} +1 -1
  150. package/dist/{worker/worker.test.js → worker.test.js} +136 -22
  151. package/dist/workflow.d.ts +60 -0
  152. package/dist/workflow.d.ts.map +1 -0
  153. package/dist/workflow.js +48 -0
  154. package/dist/workflow.test.d.ts +2 -0
  155. package/dist/workflow.test.d.ts.map +1 -0
  156. package/dist/workflow.test.js +84 -0
  157. package/package.json +28 -4
  158. package/dist/backend-sqlite/backend.d.ts.map +0 -1
  159. package/dist/backend-sqlite/backend.js.map +0 -1
  160. package/dist/backend-sqlite/index.d.ts +0 -2
  161. package/dist/backend-sqlite/index.d.ts.map +0 -1
  162. package/dist/backend-sqlite/index.js +0 -2
  163. package/dist/backend-sqlite/index.js.map +0 -1
  164. package/dist/backend-sqlite/sqlite.d.ts.map +0 -1
  165. package/dist/backend-sqlite/sqlite.js.map +0 -1
  166. package/dist/config/config.d.ts +0 -102
  167. package/dist/config/config.d.ts.map +0 -1
  168. package/dist/config/config.js +0 -29
  169. package/dist/config/config.js.map +0 -1
  170. package/dist/config/index.d.ts +0 -3
  171. package/dist/config/index.d.ts.map +0 -1
  172. package/dist/config/index.js +0 -2
  173. package/dist/config/index.js.map +0 -1
  174. package/dist/config.d.ts +0 -28
  175. package/dist/config.d.ts.map +0 -1
  176. package/dist/config.js +0 -41
  177. package/dist/config.js.map +0 -1
  178. package/dist/core/backend-test-suite.d.ts +0 -22
  179. package/dist/core/backend-test-suite.d.ts.map +0 -1
  180. package/dist/core/backend-test-suite.js.map +0 -1
  181. package/dist/core/backend.d.ts.map +0 -1
  182. package/dist/core/backend.js.map +0 -1
  183. package/dist/core/backend.testsuite.d.ts +0 -21
  184. package/dist/core/backend.testsuite.d.ts.map +0 -1
  185. package/dist/core/backend.testsuite.js.map +0 -1
  186. package/dist/core/duration.js.map +0 -1
  187. package/dist/core/duration.test.js.map +0 -1
  188. package/dist/core/json.js.map +0 -1
  189. package/dist/core/result.js.map +0 -1
  190. package/dist/core/result.test.js.map +0 -1
  191. package/dist/core/retry.js.map +0 -1
  192. package/dist/core/retry.test.d.ts +0 -2
  193. package/dist/core/retry.test.d.ts.map +0 -1
  194. package/dist/core/retry.test.js +0 -36
  195. package/dist/core/retry.test.js.map +0 -1
  196. package/dist/core/schema.js.map +0 -1
  197. package/dist/core/step.js.map +0 -1
  198. package/dist/core/step.test.js.map +0 -1
  199. package/dist/core/workflow.js.map +0 -1
  200. package/dist/core/workflow.test.js.map +0 -1
  201. package/dist/execution/execution.d.ts.map +0 -1
  202. package/dist/execution/execution.js.map +0 -1
  203. package/dist/execution/execution.test.d.ts.map +0 -1
  204. package/dist/execution/execution.test.js.map +0 -1
  205. package/dist/global.d.ts +0 -62
  206. package/dist/global.d.ts.map +0 -1
  207. package/dist/global.js +0 -78
  208. package/dist/global.js.map +0 -1
  209. package/dist/index.js.map +0 -1
  210. package/dist/sdk/sdk.d.ts +0 -182
  211. package/dist/sdk/sdk.d.ts.map +0 -1
  212. package/dist/sdk/sdk.js.map +0 -1
  213. package/dist/sdk/sdk.test.d.ts +0 -2
  214. package/dist/sdk/sdk.test.d.ts.map +0 -1
  215. package/dist/sdk/sdk.test.js.map +0 -1
  216. package/dist/worker/worker.d.ts.map +0 -1
  217. package/dist/worker/worker.js.map +0 -1
  218. package/dist/worker/worker.test.js.map +0 -1
  219. /package/dist/{execution/execution.test.d.ts → execution.test.d.ts} +0 -0
  220. /package/dist/{worker/worker.test.d.ts → worker.test.d.ts} +0 -0
@@ -1,20 +1,15 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
3
3
  /**
4
- * Runs the Backend compliance test suite.
5
- *
6
- * This function wraps all the tests that verify a Backend implementation
7
- * conforms to the Backend interface contract. It uses the factory to create
8
- * backend instances and the teardown function to clean them up.
9
- *
10
- * @param factory - Creates a new isolated Backend instance
11
- * @param teardown - Cleans up a Backend instance
4
+ * Runs the Backend test suite.
5
+ * @param options - Test suite options
12
6
  */
13
- export function testBackend(factory, teardown) {
7
+ export function testBackend(options) {
8
+ const { setup, teardown } = options;
14
9
  describe("Backend", () => {
15
10
  let backend;
16
11
  beforeAll(async () => {
17
- backend = await factory();
12
+ backend = await setup();
18
13
  });
19
14
  afterAll(async () => {
20
15
  await teardown(backend);
@@ -87,77 +82,119 @@ export function testBackend(factory, teardown) {
87
82
  });
88
83
  describe("listWorkflowRuns()", () => {
89
84
  test("lists workflow runs ordered by creation time", async () => {
90
- const isolatedBackend = await factory();
91
- const first = await createPendingWorkflowRun(isolatedBackend);
85
+ const backend = await setup();
86
+ const first = await createPendingWorkflowRun(backend);
92
87
  await sleep(10); // ensure timestamp difference
93
- const second = await createPendingWorkflowRun(isolatedBackend);
94
- const listed = await isolatedBackend.listWorkflowRuns({});
95
- expect(listed.data.map((run) => run.id)).toEqual([first.id, second.id]);
96
- await teardown(isolatedBackend);
88
+ const second = await createPendingWorkflowRun(backend);
89
+ const listed = await backend.listWorkflowRuns({});
90
+ const listedIds = listed.data.map((run) => run.id);
91
+ expect(listedIds).toEqual([second.id, first.id]);
92
+ await teardown(backend);
97
93
  });
98
94
  test("paginates workflow runs", async () => {
99
- const isolatedBackend = await factory();
95
+ const backend = await setup();
100
96
  const runs = [];
101
97
  for (let i = 0; i < 5; i++) {
102
- runs.push(await createPendingWorkflowRun(isolatedBackend));
98
+ runs.push(await createPendingWorkflowRun(backend));
103
99
  await sleep(10);
104
100
  }
105
101
  // p1
106
- const page1 = await isolatedBackend.listWorkflowRuns({ limit: 2 });
102
+ const page1 = await backend.listWorkflowRuns({ limit: 2 });
107
103
  expect(page1.data).toHaveLength(2);
108
- expect(page1.data[0]?.id).toBe(runs[0]?.id);
109
- expect(page1.data[1]?.id).toBe(runs[1]?.id);
104
+ expect(page1.data[0]?.id).toBe(runs[4]?.id);
105
+ expect(page1.data[1]?.id).toBe(runs[3]?.id);
110
106
  expect(page1.pagination.next).not.toBeNull();
111
107
  expect(page1.pagination.prev).toBeNull();
112
108
  // p2
113
- const page2 = await isolatedBackend.listWorkflowRuns({
109
+ const page2 = await backend.listWorkflowRuns({
114
110
  limit: 2,
115
111
  after: page1.pagination.next, // eslint-disable-line @typescript-eslint/no-non-null-assertion
116
112
  });
117
113
  expect(page2.data).toHaveLength(2);
118
114
  expect(page2.data[0]?.id).toBe(runs[2]?.id);
119
- expect(page2.data[1]?.id).toBe(runs[3]?.id);
115
+ expect(page2.data[1]?.id).toBe(runs[1]?.id);
120
116
  expect(page2.pagination.next).not.toBeNull();
121
117
  expect(page2.pagination.prev).not.toBeNull();
122
118
  // p3
123
- const page3 = await isolatedBackend.listWorkflowRuns({
119
+ const page3 = await backend.listWorkflowRuns({
124
120
  limit: 2,
125
121
  after: page2.pagination.next, // eslint-disable-line @typescript-eslint/no-non-null-assertion
126
122
  });
127
123
  expect(page3.data).toHaveLength(1);
128
- expect(page3.data[0]?.id).toBe(runs[4]?.id);
124
+ expect(page3.data[0]?.id).toBe(runs[0]?.id);
129
125
  expect(page3.pagination.next).toBeNull();
130
126
  expect(page3.pagination.prev).not.toBeNull();
131
127
  // p2 again
132
- const page2Back = await isolatedBackend.listWorkflowRuns({
128
+ const page2Back = await backend.listWorkflowRuns({
133
129
  limit: 2,
134
130
  before: page3.pagination.prev, // eslint-disable-line @typescript-eslint/no-non-null-assertion
135
131
  });
136
132
  expect(page2Back.data).toHaveLength(2);
137
133
  expect(page2Back.data[0]?.id).toBe(runs[2]?.id);
138
- expect(page2Back.data[1]?.id).toBe(runs[3]?.id);
134
+ expect(page2Back.data[1]?.id).toBe(runs[1]?.id);
139
135
  expect(page2Back.pagination.next).toEqual(page2.pagination.next);
140
136
  expect(page2Back.pagination.prev).toEqual(page2.pagination.prev);
141
- await teardown(isolatedBackend);
137
+ await teardown(backend);
142
138
  });
143
139
  test("handles empty results", async () => {
144
- const isolatedBackend = await factory();
145
- const listed = await isolatedBackend.listWorkflowRuns({});
140
+ const backend = await setup();
141
+ const listed = await backend.listWorkflowRuns({});
146
142
  expect(listed.data).toHaveLength(0);
147
143
  expect(listed.pagination.next).toBeNull();
148
144
  expect(listed.pagination.prev).toBeNull();
149
- await teardown(isolatedBackend);
145
+ await teardown(backend);
146
+ });
147
+ test("paginates correctly with id as tiebreaker when multiple items have the same created_at timestamp", async () => {
148
+ const backend = await setup();
149
+ const runs = [];
150
+ for (let i = 0; i < 5; i++) {
151
+ runs.push(await createPendingWorkflowRun(backend));
152
+ }
153
+ runs.sort((a, b) => {
154
+ const timeDiff = b.createdAt.getTime() - a.createdAt.getTime();
155
+ if (timeDiff !== 0)
156
+ return timeDiff;
157
+ return b.id.localeCompare(a.id);
158
+ });
159
+ const page1 = await backend.listWorkflowRuns({ limit: 2 });
160
+ expect(page1.data).toHaveLength(2);
161
+ expect(page1.data[0]?.id).toBe(runs[0]?.id);
162
+ expect(page1.data[1]?.id).toBe(runs[1]?.id);
163
+ expect(page1.pagination.next).not.toBeNull();
164
+ const page2 = await backend.listWorkflowRuns({
165
+ limit: 2,
166
+ after: page1.pagination.next, // eslint-disable-line @typescript-eslint/no-non-null-assertion
167
+ });
168
+ expect(page2.data).toHaveLength(2);
169
+ expect(page2.data[0]?.id).toBe(runs[2]?.id);
170
+ expect(page2.data[1]?.id).toBe(runs[3]?.id);
171
+ expect(page2.pagination.next).not.toBeNull();
172
+ const page3 = await backend.listWorkflowRuns({
173
+ limit: 2,
174
+ after: page2.pagination.next, // eslint-disable-line @typescript-eslint/no-non-null-assertion
175
+ });
176
+ expect(page3.data).toHaveLength(1);
177
+ expect(page3.data[0]?.id).toBe(runs[4]?.id);
178
+ expect(page3.pagination.next).toBeNull();
179
+ const page2Back = await backend.listWorkflowRuns({
180
+ limit: 2,
181
+ before: page3.pagination.prev, // eslint-disable-line @typescript-eslint/no-non-null-assertion
182
+ });
183
+ expect(page2Back.data).toHaveLength(2);
184
+ expect(page2Back.data[0]?.id).toBe(runs[2]?.id);
185
+ expect(page2Back.data[1]?.id).toBe(runs[3]?.id);
186
+ await teardown(backend);
150
187
  });
151
188
  });
152
189
  describe("claimWorkflowRun()", () => {
153
190
  // because claims involve timing and leases, we create and teardown a new
154
191
  // namespaced backend instance for each test
155
192
  test("claims workflow runs and respects leases, reclaiming if lease expires", async () => {
156
- const isolatedBackend = await factory();
157
- await createPendingWorkflowRun(isolatedBackend);
193
+ const backend = await setup();
194
+ await createPendingWorkflowRun(backend);
158
195
  const firstLeaseMs = 30;
159
196
  const firstWorker = randomUUID();
160
- const claimed = await isolatedBackend.claimWorkflowRun({
197
+ const claimed = await backend.claimWorkflowRun({
161
198
  workerId: firstWorker,
162
199
  leaseDurationMs: firstLeaseMs,
163
200
  });
@@ -166,13 +203,13 @@ export function testBackend(factory, teardown) {
166
203
  expect(claimed?.attempts).toBe(1);
167
204
  expect(claimed?.startedAt).not.toBeNull();
168
205
  const secondWorker = randomUUID();
169
- const blocked = await isolatedBackend.claimWorkflowRun({
206
+ const blocked = await backend.claimWorkflowRun({
170
207
  workerId: secondWorker,
171
208
  leaseDurationMs: 10,
172
209
  });
173
210
  expect(blocked).toBeNull();
174
- await sleep(firstLeaseMs);
175
- const reclaimed = await isolatedBackend.claimWorkflowRun({
211
+ await sleep(firstLeaseMs + 5); // small buffer for timing variability
212
+ const reclaimed = await backend.claimWorkflowRun({
176
213
  workerId: secondWorker,
177
214
  leaseDurationMs: 10,
178
215
  });
@@ -180,12 +217,12 @@ export function testBackend(factory, teardown) {
180
217
  expect(reclaimed?.attempts).toBe(2);
181
218
  expect(reclaimed?.workerId).toBe(secondWorker);
182
219
  expect(reclaimed?.startedAt?.getTime()).toBe(claimed?.startedAt?.getTime());
183
- await teardown(isolatedBackend);
220
+ await teardown(backend);
184
221
  });
185
222
  test("prioritizes pending workflow runs over expired running ones", async () => {
186
- const isolatedBackend = await factory();
187
- const running = await createPendingWorkflowRun(isolatedBackend);
188
- const runningClaim = await isolatedBackend.claimWorkflowRun({
223
+ const backend = await setup();
224
+ const running = await createPendingWorkflowRun(backend);
225
+ const runningClaim = await backend.claimWorkflowRun({
189
226
  workerId: "worker-running",
190
227
  leaseDurationMs: 5,
191
228
  });
@@ -194,28 +231,28 @@ export function testBackend(factory, teardown) {
194
231
  expect(runningClaim.id).toBe(running.id);
195
232
  await sleep(10); // wait for running's lease to expire
196
233
  // pending claimed first, even though running expired
197
- const pending = await createPendingWorkflowRun(isolatedBackend);
198
- const claimedFirst = await isolatedBackend.claimWorkflowRun({
234
+ const pending = await createPendingWorkflowRun(backend);
235
+ const claimedFirst = await backend.claimWorkflowRun({
199
236
  workerId: "worker-second",
200
237
  leaseDurationMs: 100,
201
238
  });
202
239
  expect(claimedFirst?.id).toBe(pending.id);
203
240
  // running claimed second
204
- const claimedSecond = await isolatedBackend.claimWorkflowRun({
241
+ const claimedSecond = await backend.claimWorkflowRun({
205
242
  workerId: "worker-third",
206
243
  leaseDurationMs: 100,
207
244
  });
208
245
  expect(claimedSecond?.id).toBe(running.id);
209
- await teardown(isolatedBackend);
246
+ await teardown(backend);
210
247
  });
211
248
  test("returns null when no workflow runs are available", async () => {
212
- const isolatedBackend = await factory();
213
- const claimed = await isolatedBackend.claimWorkflowRun({
249
+ const backend = await setup();
250
+ const claimed = await backend.claimWorkflowRun({
214
251
  workerId: randomUUID(),
215
252
  leaseDurationMs: 10,
216
253
  });
217
254
  expect(claimed).toBeNull();
218
- await teardown(isolatedBackend);
255
+ await teardown(backend);
219
256
  });
220
257
  });
221
258
  describe("extendWorkflowRunLease()", () => {
@@ -262,42 +299,42 @@ export function testBackend(factory, teardown) {
262
299
  expect(fetched?.status).toBe("sleeping");
263
300
  });
264
301
  test("fails when trying to sleep a canceled workflow", async () => {
265
- const isolatedBackend = await factory();
302
+ const backend = await setup();
266
303
  // completed run
267
- let claimed = await createClaimedWorkflowRun(isolatedBackend);
268
- await isolatedBackend.completeWorkflowRun({
304
+ let claimed = await createClaimedWorkflowRun(backend);
305
+ await backend.completeWorkflowRun({
269
306
  workflowRunId: claimed.id,
270
307
  workerId: claimed.workerId ?? "",
271
308
  output: null,
272
309
  });
273
- await expect(isolatedBackend.sleepWorkflowRun({
310
+ await expect(backend.sleepWorkflowRun({
274
311
  workflowRunId: claimed.id,
275
312
  workerId: claimed.workerId ?? "",
276
313
  availableAt: new Date(Date.now() + 60_000),
277
314
  })).rejects.toThrow("Failed to sleep workflow run");
278
315
  // failed run
279
- claimed = await createClaimedWorkflowRun(isolatedBackend);
280
- await isolatedBackend.failWorkflowRun({
316
+ claimed = await createClaimedWorkflowRun(backend);
317
+ await backend.failWorkflowRun({
281
318
  workflowRunId: claimed.id,
282
319
  workerId: claimed.workerId ?? "",
283
- error: null,
320
+ error: { message: "failed" },
284
321
  });
285
- await expect(isolatedBackend.sleepWorkflowRun({
322
+ await expect(backend.sleepWorkflowRun({
286
323
  workflowRunId: claimed.id,
287
324
  workerId: claimed.workerId ?? "",
288
325
  availableAt: new Date(Date.now() + 60_000),
289
326
  })).rejects.toThrow("Failed to sleep workflow run");
290
327
  // canceled run
291
- claimed = await createClaimedWorkflowRun(isolatedBackend);
292
- await isolatedBackend.cancelWorkflowRun({
328
+ claimed = await createClaimedWorkflowRun(backend);
329
+ await backend.cancelWorkflowRun({
293
330
  workflowRunId: claimed.id,
294
331
  });
295
- await expect(isolatedBackend.sleepWorkflowRun({
332
+ await expect(backend.sleepWorkflowRun({
296
333
  workflowRunId: claimed.id,
297
334
  workerId: claimed.workerId ?? "",
298
335
  availableAt: new Date(Date.now() + 60_000),
299
336
  })).rejects.toThrow("Failed to sleep workflow run");
300
- await teardown(isolatedBackend);
337
+ await teardown(backend);
301
338
  });
302
339
  });
303
340
  describe("completeWorkflowRun()", () => {
@@ -346,6 +383,7 @@ export function testBackend(factory, teardown) {
346
383
  expect(failed.output).toBeNull();
347
384
  expect(failed.finishedAt).toBeNull();
348
385
  expect(failed.workerId).toBeNull();
386
+ expect(failed.startedAt).toBeNull(); // cleared on failure for retry
349
387
  expect(failed.availableAt).not.toBeNull();
350
388
  if (!failed.availableAt)
351
389
  throw new Error("Expected availableAt");
@@ -353,20 +391,20 @@ export function testBackend(factory, teardown) {
353
391
  expect(delayMs).toBeGreaterThanOrEqual(900); // ~1s with some tolerance
354
392
  expect(delayMs).toBeLessThan(1500);
355
393
  });
356
- test("reschedules with increasing backoff on multiple failures (known slow test)", async () => {
394
+ test("reschedules with increasing backoff on multiple failures", async () => {
357
395
  // this test needs isolated namespace
358
- const isolatedBackend = await factory();
359
- await createPendingWorkflowRun(isolatedBackend);
396
+ const backend = await setup();
397
+ await createPendingWorkflowRun(backend);
360
398
  // fail first attempt
361
399
  let workerId = randomUUID();
362
- let claimed = await isolatedBackend.claimWorkflowRun({
400
+ let claimed = await backend.claimWorkflowRun({
363
401
  workerId,
364
402
  leaseDurationMs: 20,
365
403
  });
366
404
  if (!claimed)
367
405
  throw new Error("Expected workflow run to be claimed");
368
406
  expect(claimed.attempts).toBe(1);
369
- const firstFailed = await isolatedBackend.failWorkflowRun({
407
+ const firstFailed = await backend.failWorkflowRun({
370
408
  workflowRunId: claimed.id,
371
409
  workerId,
372
410
  error: { message: "first failure" },
@@ -375,7 +413,7 @@ export function testBackend(factory, teardown) {
375
413
  await sleep(1100); // wait for first backoff (~1s)
376
414
  // fail second attempt
377
415
  workerId = randomUUID();
378
- claimed = await isolatedBackend.claimWorkflowRun({
416
+ claimed = await backend.claimWorkflowRun({
379
417
  workerId,
380
418
  leaseDurationMs: 20,
381
419
  });
@@ -383,7 +421,7 @@ export function testBackend(factory, teardown) {
383
421
  throw new Error("Expected workflow run to be claimed");
384
422
  expect(claimed.attempts).toBe(2);
385
423
  const beforeSecondFail = Date.now();
386
- const secondFailed = await isolatedBackend.failWorkflowRun({
424
+ const secondFailed = await backend.failWorkflowRun({
387
425
  workflowRunId: claimed.id,
388
426
  workerId,
389
427
  error: { message: "second failure" },
@@ -395,7 +433,7 @@ export function testBackend(factory, teardown) {
395
433
  const delayMs = secondFailed.availableAt.getTime() - beforeSecondFail;
396
434
  expect(delayMs).toBeGreaterThanOrEqual(1900); // ~2s with some tolerance
397
435
  expect(delayMs).toBeLessThan(2500);
398
- await teardown(isolatedBackend);
436
+ await teardown(backend);
399
437
  });
400
438
  });
401
439
  describe("createStepAttempt()", () => {
@@ -472,6 +510,7 @@ export function testBackend(factory, teardown) {
472
510
  workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion,
473
511
  output: { ok: true },
474
512
  });
513
+ await sleep(10); // ensure timestamp difference
475
514
  const second = await backend.createStepAttempt({
476
515
  workflowRunId: claimed.id,
477
516
  workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
@@ -483,10 +522,8 @@ export function testBackend(factory, teardown) {
483
522
  const listed = await backend.listStepAttempts({
484
523
  workflowRunId: claimed.id,
485
524
  });
486
- expect(listed.data.map((step) => step.stepName)).toEqual([
487
- first.stepName,
488
- second.stepName,
489
- ]);
525
+ const listedStepNames = listed.data.map((step) => step.stepName);
526
+ expect(listedStepNames).toEqual([first.stepName, second.stepName]);
490
527
  });
491
528
  test("paginates step attempts", async () => {
492
529
  const claimed = await createClaimedWorkflowRun(backend);
@@ -572,23 +609,6 @@ export function testBackend(factory, teardown) {
572
609
  expect(listed.pagination.prev).toBeNull();
573
610
  });
574
611
  });
575
- describe("getStepAttempt() duplicate", () => {
576
- test("returns a persisted step attempt", async () => {
577
- const claimed = await createClaimedWorkflowRun(backend);
578
- const created = await backend.createStepAttempt({
579
- workflowRunId: claimed.id,
580
- workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
581
- stepName: randomUUID(),
582
- kind: "function",
583
- config: {},
584
- context: null,
585
- });
586
- const got = await backend.getStepAttempt({
587
- stepAttemptId: created.id,
588
- });
589
- expect(got).toEqual(created);
590
- });
591
- });
592
612
  describe("completeStepAttempt()", () => {
593
613
  test("marks running step attempts as completed", async () => {
594
614
  const claimed = await createClaimedWorkflowRun(backend);
@@ -619,6 +639,50 @@ export function testBackend(factory, teardown) {
619
639
  expect(fetched?.error).toBeNull();
620
640
  expect(fetched?.finishedAt).not.toBeNull();
621
641
  });
642
+ test("throws when workflow is not running", async () => {
643
+ const backend = await setup();
644
+ await createPendingWorkflowRun(backend);
645
+ // create a step attempt by first claiming the workflow
646
+ const claimed = await backend.claimWorkflowRun({
647
+ workerId: randomUUID(),
648
+ leaseDurationMs: 100,
649
+ });
650
+ if (!claimed)
651
+ throw new Error("Failed to claim workflow run");
652
+ const stepAttempt = await backend.createStepAttempt({
653
+ workflowRunId: claimed.id,
654
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
655
+ stepName: randomUUID(),
656
+ kind: "function",
657
+ config: {},
658
+ context: null,
659
+ });
660
+ // complete the workflow so it's no longer running
661
+ await backend.completeWorkflowRun({
662
+ workflowRunId: claimed.id,
663
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
664
+ output: null,
665
+ });
666
+ // try to complete the step attempt
667
+ await expect(backend.completeStepAttempt({
668
+ workflowRunId: claimed.id,
669
+ stepAttemptId: stepAttempt.id,
670
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
671
+ output: { foo: "bar" },
672
+ })).rejects.toThrow("Failed to mark step attempt completed");
673
+ await teardown(backend);
674
+ });
675
+ test("throws when step attempt does not exist", async () => {
676
+ const backend = await setup();
677
+ const claimed = await createClaimedWorkflowRun(backend);
678
+ await expect(backend.completeStepAttempt({
679
+ workflowRunId: claimed.id,
680
+ stepAttemptId: randomUUID(),
681
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
682
+ output: { foo: "bar" },
683
+ })).rejects.toThrow("Failed to mark step attempt completed");
684
+ await teardown(backend);
685
+ });
622
686
  });
623
687
  describe("failStepAttempt()", () => {
624
688
  test("marks running step attempts as failed", async () => {
@@ -650,6 +714,50 @@ export function testBackend(factory, teardown) {
650
714
  expect(fetched?.output).toBeNull();
651
715
  expect(fetched?.finishedAt).not.toBeNull();
652
716
  });
717
+ test("throws when workflow is not running", async () => {
718
+ const backend = await setup();
719
+ await createPendingWorkflowRun(backend);
720
+ // create a step attempt by first claiming the workflow
721
+ const claimed = await backend.claimWorkflowRun({
722
+ workerId: randomUUID(),
723
+ leaseDurationMs: 100,
724
+ });
725
+ if (!claimed)
726
+ throw new Error("Failed to claim workflow run");
727
+ const stepAttempt = await backend.createStepAttempt({
728
+ workflowRunId: claimed.id,
729
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
730
+ stepName: randomUUID(),
731
+ kind: "function",
732
+ config: {},
733
+ context: null,
734
+ });
735
+ // complete the workflow so it's no longer running
736
+ await backend.completeWorkflowRun({
737
+ workflowRunId: claimed.id,
738
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
739
+ output: null,
740
+ });
741
+ // try to fail the step attempt
742
+ await expect(backend.failStepAttempt({
743
+ workflowRunId: claimed.id,
744
+ stepAttemptId: stepAttempt.id,
745
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
746
+ error: { message: "nope" },
747
+ })).rejects.toThrow("Failed to mark step attempt failed");
748
+ await teardown(backend);
749
+ });
750
+ test("throws when step attempt does not exist", async () => {
751
+ const backend = await setup();
752
+ const claimed = await createClaimedWorkflowRun(backend);
753
+ await expect(backend.failStepAttempt({
754
+ workflowRunId: claimed.id,
755
+ stepAttemptId: randomUUID(),
756
+ workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
757
+ error: { message: "nope" },
758
+ })).rejects.toThrow("Failed to mark step attempt failed");
759
+ await teardown(backend);
760
+ });
653
761
  });
654
762
  describe("deadline_at", () => {
655
763
  test("creates a workflow run with a deadline", async () => {
@@ -668,9 +776,9 @@ export function testBackend(factory, teardown) {
668
776
  expect(created.deadlineAt?.getTime()).toBe(deadline.getTime());
669
777
  });
670
778
  test("does not claim workflow runs past their deadline", async () => {
671
- const isolatedBackend = await factory();
779
+ const backend = await setup();
672
780
  const pastDeadline = new Date(Date.now() - 1000);
673
- await isolatedBackend.createWorkflowRun({
781
+ await backend.createWorkflowRun({
674
782
  workflowName: randomUUID(),
675
783
  version: null,
676
784
  idempotencyKey: null,
@@ -680,17 +788,17 @@ export function testBackend(factory, teardown) {
680
788
  availableAt: null,
681
789
  deadlineAt: pastDeadline,
682
790
  });
683
- const claimed = await isolatedBackend.claimWorkflowRun({
791
+ const claimed = await backend.claimWorkflowRun({
684
792
  workerId: randomUUID(),
685
793
  leaseDurationMs: 1000,
686
794
  });
687
795
  expect(claimed).toBeNull();
688
- await teardown(isolatedBackend);
796
+ await teardown(backend);
689
797
  });
690
798
  test("marks deadline-expired workflow runs as failed when claiming", async () => {
691
- const isolatedBackend = await factory();
799
+ const backend = await setup();
692
800
  const pastDeadline = new Date(Date.now() - 1000);
693
- const created = await isolatedBackend.createWorkflowRun({
801
+ const created = await backend.createWorkflowRun({
694
802
  workflowName: randomUUID(),
695
803
  version: null,
696
804
  idempotencyKey: null,
@@ -701,13 +809,13 @@ export function testBackend(factory, teardown) {
701
809
  deadlineAt: pastDeadline,
702
810
  });
703
811
  // attempt to claim triggers deadline check
704
- const claimed = await isolatedBackend.claimWorkflowRun({
812
+ const claimed = await backend.claimWorkflowRun({
705
813
  workerId: randomUUID(),
706
814
  leaseDurationMs: 1000,
707
815
  });
708
816
  expect(claimed).toBeNull();
709
817
  // verify it was marked as failed
710
- const failed = await isolatedBackend.getWorkflowRun({
818
+ const failed = await backend.getWorkflowRun({
711
819
  workflowRunId: created.id,
712
820
  });
713
821
  expect(failed?.status).toBe("failed");
@@ -716,12 +824,12 @@ export function testBackend(factory, teardown) {
716
824
  });
717
825
  expect(failed?.finishedAt).not.toBeNull();
718
826
  expect(failed?.availableAt).toBeNull();
719
- await teardown(isolatedBackend);
827
+ await teardown(backend);
720
828
  });
721
829
  test("does not reschedule failed workflow runs if next retry would exceed deadline", async () => {
722
- const isolatedBackend = await factory();
830
+ const backend = await setup();
723
831
  const deadline = new Date(Date.now() + 500); // 500ms from now
724
- const created = await isolatedBackend.createWorkflowRun({
832
+ const created = await backend.createWorkflowRun({
725
833
  workflowName: randomUUID(),
726
834
  version: null,
727
835
  idempotencyKey: null,
@@ -732,13 +840,13 @@ export function testBackend(factory, teardown) {
732
840
  deadlineAt: deadline,
733
841
  });
734
842
  const workerId = randomUUID();
735
- const claimed = await isolatedBackend.claimWorkflowRun({
843
+ const claimed = await backend.claimWorkflowRun({
736
844
  workerId,
737
845
  leaseDurationMs: 100,
738
846
  });
739
847
  expect(claimed).not.toBeNull();
740
848
  // should mark as permanently failed since retry backoff (1s) would exceed deadline (500ms)
741
- const failed = await isolatedBackend.failWorkflowRun({
849
+ const failed = await backend.failWorkflowRun({
742
850
  workflowRunId: created.id,
743
851
  workerId,
744
852
  error: { message: "test error" },
@@ -746,12 +854,13 @@ export function testBackend(factory, teardown) {
746
854
  expect(failed.status).toBe("failed");
747
855
  expect(failed.availableAt).toBeNull();
748
856
  expect(failed.finishedAt).not.toBeNull();
749
- await teardown(isolatedBackend);
857
+ expect(failed.startedAt).toBeNull(); // cleared on permanent failure
858
+ await teardown(backend);
750
859
  });
751
860
  test("reschedules failed workflow runs if retry would complete before deadline", async () => {
752
- const isolatedBackend = await factory();
861
+ const backend = await setup();
753
862
  const deadline = new Date(Date.now() + 5000); // in 5 seconds
754
- const created = await isolatedBackend.createWorkflowRun({
863
+ const created = await backend.createWorkflowRun({
755
864
  workflowName: randomUUID(),
756
865
  version: null,
757
866
  idempotencyKey: null,
@@ -762,13 +871,13 @@ export function testBackend(factory, teardown) {
762
871
  deadlineAt: deadline,
763
872
  });
764
873
  const workerId = randomUUID();
765
- const claimed = await isolatedBackend.claimWorkflowRun({
874
+ const claimed = await backend.claimWorkflowRun({
766
875
  workerId,
767
876
  leaseDurationMs: 100,
768
877
  });
769
878
  expect(claimed).not.toBeNull();
770
879
  // should reschedule since retry backoff (1s) is before deadline (5s
771
- const failed = await isolatedBackend.failWorkflowRun({
880
+ const failed = await backend.failWorkflowRun({
772
881
  workflowRunId: created.id,
773
882
  workerId,
774
883
  error: { message: "test error" },
@@ -776,15 +885,15 @@ export function testBackend(factory, teardown) {
776
885
  expect(failed.status).toBe("pending");
777
886
  expect(failed.availableAt).not.toBeNull();
778
887
  expect(failed.finishedAt).toBeNull();
779
- await teardown(isolatedBackend);
888
+ await teardown(backend);
780
889
  });
781
890
  });
782
891
  describe("cancelWorkflowRun()", () => {
783
892
  test("cancels a pending workflow run", async () => {
784
- const isolatedBackend = await factory();
785
- const created = await createPendingWorkflowRun(isolatedBackend);
893
+ const backend = await setup();
894
+ const created = await createPendingWorkflowRun(backend);
786
895
  expect(created.status).toBe("pending");
787
- const canceled = await isolatedBackend.cancelWorkflowRun({
896
+ const canceled = await backend.cancelWorkflowRun({
788
897
  workflowRunId: created.id,
789
898
  });
790
899
  expect(canceled.status).toBe("canceled");
@@ -792,60 +901,60 @@ export function testBackend(factory, teardown) {
792
901
  expect(canceled.availableAt).toBeNull();
793
902
  expect(canceled.finishedAt).not.toBeNull();
794
903
  expect(deltaSeconds(canceled.finishedAt)).toBeLessThan(1);
795
- await teardown(isolatedBackend);
904
+ await teardown(backend);
796
905
  });
797
906
  test("cancels a running workflow run", async () => {
798
- const isolatedBackend = await factory();
799
- const created = await createClaimedWorkflowRun(isolatedBackend);
907
+ const backend = await setup();
908
+ const created = await createClaimedWorkflowRun(backend);
800
909
  expect(created.status).toBe("running");
801
910
  expect(created.workerId).not.toBeNull();
802
- const canceled = await isolatedBackend.cancelWorkflowRun({
911
+ const canceled = await backend.cancelWorkflowRun({
803
912
  workflowRunId: created.id,
804
913
  });
805
914
  expect(canceled.status).toBe("canceled");
806
915
  expect(canceled.workerId).toBeNull();
807
916
  expect(canceled.availableAt).toBeNull();
808
917
  expect(canceled.finishedAt).not.toBeNull();
809
- await teardown(isolatedBackend);
918
+ await teardown(backend);
810
919
  });
811
920
  test("cancels a sleeping workflow run", async () => {
812
- const isolatedBackend = await factory();
813
- const claimed = await createClaimedWorkflowRun(isolatedBackend);
921
+ const backend = await setup();
922
+ const claimed = await createClaimedWorkflowRun(backend);
814
923
  // put workflow to sleep
815
924
  const sleepUntil = new Date(Date.now() + 60_000); // 1 minute from now
816
- const sleeping = await isolatedBackend.sleepWorkflowRun({
925
+ const sleeping = await backend.sleepWorkflowRun({
817
926
  workflowRunId: claimed.id,
818
927
  workerId: claimed.workerId ?? "",
819
928
  availableAt: sleepUntil,
820
929
  });
821
930
  expect(sleeping.status).toBe("sleeping");
822
- const canceled = await isolatedBackend.cancelWorkflowRun({
931
+ const canceled = await backend.cancelWorkflowRun({
823
932
  workflowRunId: sleeping.id,
824
933
  });
825
934
  expect(canceled.status).toBe("canceled");
826
935
  expect(canceled.workerId).toBeNull();
827
936
  expect(canceled.availableAt).toBeNull();
828
937
  expect(canceled.finishedAt).not.toBeNull();
829
- await teardown(isolatedBackend);
938
+ await teardown(backend);
830
939
  });
831
940
  test("throws error when canceling a completed workflow run", async () => {
832
- const isolatedBackend = await factory();
833
- const claimed = await createClaimedWorkflowRun(isolatedBackend);
941
+ const backend = await setup();
942
+ const claimed = await createClaimedWorkflowRun(backend);
834
943
  // mark as completed
835
- await isolatedBackend.completeWorkflowRun({
944
+ await backend.completeWorkflowRun({
836
945
  workflowRunId: claimed.id,
837
946
  workerId: claimed.workerId ?? "",
838
947
  output: { result: "success" },
839
948
  });
840
- await expect(isolatedBackend.cancelWorkflowRun({
949
+ await expect(backend.cancelWorkflowRun({
841
950
  workflowRunId: claimed.id,
842
951
  })).rejects.toThrow(/Cannot cancel workflow run .* with status completed/);
843
- await teardown(isolatedBackend);
952
+ await teardown(backend);
844
953
  });
845
954
  test("throws error when canceling a failed workflow run", async () => {
846
- const isolatedBackend = await factory();
955
+ const backend = await setup();
847
956
  // create with deadline that's already passed to make it fail
848
- const workflowWithDeadline = await isolatedBackend.createWorkflowRun({
957
+ const workflowWithDeadline = await backend.createWorkflowRun({
849
958
  workflowName: randomUUID(),
850
959
  version: null,
851
960
  idempotencyKey: null,
@@ -856,105 +965,126 @@ export function testBackend(factory, teardown) {
856
965
  deadlineAt: new Date(Date.now() - 1000), // deadline in the past
857
966
  });
858
967
  // try to claim it, which should mark it as failed due to deadline
859
- const claimed = await isolatedBackend.claimWorkflowRun({
968
+ const claimed = await backend.claimWorkflowRun({
860
969
  workerId: randomUUID(),
861
970
  leaseDurationMs: 100,
862
971
  });
863
972
  // if claim succeeds, manually fail it
864
973
  if (claimed?.workerId) {
865
- await isolatedBackend.failWorkflowRun({
974
+ await backend.failWorkflowRun({
866
975
  workflowRunId: claimed.id,
867
976
  workerId: claimed.workerId,
868
977
  error: { message: "test error" },
869
978
  });
870
979
  }
871
980
  // get a workflow that's definitely failed
872
- const failedRun = await isolatedBackend.getWorkflowRun({
981
+ const failedRun = await backend.getWorkflowRun({
873
982
  workflowRunId: workflowWithDeadline.id,
874
983
  });
875
984
  if (failedRun?.status === "failed") {
876
- await expect(isolatedBackend.cancelWorkflowRun({
985
+ await expect(backend.cancelWorkflowRun({
877
986
  workflowRunId: failedRun.id,
878
987
  })).rejects.toThrow(/Cannot cancel workflow run .* with status failed/);
879
988
  }
880
- await teardown(isolatedBackend);
989
+ await teardown(backend);
881
990
  });
882
991
  test("is idempotent when canceling an already canceled workflow run", async () => {
883
- const isolatedBackend = await factory();
884
- const created = await createPendingWorkflowRun(isolatedBackend);
885
- const firstCancel = await isolatedBackend.cancelWorkflowRun({
992
+ const backend = await setup();
993
+ const created = await createPendingWorkflowRun(backend);
994
+ const firstCancel = await backend.cancelWorkflowRun({
886
995
  workflowRunId: created.id,
887
996
  });
888
997
  expect(firstCancel.status).toBe("canceled");
889
- const secondCancel = await isolatedBackend.cancelWorkflowRun({
998
+ const secondCancel = await backend.cancelWorkflowRun({
890
999
  workflowRunId: created.id,
891
1000
  });
892
1001
  expect(secondCancel.status).toBe("canceled");
893
1002
  expect(secondCancel.id).toBe(firstCancel.id);
894
- await teardown(isolatedBackend);
1003
+ await teardown(backend);
895
1004
  });
896
1005
  test("throws error when canceling a non-existent workflow run", async () => {
897
- const isolatedBackend = await factory();
1006
+ const backend = await setup();
898
1007
  const nonExistentId = randomUUID();
899
- await expect(isolatedBackend.cancelWorkflowRun({
1008
+ await expect(backend.cancelWorkflowRun({
900
1009
  workflowRunId: nonExistentId,
901
1010
  })).rejects.toThrow(`Workflow run ${nonExistentId} does not exist`);
902
- await teardown(isolatedBackend);
1011
+ await teardown(backend);
903
1012
  });
904
1013
  test("canceled workflow is not claimed by workers", async () => {
905
- const isolatedBackend = await factory();
906
- const created = await createPendingWorkflowRun(isolatedBackend);
1014
+ const backend = await setup();
1015
+ const created = await createPendingWorkflowRun(backend);
907
1016
  // cancel the workflow
908
- await isolatedBackend.cancelWorkflowRun({
1017
+ await backend.cancelWorkflowRun({
909
1018
  workflowRunId: created.id,
910
1019
  });
911
1020
  // try to claim work
912
- const claimed = await isolatedBackend.claimWorkflowRun({
1021
+ const claimed = await backend.claimWorkflowRun({
913
1022
  workerId: randomUUID(),
914
1023
  leaseDurationMs: 100,
915
1024
  });
916
1025
  // should not claim the canceled workflow
917
1026
  expect(claimed).toBeNull();
918
- await teardown(isolatedBackend);
1027
+ await teardown(backend);
919
1028
  });
920
1029
  });
921
- // Helper function for creating workflow runs that uses the shared backend
922
- async function createPendingWorkflowRun(b) {
923
- return await b.createWorkflowRun({
924
- workflowName: randomUUID(),
925
- version: null,
926
- idempotencyKey: null,
927
- input: null,
928
- config: {},
929
- context: null,
930
- availableAt: null,
931
- deadlineAt: null,
932
- });
933
- }
934
- async function createClaimedWorkflowRun(b) {
935
- await createPendingWorkflowRun(b);
936
- const claimed = await b.claimWorkflowRun({
937
- workerId: randomUUID(),
938
- leaseDurationMs: 100,
939
- });
940
- if (!claimed)
941
- throw new Error("Failed to claim workflow run");
942
- return claimed;
943
- }
944
1030
  });
945
1031
  }
946
- // Helper functions
1032
+ /**
1033
+ * Create a pending workflow run for tests.
1034
+ * @param b - Backend
1035
+ * @returns Created workflow run
1036
+ */
1037
+ async function createPendingWorkflowRun(b) {
1038
+ return await b.createWorkflowRun({
1039
+ workflowName: randomUUID(),
1040
+ version: null,
1041
+ idempotencyKey: null,
1042
+ input: null,
1043
+ config: {},
1044
+ context: null,
1045
+ availableAt: null,
1046
+ deadlineAt: null,
1047
+ });
1048
+ }
1049
+ /**
1050
+ * Create and claim a workflow run for tests.
1051
+ * @param b - Backend
1052
+ * @returns Claimed workflow run
1053
+ */
1054
+ async function createClaimedWorkflowRun(b) {
1055
+ await createPendingWorkflowRun(b);
1056
+ const claimed = await b.claimWorkflowRun({
1057
+ workerId: randomUUID(),
1058
+ leaseDurationMs: 100,
1059
+ });
1060
+ if (!claimed)
1061
+ throw new Error("Failed to claim workflow run");
1062
+ return claimed;
1063
+ }
1064
+ /**
1065
+ * Get delta in seconds from now.
1066
+ * @param date - Date to compare
1067
+ * @returns Delta in seconds
1068
+ */
947
1069
  function deltaSeconds(date) {
948
1070
  if (!date)
949
1071
  return Infinity;
950
1072
  return Math.abs((Date.now() - date.getTime()) / 1000);
951
1073
  }
1074
+ /**
1075
+ * Create a Date one year in the future.
1076
+ * @returns Future Date
1077
+ */
952
1078
  function newDateInOneYear() {
953
1079
  const d = new Date();
954
1080
  d.setFullYear(d.getFullYear() + 1);
955
1081
  return d;
956
1082
  }
1083
+ /**
1084
+ * Sleep for a given duration.
1085
+ * @param ms - Milliseconds to sleep
1086
+ * @returns Promise resolved after sleeping
1087
+ */
957
1088
  function sleep(ms) {
958
1089
  return new Promise((resolve) => setTimeout(resolve, ms));
959
1090
  }
960
- //# sourceMappingURL=backend-test-suite.js.map