openworkflow 0.4.1 → 0.5.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 (193) hide show
  1. package/README.md +53 -48
  2. package/dist/backend-postgres/index.d.ts +44 -0
  3. package/dist/backend-postgres/index.d.ts.map +1 -0
  4. package/dist/backend-postgres/index.js +535 -0
  5. package/dist/backend-postgres/index.js.map +1 -0
  6. package/dist/backend-postgres/postgres.d.ts +42 -0
  7. package/dist/backend-postgres/postgres.d.ts.map +1 -0
  8. package/dist/backend-postgres/postgres.js +234 -0
  9. package/dist/backend-postgres/postgres.js.map +1 -0
  10. package/dist/backend-sqlite/index.d.ts +41 -1
  11. package/dist/backend-sqlite/index.d.ts.map +1 -1
  12. package/dist/backend-sqlite/index.js +654 -1
  13. package/dist/backend-sqlite/index.js.map +1 -1
  14. package/dist/backend-sqlite/sqlite.d.ts +18 -2
  15. package/dist/backend-sqlite/sqlite.d.ts.map +1 -1
  16. package/dist/backend-sqlite/sqlite.js +20 -2
  17. package/dist/backend-sqlite/sqlite.js.map +1 -1
  18. package/dist/{core/backend.d.ts → backend.d.ts} +7 -5
  19. package/dist/backend.d.ts.map +1 -0
  20. package/dist/backend.js.map +1 -0
  21. package/dist/bin/openworkflow.d.ts +3 -0
  22. package/dist/bin/openworkflow.d.ts.map +1 -0
  23. package/dist/bin/openworkflow.js +44 -0
  24. package/dist/bin/openworkflow.js.map +1 -0
  25. package/dist/client.d.ts +141 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/{sdk/sdk.js → client.js} +44 -71
  28. package/dist/client.js.map +1 -0
  29. package/dist/config.d.ts +27 -21
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +44 -36
  32. package/dist/config.js.map +1 -1
  33. package/dist/core/duration.d.ts +4 -2
  34. package/dist/core/duration.d.ts.map +1 -1
  35. package/dist/core/duration.js +3 -1
  36. package/dist/core/duration.js.map +1 -1
  37. package/dist/core/error.d.ts +14 -0
  38. package/dist/core/error.d.ts.map +1 -0
  39. package/dist/core/error.js +18 -0
  40. package/dist/core/error.js.map +1 -0
  41. package/dist/core/result.d.ts +14 -4
  42. package/dist/core/result.d.ts.map +1 -1
  43. package/dist/core/result.js +10 -0
  44. package/dist/core/result.js.map +1 -1
  45. package/dist/core/retry.d.ts +0 -9
  46. package/dist/core/retry.d.ts.map +1 -1
  47. package/dist/core/retry.js +0 -14
  48. package/dist/core/retry.js.map +1 -1
  49. package/dist/core/step.d.ts +1 -32
  50. package/dist/core/step.d.ts.map +1 -1
  51. package/dist/core/step.js +0 -35
  52. package/dist/core/step.js.map +1 -1
  53. package/dist/core/workflow.d.ts +2 -47
  54. package/dist/core/workflow.d.ts.map +1 -1
  55. package/dist/core/workflow.js +0 -44
  56. package/dist/core/workflow.js.map +1 -1
  57. package/dist/{execution/execution.d.ts → execution.d.ts} +4 -26
  58. package/dist/execution.d.ts.map +1 -0
  59. package/dist/{execution/execution.js → execution.js} +4 -4
  60. package/dist/execution.js.map +1 -0
  61. package/dist/index.d.ts +6 -9
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +4 -4
  64. package/dist/index.js.map +1 -1
  65. package/dist/internal.d.ts +8 -0
  66. package/dist/internal.d.ts.map +1 -0
  67. package/dist/internal.js +5 -0
  68. package/dist/internal.js.map +1 -0
  69. package/dist/pg/backend.d.ts +42 -0
  70. package/dist/pg/backend.d.ts.map +1 -0
  71. package/dist/pg/backend.js +534 -0
  72. package/dist/pg/backend.js.map +1 -0
  73. package/dist/pg/index.d.ts +3 -0
  74. package/dist/pg/index.d.ts.map +1 -0
  75. package/dist/pg/index.js +3 -0
  76. package/dist/pg/index.js.map +1 -0
  77. package/dist/pg/postgres.d.ts +42 -0
  78. package/dist/pg/postgres.d.ts.map +1 -0
  79. package/dist/pg/postgres.js +234 -0
  80. package/dist/pg/postgres.js.map +1 -0
  81. package/dist/pg/scripts/db-migrate.d.ts +2 -0
  82. package/dist/pg/scripts/db-migrate.d.ts.map +1 -0
  83. package/dist/pg/scripts/db-migrate.js +5 -0
  84. package/dist/pg/scripts/db-migrate.js.map +1 -0
  85. package/dist/pg/scripts/db-reset.d.ts +2 -0
  86. package/dist/pg/scripts/db-reset.d.ts.map +1 -0
  87. package/dist/pg/scripts/db-reset.js +6 -0
  88. package/dist/pg/scripts/db-reset.js.map +1 -0
  89. package/dist/pg/scripts/squawk.d.ts +2 -0
  90. package/dist/pg/scripts/squawk.d.ts.map +1 -0
  91. package/dist/pg/scripts/squawk.js +17 -0
  92. package/dist/pg/scripts/squawk.js.map +1 -0
  93. package/dist/pg/vitest.global-setup.d.ts +3 -0
  94. package/dist/pg/vitest.global-setup.d.ts.map +1 -0
  95. package/dist/pg/vitest.global-setup.js +8 -0
  96. package/dist/pg/vitest.global-setup.js.map +1 -0
  97. package/dist/registry.d.ts +27 -0
  98. package/dist/registry.d.ts.map +1 -0
  99. package/dist/registry.js +49 -0
  100. package/dist/registry.js.map +1 -0
  101. package/dist/{backend-sqlite → sqlite}/backend.d.ts +6 -4
  102. package/dist/sqlite/backend.d.ts.map +1 -0
  103. package/dist/{backend-sqlite → sqlite}/backend.js +31 -5
  104. package/dist/sqlite/backend.js.map +1 -0
  105. package/dist/sqlite/index.d.ts +3 -0
  106. package/dist/sqlite/index.d.ts.map +1 -0
  107. package/dist/sqlite/index.js +3 -0
  108. package/dist/sqlite/index.js.map +1 -0
  109. package/dist/sqlite/sqlite.d.ts +61 -0
  110. package/dist/sqlite/sqlite.d.ts.map +1 -0
  111. package/dist/sqlite/sqlite.js +247 -0
  112. package/dist/sqlite/sqlite.js.map +1 -0
  113. package/dist/testing/backend.testsuite.d.ts +20 -0
  114. package/dist/testing/backend.testsuite.d.ts.map +1 -0
  115. package/dist/{core → testing}/backend.testsuite.js +186 -53
  116. package/dist/testing/backend.testsuite.js.map +1 -0
  117. package/dist/testing/index.d.ts +2 -0
  118. package/dist/testing/index.d.ts.map +1 -0
  119. package/dist/testing/index.js +2 -0
  120. package/dist/testing/index.js.map +1 -0
  121. package/dist/tsconfig.tsbuildinfo +1 -1
  122. package/dist/{worker/worker.d.ts → worker.d.ts} +11 -4
  123. package/dist/worker.d.ts.map +1 -0
  124. package/dist/{worker/worker.js → worker.js} +20 -10
  125. package/dist/worker.js.map +1 -0
  126. package/dist/workflow.d.ts +60 -0
  127. package/dist/workflow.d.ts.map +1 -0
  128. package/dist/workflow.js +49 -0
  129. package/dist/workflow.js.map +1 -0
  130. package/package.json +13 -3
  131. package/dist/backend-sqlite/backend.d.ts.map +0 -1
  132. package/dist/backend-sqlite/backend.js.map +0 -1
  133. package/dist/config/config.d.ts +0 -102
  134. package/dist/config/config.d.ts.map +0 -1
  135. package/dist/config/config.js +0 -29
  136. package/dist/config/config.js.map +0 -1
  137. package/dist/config/index.d.ts +0 -3
  138. package/dist/config/index.d.ts.map +0 -1
  139. package/dist/config/index.js +0 -2
  140. package/dist/config/index.js.map +0 -1
  141. package/dist/core/backend-test-suite.d.ts +0 -22
  142. package/dist/core/backend-test-suite.d.ts.map +0 -1
  143. package/dist/core/backend-test-suite.js +0 -960
  144. package/dist/core/backend-test-suite.js.map +0 -1
  145. package/dist/core/backend.d.ts.map +0 -1
  146. package/dist/core/backend.js.map +0 -1
  147. package/dist/core/backend.testsuite.d.ts +0 -21
  148. package/dist/core/backend.testsuite.d.ts.map +0 -1
  149. package/dist/core/backend.testsuite.js.map +0 -1
  150. package/dist/core/duration.test.d.ts +0 -2
  151. package/dist/core/duration.test.d.ts.map +0 -1
  152. package/dist/core/duration.test.js +0 -264
  153. package/dist/core/duration.test.js.map +0 -1
  154. package/dist/core/result.test.d.ts +0 -2
  155. package/dist/core/result.test.d.ts.map +0 -1
  156. package/dist/core/result.test.js +0 -11
  157. package/dist/core/result.test.js.map +0 -1
  158. package/dist/core/retry.test.d.ts +0 -2
  159. package/dist/core/retry.test.d.ts.map +0 -1
  160. package/dist/core/retry.test.js +0 -36
  161. package/dist/core/retry.test.js.map +0 -1
  162. package/dist/core/step.test.d.ts +0 -2
  163. package/dist/core/step.test.d.ts.map +0 -1
  164. package/dist/core/step.test.js +0 -340
  165. package/dist/core/step.test.js.map +0 -1
  166. package/dist/core/workflow.test.d.ts +0 -2
  167. package/dist/core/workflow.test.d.ts.map +0 -1
  168. package/dist/core/workflow.test.js +0 -216
  169. package/dist/core/workflow.test.js.map +0 -1
  170. package/dist/execution/execution.d.ts.map +0 -1
  171. package/dist/execution/execution.js.map +0 -1
  172. package/dist/execution/execution.test.d.ts +0 -2
  173. package/dist/execution/execution.test.d.ts.map +0 -1
  174. package/dist/execution/execution.test.js +0 -382
  175. package/dist/execution/execution.test.js.map +0 -1
  176. package/dist/global.d.ts +0 -62
  177. package/dist/global.d.ts.map +0 -1
  178. package/dist/global.js +0 -78
  179. package/dist/global.js.map +0 -1
  180. package/dist/sdk/sdk.d.ts +0 -182
  181. package/dist/sdk/sdk.d.ts.map +0 -1
  182. package/dist/sdk/sdk.js.map +0 -1
  183. package/dist/sdk/sdk.test.d.ts +0 -2
  184. package/dist/sdk/sdk.test.d.ts.map +0 -1
  185. package/dist/sdk/sdk.test.js +0 -195
  186. package/dist/sdk/sdk.test.js.map +0 -1
  187. package/dist/worker/worker.d.ts.map +0 -1
  188. package/dist/worker/worker.js.map +0 -1
  189. package/dist/worker/worker.test.d.ts +0 -2
  190. package/dist/worker/worker.test.d.ts.map +0 -1
  191. package/dist/worker/worker.test.js +0 -786
  192. package/dist/worker/worker.test.js.map +0 -1
  193. /package/dist/{core/backend.js → backend.js} +0 -0
@@ -0,0 +1,247 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ /**
4
+ * newDatabase creates a new SQLite database connection.
5
+ * @param path - Database file path (or ":memory:") for testing
6
+ * @returns SQLite database connection
7
+ */
8
+ export function newDatabase(path) {
9
+ const db = new DatabaseSync(path);
10
+ // Only enable WAL mode for file-based databases
11
+ if (path !== ":memory:") {
12
+ db.exec("PRAGMA journal_mode = WAL;");
13
+ }
14
+ db.exec("PRAGMA foreign_keys = ON;");
15
+ return db;
16
+ }
17
+ /**
18
+ * migrations returns the list of migration SQL statements.
19
+ * @returns Migration SQL statements
20
+ */
21
+ export function migrations() {
22
+ return [
23
+ // 0 - init
24
+ `BEGIN;
25
+
26
+ CREATE TABLE IF NOT EXISTS "openworkflow_migrations" (
27
+ "version" INTEGER NOT NULL PRIMARY KEY
28
+ );
29
+
30
+ INSERT OR IGNORE INTO "openworkflow_migrations" ("version")
31
+ VALUES (0);
32
+
33
+ COMMIT;`,
34
+ // 1 - add workflow_runs and step_attempts tables
35
+ `BEGIN;
36
+
37
+ PRAGMA defer_foreign_keys = ON;
38
+
39
+ CREATE TABLE IF NOT EXISTS "workflow_runs" (
40
+ "namespace_id" TEXT NOT NULL,
41
+ "id" TEXT NOT NULL,
42
+ --
43
+ "workflow_name" TEXT NOT NULL,
44
+ "version" TEXT,
45
+ "status" TEXT NOT NULL,
46
+ "idempotency_key" TEXT,
47
+ "config" TEXT NOT NULL,
48
+ "context" TEXT,
49
+ "input" TEXT,
50
+ "output" TEXT,
51
+ "error" TEXT,
52
+ "attempts" INTEGER NOT NULL,
53
+ "parent_step_attempt_namespace_id" TEXT,
54
+ "parent_step_attempt_id" TEXT,
55
+ "worker_id" TEXT,
56
+ "available_at" TEXT,
57
+ "deadline_at" TEXT,
58
+ "started_at" TEXT,
59
+ "finished_at" TEXT,
60
+ "created_at" TEXT NOT NULL,
61
+ "updated_at" TEXT NOT NULL,
62
+ PRIMARY KEY ("namespace_id", "id"),
63
+ FOREIGN KEY ("parent_step_attempt_namespace_id", "parent_step_attempt_id")
64
+ REFERENCES "step_attempts" ("namespace_id", "id")
65
+ ON DELETE SET NULL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS "step_attempts" (
69
+ "namespace_id" TEXT NOT NULL,
70
+ "id" TEXT NOT NULL,
71
+ --
72
+ "workflow_run_id" TEXT NOT NULL,
73
+ "step_name" TEXT NOT NULL,
74
+ "kind" TEXT NOT NULL,
75
+ "status" TEXT NOT NULL,
76
+ "config" TEXT NOT NULL,
77
+ "context" TEXT,
78
+ "output" TEXT,
79
+ "error" TEXT,
80
+ "child_workflow_run_namespace_id" TEXT,
81
+ "child_workflow_run_id" TEXT,
82
+ "started_at" TEXT,
83
+ "finished_at" TEXT,
84
+ "created_at" TEXT NOT NULL,
85
+ "updated_at" TEXT NOT NULL,
86
+ PRIMARY KEY ("namespace_id", "id"),
87
+ FOREIGN KEY ("namespace_id", "workflow_run_id")
88
+ REFERENCES "workflow_runs" ("namespace_id", "id")
89
+ ON DELETE CASCADE,
90
+ FOREIGN KEY ("child_workflow_run_namespace_id", "child_workflow_run_id")
91
+ REFERENCES "workflow_runs" ("namespace_id", "id")
92
+ ON DELETE SET NULL
93
+ );
94
+
95
+ INSERT OR IGNORE INTO "openworkflow_migrations" ("version")
96
+ VALUES (1);
97
+
98
+ COMMIT;`,
99
+ // 2 - foreign keys
100
+ `BEGIN;
101
+
102
+ -- Foreign keys are defined in migration 1 since SQLite requires them during table creation
103
+ -- This migration exists for version parity with PostgreSQL backend
104
+
105
+ INSERT OR IGNORE INTO "openworkflow_migrations" ("version")
106
+ VALUES (2);
107
+
108
+ COMMIT;`,
109
+ // 3 - validate foreign keys
110
+ `BEGIN;
111
+
112
+ -- Foreign key validation happens automatically in SQLite when PRAGMA foreign_keys = ON
113
+ -- This migration exists for version parity with PostgreSQL backend
114
+
115
+ INSERT OR IGNORE INTO "openworkflow_migrations" ("version")
116
+ VALUES (3);
117
+
118
+ COMMIT;`,
119
+ // 4 - indexes
120
+ `BEGIN;
121
+
122
+ CREATE INDEX IF NOT EXISTS "workflow_runs_status_available_at_created_at_idx"
123
+ ON "workflow_runs" ("namespace_id", "status", "available_at", "created_at");
124
+
125
+ CREATE INDEX IF NOT EXISTS "workflow_runs_workflow_name_idempotency_key_created_at_idx"
126
+ ON "workflow_runs" ("namespace_id", "workflow_name", "idempotency_key", "created_at");
127
+
128
+ CREATE INDEX IF NOT EXISTS "workflow_runs_parent_step_idx"
129
+ ON "workflow_runs" ("parent_step_attempt_namespace_id", "parent_step_attempt_id")
130
+ WHERE parent_step_attempt_namespace_id IS NOT NULL AND parent_step_attempt_id IS NOT NULL;
131
+
132
+ CREATE INDEX IF NOT EXISTS "workflow_runs_created_at_desc_idx"
133
+ ON "workflow_runs" ("namespace_id", "created_at" DESC);
134
+
135
+ CREATE INDEX IF NOT EXISTS "workflow_runs_status_created_at_desc_idx"
136
+ ON "workflow_runs" ("namespace_id", "status", "created_at" DESC);
137
+
138
+ CREATE INDEX IF NOT EXISTS "workflow_runs_workflow_name_status_created_at_desc_idx"
139
+ ON "workflow_runs" ("namespace_id", "workflow_name", "status", "created_at" DESC);
140
+
141
+ CREATE INDEX IF NOT EXISTS "step_attempts_workflow_run_created_at_idx"
142
+ ON "step_attempts" ("namespace_id", "workflow_run_id", "created_at");
143
+
144
+ CREATE INDEX IF NOT EXISTS "step_attempts_workflow_run_step_name_created_at_idx"
145
+ ON "step_attempts" ("namespace_id", "workflow_run_id", "step_name", "created_at");
146
+
147
+ CREATE INDEX IF NOT EXISTS "step_attempts_child_workflow_run_idx"
148
+ ON "step_attempts" ("child_workflow_run_namespace_id", "child_workflow_run_id")
149
+ WHERE child_workflow_run_namespace_id IS NOT NULL AND child_workflow_run_id IS NOT NULL;
150
+
151
+ INSERT OR IGNORE INTO "openworkflow_migrations" ("version")
152
+ VALUES (4);
153
+
154
+ COMMIT;`,
155
+ ];
156
+ }
157
+ /**
158
+ * migrate applies pending migrations to the database. Does nothing if the
159
+ * database is already up to date.
160
+ * @param db - SQLite database
161
+ */
162
+ export function migrate(db) {
163
+ const currentMigrationVersion = getCurrentMigrationVersion(db);
164
+ for (const [i, migrationSql] of migrations().entries()) {
165
+ if (i <= currentMigrationVersion)
166
+ continue; // already applied
167
+ db.exec(migrationSql);
168
+ }
169
+ }
170
+ /**
171
+ * getCurrentMigrationVersion returns the current migration version of the database.
172
+ * @param db - SQLite database
173
+ * @returns Current migration version
174
+ */
175
+ function getCurrentMigrationVersion(db) {
176
+ // check if migrations table exists
177
+ const existsStmt = db.prepare(`
178
+ SELECT COUNT(*) as count
179
+ FROM sqlite_master
180
+ WHERE type = 'table' AND name = 'openworkflow_migrations'
181
+ `);
182
+ const existsResult = existsStmt.get();
183
+ if (!existsResult || existsResult.count === 0)
184
+ return -1;
185
+ // get current version
186
+ const versionStmt = db.prepare(`SELECT MAX("version") AS "version" FROM "openworkflow_migrations";`);
187
+ const versionResult = versionStmt.get();
188
+ return versionResult?.version ?? -1;
189
+ }
190
+ /**
191
+ * Helper to generate UUIDs (SQLite doesn't have built-in UUID generation)
192
+ * @returns A UUID string
193
+ */
194
+ export function generateUUID() {
195
+ return randomUUID();
196
+ }
197
+ /**
198
+ * Helper to get current timestamp in ISO8601 format
199
+ * @returns ISO8601 timestamp string
200
+ */
201
+ export function now() {
202
+ return new Date().toISOString();
203
+ }
204
+ /**
205
+ * Helper to add milliseconds to a date and return ISO8601 string
206
+ * @param date - ISO8601 date string
207
+ * @param ms - Milliseconds to add
208
+ * @returns Updated ISO8601 date string
209
+ */
210
+ export function addMilliseconds(date, ms) {
211
+ const d = new Date(date);
212
+ d.setMilliseconds(d.getMilliseconds() + ms);
213
+ return d.toISOString();
214
+ }
215
+ /**
216
+ * Helper to serialize JSON for SQLite storage
217
+ * @param value - Value to serialize
218
+ * @returns JSON string or null
219
+ */
220
+ export function toJSON(value) {
221
+ return value === null || value === undefined ? null : JSON.stringify(value);
222
+ }
223
+ /**
224
+ * Helper to deserialize JSON from SQLite storage
225
+ * @param value - JSON string or null
226
+ * @returns Parsed value
227
+ */
228
+ export function fromJSON(value) {
229
+ return value === null ? null : JSON.parse(value);
230
+ }
231
+ /**
232
+ * Helper to convert Date to ISO8601 string for SQLite
233
+ * @param date - Date or null
234
+ * @returns ISO8601 date string or null
235
+ */
236
+ export function toISO(date) {
237
+ return date ? date.toISOString() : null;
238
+ }
239
+ /**
240
+ * Helper to convert ISO8601 string from SQLite to Date
241
+ * @param dateStr - ISO8601 date string or null
242
+ * @returns Date or null
243
+ */
244
+ export function fromISO(dateStr) {
245
+ return dateStr ? new Date(dateStr) : null;
246
+ }
247
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../sqlite/sqlite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { Backend } from "openworkflow/internal";
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":["../../testing/backend.testsuite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAA4B,MAAM,uBAAuB,CAAC;AAG/E;;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,CA8sC7D"}
@@ -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([first.id, second.id]);
94
92
  await teardown(backend);
95
93
  });
96
94
  test("paginates workflow runs", async () => {
@@ -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 = a.createdAt.getTime() - b.createdAt.getTime();
155
+ if (timeDiff !== 0)
156
+ return timeDiff;
157
+ return a.id.localeCompare(b.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,42 +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
  }