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
@@ -2,10 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
3
3
  /**
4
4
  * Runs the Backend 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 setup function to
8
- * create backend instances and the teardown function to clean them up.
5
+ * @param options - Test suite options
9
6
  */
10
7
  export function testBackend(options) {
11
8
  const { setup, teardown } = options;
@@ -90,7 +87,8 @@ export function testBackend(options) {
90
87
  await sleep(10); // ensure timestamp difference
91
88
  const second = await createPendingWorkflowRun(backend);
92
89
  const listed = await backend.listWorkflowRuns({});
93
- expect(listed.data.map((run) => run.id)).toEqual([first.id, second.id]);
90
+ const listedIds = listed.data.map((run) => run.id);
91
+ expect(listedIds).toEqual([second.id, first.id]);
94
92
  await teardown(backend);
95
93
  });
96
94
  test("paginates workflow runs", async () => {
@@ -103,8 +101,8 @@ export function testBackend(options) {
103
101
  // p1
104
102
  const page1 = await backend.listWorkflowRuns({ limit: 2 });
105
103
  expect(page1.data).toHaveLength(2);
106
- expect(page1.data[0]?.id).toBe(runs[0]?.id);
107
- 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);
108
106
  expect(page1.pagination.next).not.toBeNull();
109
107
  expect(page1.pagination.prev).toBeNull();
110
108
  // p2
@@ -114,7 +112,7 @@ export function testBackend(options) {
114
112
  });
115
113
  expect(page2.data).toHaveLength(2);
116
114
  expect(page2.data[0]?.id).toBe(runs[2]?.id);
117
- expect(page2.data[1]?.id).toBe(runs[3]?.id);
115
+ expect(page2.data[1]?.id).toBe(runs[1]?.id);
118
116
  expect(page2.pagination.next).not.toBeNull();
119
117
  expect(page2.pagination.prev).not.toBeNull();
120
118
  // p3
@@ -123,7 +121,7 @@ export function testBackend(options) {
123
121
  after: page2.pagination.next, // eslint-disable-line @typescript-eslint/no-non-null-assertion
124
122
  });
125
123
  expect(page3.data).toHaveLength(1);
126
- expect(page3.data[0]?.id).toBe(runs[4]?.id);
124
+ expect(page3.data[0]?.id).toBe(runs[0]?.id);
127
125
  expect(page3.pagination.next).toBeNull();
128
126
  expect(page3.pagination.prev).not.toBeNull();
129
127
  // p2 again
@@ -133,7 +131,7 @@ export function testBackend(options) {
133
131
  });
134
132
  expect(page2Back.data).toHaveLength(2);
135
133
  expect(page2Back.data[0]?.id).toBe(runs[2]?.id);
136
- expect(page2Back.data[1]?.id).toBe(runs[3]?.id);
134
+ expect(page2Back.data[1]?.id).toBe(runs[1]?.id);
137
135
  expect(page2Back.pagination.next).toEqual(page2.pagination.next);
138
136
  expect(page2Back.pagination.prev).toEqual(page2.pagination.prev);
139
137
  await teardown(backend);
@@ -146,6 +144,47 @@ export function testBackend(options) {
146
144
  expect(listed.pagination.prev).toBeNull();
147
145
  await teardown(backend);
148
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);
187
+ });
149
188
  });
150
189
  describe("claimWorkflowRun()", () => {
151
190
  // because claims involve timing and leases, we create and teardown a new
@@ -169,7 +208,7 @@ export function testBackend(options) {
169
208
  leaseDurationMs: 10,
170
209
  });
171
210
  expect(blocked).toBeNull();
172
- await sleep(firstLeaseMs);
211
+ await sleep(firstLeaseMs + 5); // small buffer for timing variability
173
212
  const reclaimed = await backend.claimWorkflowRun({
174
213
  workerId: secondWorker,
175
214
  leaseDurationMs: 10,
@@ -278,7 +317,7 @@ export function testBackend(options) {
278
317
  await backend.failWorkflowRun({
279
318
  workflowRunId: claimed.id,
280
319
  workerId: claimed.workerId ?? "",
281
- error: null,
320
+ error: { message: "failed" },
282
321
  });
283
322
  await expect(backend.sleepWorkflowRun({
284
323
  workflowRunId: claimed.id,
@@ -344,6 +383,7 @@ export function testBackend(options) {
344
383
  expect(failed.output).toBeNull();
345
384
  expect(failed.finishedAt).toBeNull();
346
385
  expect(failed.workerId).toBeNull();
386
+ expect(failed.startedAt).toBeNull(); // cleared on failure for retry
347
387
  expect(failed.availableAt).not.toBeNull();
348
388
  if (!failed.availableAt)
349
389
  throw new Error("Expected availableAt");
@@ -351,7 +391,7 @@ export function testBackend(options) {
351
391
  expect(delayMs).toBeGreaterThanOrEqual(900); // ~1s with some tolerance
352
392
  expect(delayMs).toBeLessThan(1500);
353
393
  });
354
- test("reschedules with increasing backoff on multiple failures (known slow test)", async () => {
394
+ test("reschedules with increasing backoff on multiple failures", async () => {
355
395
  // this test needs isolated namespace
356
396
  const backend = await setup();
357
397
  await createPendingWorkflowRun(backend);
@@ -470,6 +510,7 @@ export function testBackend(options) {
470
510
  workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion,
471
511
  output: { ok: true },
472
512
  });
513
+ await sleep(10); // ensure timestamp difference
473
514
  const second = await backend.createStepAttempt({
474
515
  workflowRunId: claimed.id,
475
516
  workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
@@ -481,10 +522,8 @@ export function testBackend(options) {
481
522
  const listed = await backend.listStepAttempts({
482
523
  workflowRunId: claimed.id,
483
524
  });
484
- expect(listed.data.map((step) => step.stepName)).toEqual([
485
- first.stepName,
486
- second.stepName,
487
- ]);
525
+ const listedStepNames = listed.data.map((step) => step.stepName);
526
+ expect(listedStepNames).toEqual([first.stepName, second.stepName]);
488
527
  });
489
528
  test("paginates step attempts", async () => {
490
529
  const claimed = await createClaimedWorkflowRun(backend);
@@ -570,23 +609,6 @@ export function testBackend(options) {
570
609
  expect(listed.pagination.prev).toBeNull();
571
610
  });
572
611
  });
573
- describe("getStepAttempt() duplicate", () => {
574
- test("returns a persisted step attempt", async () => {
575
- const claimed = await createClaimedWorkflowRun(backend);
576
- const created = await backend.createStepAttempt({
577
- workflowRunId: claimed.id,
578
- workerId: claimed.workerId, // eslint-disable-line @typescript-eslint/no-non-null-assertion
579
- stepName: randomUUID(),
580
- kind: "function",
581
- config: {},
582
- context: null,
583
- });
584
- const got = await backend.getStepAttempt({
585
- stepAttemptId: created.id,
586
- });
587
- expect(got).toEqual(created);
588
- });
589
- });
590
612
  describe("completeStepAttempt()", () => {
591
613
  test("marks running step attempts as completed", async () => {
592
614
  const claimed = await createClaimedWorkflowRun(backend);
@@ -617,6 +639,50 @@ export function testBackend(options) {
617
639
  expect(fetched?.error).toBeNull();
618
640
  expect(fetched?.finishedAt).not.toBeNull();
619
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
+ });
620
686
  });
621
687
  describe("failStepAttempt()", () => {
622
688
  test("marks running step attempts as failed", async () => {
@@ -648,6 +714,50 @@ export function testBackend(options) {
648
714
  expect(fetched?.output).toBeNull();
649
715
  expect(fetched?.finishedAt).not.toBeNull();
650
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
+ });
651
761
  });
652
762
  describe("deadline_at", () => {
653
763
  test("creates a workflow run with a deadline", async () => {
@@ -744,6 +854,7 @@ export function testBackend(options) {
744
854
  expect(failed.status).toBe("failed");
745
855
  expect(failed.availableAt).toBeNull();
746
856
  expect(failed.finishedAt).not.toBeNull();
857
+ expect(failed.startedAt).toBeNull(); // cleared on permanent failure
747
858
  await teardown(backend);
748
859
  });
749
860
  test("reschedules failed workflow runs if retry would complete before deadline", async () => {
@@ -916,43 +1027,64 @@ export function testBackend(options) {
916
1027
  await teardown(backend);
917
1028
  });
918
1029
  });
919
- // Helper function for creating workflow runs that uses the shared backend
920
- async function createPendingWorkflowRun(b) {
921
- return await b.createWorkflowRun({
922
- workflowName: randomUUID(),
923
- version: null,
924
- idempotencyKey: null,
925
- input: null,
926
- config: {},
927
- context: null,
928
- availableAt: null,
929
- deadlineAt: null,
930
- });
931
- }
932
- async function createClaimedWorkflowRun(b) {
933
- await createPendingWorkflowRun(b);
934
- const claimed = await b.claimWorkflowRun({
935
- workerId: randomUUID(),
936
- leaseDurationMs: 100,
937
- });
938
- if (!claimed)
939
- throw new Error("Failed to claim workflow run");
940
- return claimed;
941
- }
942
1030
  });
943
1031
  }
944
- // 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
+ */
945
1069
  function deltaSeconds(date) {
946
1070
  if (!date)
947
1071
  return Infinity;
948
1072
  return Math.abs((Date.now() - date.getTime()) / 1000);
949
1073
  }
1074
+ /**
1075
+ * Create a Date one year in the future.
1076
+ * @returns Future Date
1077
+ */
950
1078
  function newDateInOneYear() {
951
1079
  const d = new Date();
952
1080
  d.setFullYear(d.getFullYear() + 1);
953
1081
  return d;
954
1082
  }
1083
+ /**
1084
+ * Sleep for a given duration.
1085
+ * @param ms - Milliseconds to sleep
1086
+ * @returns Promise resolved after sleeping
1087
+ */
955
1088
  function sleep(ms) {
956
1089
  return new Promise((resolve) => setTimeout(resolve, ms));
957
1090
  }
958
- //# sourceMappingURL=backend.testsuite.js.map
@@ -0,0 +1,2 @@
1
+ export { testBackend } from "./backend.testsuite.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../backend-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1 @@
1
+ export { testBackend } from "./backend.testsuite.js";
@@ -1,6 +1,7 @@
1
- import { JsonValue } from "./json.js";
2
- import type { StepAttempt, StepAttemptContext, StepKind } from "./step.js";
3
- import type { WorkflowRun } from "./workflow.js";
1
+ import type { SerializedError } from "./core/error.js";
2
+ import { JsonValue } from "./core/json.js";
3
+ import type { StepAttempt, StepAttemptContext, StepKind } from "./core/step.js";
4
+ import type { WorkflowRun } from "./core/workflow.js";
4
5
  export declare const DEFAULT_NAMESPACE_ID = "default";
5
6
  /**
6
7
  * Backend is the interface for backend providers to implement.
@@ -20,6 +21,7 @@ export interface Backend {
20
21
  listStepAttempts(params: Readonly<ListStepAttemptsParams>): Promise<PaginatedResponse<StepAttempt>>;
21
22
  completeStepAttempt(params: Readonly<CompleteStepAttemptParams>): Promise<StepAttempt>;
22
23
  failStepAttempt(params: Readonly<FailStepAttemptParams>): Promise<StepAttempt>;
24
+ stop(): Promise<void>;
23
25
  }
24
26
  export interface CreateWorkflowRunParams {
25
27
  workflowName: string;
@@ -57,7 +59,7 @@ export interface CompleteWorkflowRunParams {
57
59
  export interface FailWorkflowRunParams {
58
60
  workflowRunId: string;
59
61
  workerId: string;
60
- error: JsonValue;
62
+ error: SerializedError;
61
63
  }
62
64
  export interface CancelWorkflowRunParams {
63
65
  workflowRunId: string;
@@ -86,7 +88,7 @@ export interface FailStepAttemptParams {
86
88
  workflowRunId: string;
87
89
  stepAttemptId: string;
88
90
  workerId: string;
89
- error: JsonValue;
91
+ error: SerializedError;
90
92
  }
91
93
  export interface PaginationOptions {
92
94
  limit?: number;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../backend.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1 @@
1
1
  export const DEFAULT_NAMESPACE_ID = "default";
2
- //# sourceMappingURL=backend.js.map
@@ -0,0 +1,20 @@
1
+ import type { Backend } from "./backend.js";
2
+ /**
3
+ * Options for the Backend test suite.
4
+ */
5
+ export interface TestBackendOptions {
6
+ /**
7
+ * Creates a new isolated Backend instance.
8
+ */
9
+ setup: () => Promise<Backend>;
10
+ /**
11
+ * Cleans up a Backend instance.
12
+ */
13
+ teardown: (backend: Backend) => Promise<void>;
14
+ }
15
+ /**
16
+ * Runs the Backend test suite.
17
+ * @param options - Test suite options
18
+ */
19
+ export declare function testBackend(options: TestBackendOptions): void;
20
+ //# sourceMappingURL=backend.testsuite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.testsuite.d.ts","sourceRoot":"","sources":["../backend.testsuite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAM5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B;;OAEG;IACH,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAgtC7D"}