@multiplier-labs/stepflow 0.3.1 → 0.3.4

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 (112) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  4. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  5. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  6. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  7. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  8. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  9. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  10. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  11. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  12. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  13. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  14. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  15. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  16. package/.github/CODEOWNERS +15 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/workflows/ci.yml +38 -0
  19. package/.github/workflows/publish.yml +17 -5
  20. package/.github/workflows/security.yml +29 -0
  21. package/CHANGELOG.md +34 -0
  22. package/README.md +24 -0
  23. package/SECURITY.md +40 -0
  24. package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
  25. package/dist/chunk-5XJBAPXF.js.map +1 -0
  26. package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
  27. package/dist/chunk-FMPJBK2N.js.map +1 -0
  28. package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
  29. package/dist/chunk-JPXPB7U5.js.map +1 -0
  30. package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
  31. package/dist/chunk-U3PCVK6O.js.map +1 -0
  32. package/dist/events/index.d.ts +60 -7
  33. package/dist/events/index.js +1 -1
  34. package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
  35. package/dist/index.d.ts +15 -23
  36. package/dist/index.js +784 -547
  37. package/dist/index.js.map +1 -1
  38. package/dist/scheduler/index.d.ts +4 -4
  39. package/dist/scheduler/index.js +2 -2
  40. package/dist/storage/index.d.ts +322 -56
  41. package/dist/storage/index.js +4 -2
  42. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  43. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  44. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  45. package/docs/stepflow-api-reference.md +5 -4
  46. package/package.json +12 -8
  47. package/src/core/engine/concurrency.ts +229 -0
  48. package/src/core/engine/lifecycle.ts +176 -0
  49. package/src/core/engine.test.ts +536 -522
  50. package/src/core/engine.ts +116 -235
  51. package/src/core/index.ts +3 -3
  52. package/src/core/orchestrator/persistence.ts +165 -0
  53. package/src/core/orchestrator/plan-resolution.ts +140 -0
  54. package/src/core/orchestrator/state-transitions.ts +109 -0
  55. package/src/core/orchestrator/step-runner.ts +427 -0
  56. package/src/core/orchestrator.ts +57 -395
  57. package/src/core/types.ts +7 -29
  58. package/src/events/index.ts +4 -4
  59. package/src/events/memory.test.ts +39 -41
  60. package/src/events/memory.ts +7 -10
  61. package/src/events/socketio.test.ts +112 -162
  62. package/src/events/socketio.ts +20 -27
  63. package/src/events/types.ts +16 -19
  64. package/src/events/webhook.test.ts +489 -235
  65. package/src/events/webhook.ts +355 -139
  66. package/src/index.ts +19 -19
  67. package/src/planning/evaluate-condition.test.ts +153 -0
  68. package/src/planning/evaluate-condition.ts +105 -0
  69. package/src/planning/index.ts +6 -3
  70. package/src/planning/planner.ts +45 -152
  71. package/src/planning/planning.test.ts +303 -366
  72. package/src/planning/registry.test.ts +204 -251
  73. package/src/planning/registry.ts +20 -106
  74. package/src/planning/types.ts +23 -27
  75. package/src/scheduler/cron.test.ts +149 -150
  76. package/src/scheduler/cron.ts +58 -69
  77. package/src/scheduler/index.ts +4 -14
  78. package/src/scheduler/postgres-persistence.ts +89 -105
  79. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  80. package/src/scheduler/sqlite-persistence.ts +40 -22
  81. package/src/scheduler/types.ts +4 -9
  82. package/src/storage/index.ts +5 -5
  83. package/src/storage/memory.test.ts +133 -183
  84. package/src/storage/memory.ts +21 -42
  85. package/src/storage/postgres-core.ts +550 -0
  86. package/src/storage/postgres-migration.test.ts +69 -0
  87. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  88. package/src/storage/postgres.ts +222 -937
  89. package/src/storage/sqlite-core.ts +151 -0
  90. package/src/storage/sqlite.test.ts +330 -281
  91. package/src/storage/sqlite.ts +225 -170
  92. package/src/storage/types.ts +33 -58
  93. package/src/utils/errors.test.ts +78 -80
  94. package/src/utils/errors.ts +27 -57
  95. package/src/utils/id.ts +3 -3
  96. package/src/utils/index.ts +6 -4
  97. package/src/utils/logger.test.ts +52 -58
  98. package/src/utils/logger.ts +4 -4
  99. package/src/utils/pinned-fetch.test.ts +84 -0
  100. package/src/utils/pinned-fetch.ts +118 -0
  101. package/src/utils/postgres-deps.ts +20 -4
  102. package/src/utils/retry.test.ts +47 -67
  103. package/src/utils/retry.ts +9 -9
  104. package/src/utils/safe-json.test.ts +166 -0
  105. package/src/utils/safe-json.ts +107 -0
  106. package/src/utils/webhook-signature.test.ts +111 -0
  107. package/src/utils/webhook-signature.ts +74 -0
  108. package/dist/chunk-FQCT3I2A.js.map +0 -1
  109. package/dist/chunk-GXY66V7Q.js.map +0 -1
  110. package/dist/chunk-U5QGN3RT.js.map +0 -1
  111. package/dist/chunk-XCF4PLN7.js.map +0 -1
  112. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,11 +1,11 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import { WorkflowEngine } from "./engine";
3
- import { MemoryStorageAdapter } from "../storage/memory";
4
- import { MemoryEventTransport } from "../events/memory";
5
- import { SilentLogger } from "../utils/logger";
6
- import type { WorkflowDefinition, WorkflowEvent } from "../index";
7
-
8
- describe("WorkflowEngine", () => {
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { WorkflowEngine } from './engine';
3
+ import { MemoryStorageAdapter } from '../storage/memory';
4
+ import { MemoryEventTransport } from '../events/memory';
5
+ import { SilentLogger } from '../utils/logger';
6
+ import type { WorkflowDefinition, WorkflowEvent } from '../index';
7
+
8
+ describe('WorkflowEngine', () => {
9
9
  let engine: WorkflowEngine;
10
10
  let storage: MemoryStorageAdapter;
11
11
  let events: MemoryEventTransport;
@@ -20,149 +20,147 @@ describe("WorkflowEngine", () => {
20
20
  });
21
21
  });
22
22
 
23
- describe("registerWorkflow", () => {
24
- it("should register a workflow definition", () => {
23
+ describe('registerWorkflow', () => {
24
+ it('should register a workflow definition', () => {
25
25
  const workflow: WorkflowDefinition = {
26
- kind: "test.workflow",
27
- name: "Test Workflow",
26
+ kind: 'test.workflow',
27
+ name: 'Test Workflow',
28
28
  steps: [],
29
29
  };
30
30
 
31
31
  engine.registerWorkflow(workflow);
32
32
 
33
- expect(engine.getWorkflow("test.workflow")).toBeDefined();
34
- expect(engine.getRegisteredWorkflows()).toContain("test.workflow");
33
+ expect(engine.getWorkflow('test.workflow')).toBeDefined();
34
+ expect(engine.getRegisteredWorkflows()).toContain('test.workflow');
35
35
  });
36
36
 
37
- it("should throw if workflow is already registered", () => {
37
+ it('should throw if workflow is already registered', () => {
38
38
  const workflow: WorkflowDefinition = {
39
- kind: "test.workflow",
40
- name: "Test Workflow",
39
+ kind: 'test.workflow',
40
+ name: 'Test Workflow',
41
41
  steps: [],
42
42
  };
43
43
 
44
44
  engine.registerWorkflow(workflow);
45
45
 
46
46
  expect(() => engine.registerWorkflow(workflow)).toThrow(
47
- 'Workflow "test.workflow" is already registered',
47
+ 'Workflow "test.workflow" is already registered'
48
48
  );
49
49
  });
50
50
  });
51
51
 
52
- describe("unregisterWorkflow", () => {
53
- it("should unregister a workflow", () => {
52
+ describe('unregisterWorkflow', () => {
53
+ it('should unregister a workflow', () => {
54
54
  const workflow: WorkflowDefinition = {
55
- kind: "test.workflow",
56
- name: "Test Workflow",
55
+ kind: 'test.workflow',
56
+ name: 'Test Workflow',
57
57
  steps: [],
58
58
  };
59
59
 
60
60
  engine.registerWorkflow(workflow);
61
- expect(engine.unregisterWorkflow("test.workflow")).toBe(true);
62
- expect(engine.getWorkflow("test.workflow")).toBeUndefined();
61
+ expect(engine.unregisterWorkflow('test.workflow')).toBe(true);
62
+ expect(engine.getWorkflow('test.workflow')).toBeUndefined();
63
63
  });
64
64
 
65
- it("should return false if workflow not found", () => {
66
- expect(engine.unregisterWorkflow("nonexistent")).toBe(false);
65
+ it('should return false if workflow not found', () => {
66
+ expect(engine.unregisterWorkflow('nonexistent')).toBe(false);
67
67
  });
68
68
  });
69
69
 
70
- describe("startRun", () => {
71
- it("should throw if workflow is not registered", async () => {
72
- await expect(engine.startRun({ kind: "nonexistent" })).rejects.toThrow(
73
- 'Workflow "nonexistent" is not registered',
70
+ describe('startRun', () => {
71
+ it('should throw if workflow is not registered', async () => {
72
+ await expect(engine.startRun({ kind: 'nonexistent' })).rejects.toThrow(
73
+ 'Workflow "nonexistent" is not registered'
74
74
  );
75
75
  });
76
76
 
77
- it("should start a workflow run and return run ID", async () => {
77
+ it('should start a workflow run and return run ID', async () => {
78
78
  engine.registerWorkflow({
79
- kind: "test.workflow",
80
- name: "Test Workflow",
79
+ kind: 'test.workflow',
80
+ name: 'Test Workflow',
81
81
  steps: [
82
82
  {
83
- key: "step1",
84
- name: "Step 1",
85
- handler: async () => "result1",
83
+ key: 'step1',
84
+ name: 'Step 1',
85
+ handler: async () => 'result1',
86
86
  },
87
87
  ],
88
88
  });
89
89
 
90
- const runId = await engine.startRun({ kind: "test.workflow" });
90
+ const runId = await engine.startRun({ kind: 'test.workflow' });
91
91
 
92
92
  expect(runId).toBeDefined();
93
- expect(typeof runId).toBe("string");
93
+ expect(typeof runId).toBe('string');
94
94
  });
95
95
 
96
- it("should execute steps in order", async () => {
96
+ it('should execute steps in order', async () => {
97
97
  const executionOrder: string[] = [];
98
98
 
99
99
  engine.registerWorkflow({
100
- kind: "test.order",
101
- name: "Test Order",
100
+ kind: 'test.order',
101
+ name: 'Test Order',
102
102
  steps: [
103
103
  {
104
- key: "first",
105
- name: "First",
104
+ key: 'first',
105
+ name: 'First',
106
106
  handler: async () => {
107
- executionOrder.push("first");
108
- return "first";
107
+ executionOrder.push('first');
108
+ return 'first';
109
109
  },
110
110
  },
111
111
  {
112
- key: "second",
113
- name: "Second",
112
+ key: 'second',
113
+ name: 'Second',
114
114
  handler: async () => {
115
- executionOrder.push("second");
116
- return "second";
115
+ executionOrder.push('second');
116
+ return 'second';
117
117
  },
118
118
  },
119
119
  {
120
- key: "third",
121
- name: "Third",
120
+ key: 'third',
121
+ name: 'Third',
122
122
  handler: async () => {
123
- executionOrder.push("third");
124
- return "third";
123
+ executionOrder.push('third');
124
+ return 'third';
125
125
  },
126
126
  },
127
127
  ],
128
128
  });
129
129
 
130
- const runId = await engine.startRun({ kind: "test.order" });
130
+ const runId = await engine.startRun({ kind: 'test.order' });
131
131
  const run = await engine.waitForRun(runId);
132
132
 
133
- expect(run.status).toBe("succeeded");
134
- expect(executionOrder).toEqual(["first", "second", "third"]);
133
+ expect(run.status).toBe('succeeded');
134
+ expect(executionOrder).toEqual(['first', 'second', 'third']);
135
135
  });
136
136
 
137
- it("should pass accumulated results to steps via context", async () => {
137
+ it('should pass accumulated results to steps via context', async () => {
138
138
  let step2Input: unknown;
139
139
 
140
140
  engine.registerWorkflow({
141
- kind: "test.results",
142
- name: "Test Results",
141
+ kind: 'test.results',
142
+ name: 'Test Results',
143
143
  steps: [
144
144
  {
145
- key: "step1",
146
- name: "Step 1",
145
+ key: 'step1',
146
+ name: 'Step 1',
147
147
  handler: async () => ({ value: 42 }),
148
148
  },
149
149
  {
150
- key: "step2",
151
- name: "Step 2",
150
+ key: 'step2',
151
+ name: 'Step 2',
152
152
  handler: async (ctx) => {
153
153
  step2Input = ctx.results.step1;
154
- return {
155
- doubled: (ctx.results.step1 as { value: number }).value * 2,
156
- };
154
+ return { doubled: (ctx.results.step1 as { value: number }).value * 2 };
157
155
  },
158
156
  },
159
157
  ],
160
158
  });
161
159
 
162
- const runId = await engine.startRun({ kind: "test.results" });
160
+ const runId = await engine.startRun({ kind: 'test.results' });
163
161
  const run = await engine.waitForRun(runId);
164
162
 
165
- expect(run.status).toBe("succeeded");
163
+ expect(run.status).toBe('succeeded');
166
164
  expect(step2Input).toEqual({ value: 42 });
167
165
  expect(run.context).toEqual({
168
166
  step1: { value: 42 },
@@ -170,238 +168,234 @@ describe("WorkflowEngine", () => {
170
168
  });
171
169
  });
172
170
 
173
- it("should pass input to context", async () => {
171
+ it('should pass input to context', async () => {
174
172
  let receivedInput: unknown;
175
173
 
176
174
  engine.registerWorkflow({
177
- kind: "test.input",
178
- name: "Test Input",
175
+ kind: 'test.input',
176
+ name: 'Test Input',
179
177
  steps: [
180
178
  {
181
- key: "step1",
182
- name: "Step 1",
179
+ key: 'step1',
180
+ name: 'Step 1',
183
181
  handler: async (ctx) => {
184
182
  receivedInput = ctx.input;
185
- return "done";
183
+ return 'done';
186
184
  },
187
185
  },
188
186
  ],
189
187
  });
190
188
 
191
189
  const runId = await engine.startRun({
192
- kind: "test.input",
193
- input: { foo: "bar", num: 123 },
190
+ kind: 'test.input',
191
+ input: { foo: 'bar', num: 123 },
194
192
  });
195
193
  await engine.waitForRun(runId);
196
194
 
197
- expect(receivedInput).toEqual({ foo: "bar", num: 123 });
195
+ expect(receivedInput).toEqual({ foo: 'bar', num: 123 });
198
196
  });
199
197
 
200
- it("should pass metadata to context", async () => {
198
+ it('should pass metadata to context', async () => {
201
199
  let receivedMetadata: unknown;
202
200
 
203
201
  engine.registerWorkflow({
204
- kind: "test.metadata",
205
- name: "Test Metadata",
202
+ kind: 'test.metadata',
203
+ name: 'Test Metadata',
206
204
  steps: [
207
205
  {
208
- key: "step1",
209
- name: "Step 1",
206
+ key: 'step1',
207
+ name: 'Step 1',
210
208
  handler: async (ctx) => {
211
209
  receivedMetadata = ctx.metadata;
212
- return "done";
210
+ return 'done';
213
211
  },
214
212
  },
215
213
  ],
216
214
  });
217
215
 
218
216
  const runId = await engine.startRun({
219
- kind: "test.metadata",
220
- metadata: { userId: "user123", topicId: "topic456" },
217
+ kind: 'test.metadata',
218
+ metadata: { userId: 'user123', topicId: 'topic456' },
221
219
  });
222
220
  await engine.waitForRun(runId);
223
221
 
224
- expect(receivedMetadata).toEqual({
225
- userId: "user123",
226
- topicId: "topic456",
227
- });
222
+ expect(receivedMetadata).toEqual({ userId: 'user123', topicId: 'topic456' });
228
223
  });
229
224
  });
230
225
 
231
- describe("error handling", () => {
232
- it("should fail the run when a step throws (default strategy)", async () => {
226
+ describe('error handling', () => {
227
+ it('should fail the run when a step throws (default strategy)', async () => {
233
228
  engine.registerWorkflow({
234
- kind: "test.fail",
235
- name: "Test Fail",
229
+ kind: 'test.fail',
230
+ name: 'Test Fail',
236
231
  steps: [
237
232
  {
238
- key: "step1",
239
- name: "Step 1",
233
+ key: 'step1',
234
+ name: 'Step 1',
240
235
  handler: async () => {
241
- throw new Error("Step failed!");
236
+ throw new Error('Step failed!');
242
237
  },
243
238
  },
244
239
  ],
245
240
  });
246
241
 
247
- const runId = await engine.startRun({ kind: "test.fail" });
242
+ const runId = await engine.startRun({ kind: 'test.fail' });
248
243
  const run = await engine.waitForRun(runId);
249
244
 
250
- expect(run.status).toBe("failed");
251
- expect(run.error?.message).toContain("Step failed!");
245
+ expect(run.status).toBe('failed');
246
+ expect(run.error?.message).toContain('Step failed!');
252
247
  });
253
248
 
254
249
  it('should skip step when onError is "skip"', async () => {
255
250
  const executionOrder: string[] = [];
256
251
 
257
252
  engine.registerWorkflow({
258
- kind: "test.skip",
259
- name: "Test Skip",
253
+ kind: 'test.skip',
254
+ name: 'Test Skip',
260
255
  steps: [
261
256
  {
262
- key: "step1",
263
- name: "Step 1",
257
+ key: 'step1',
258
+ name: 'Step 1',
264
259
  handler: async () => {
265
- executionOrder.push("step1");
266
- throw new Error("This will be skipped");
260
+ executionOrder.push('step1');
261
+ throw new Error('This will be skipped');
267
262
  },
268
- onError: "skip",
263
+ onError: 'skip',
269
264
  },
270
265
  {
271
- key: "step2",
272
- name: "Step 2",
266
+ key: 'step2',
267
+ name: 'Step 2',
273
268
  handler: async () => {
274
- executionOrder.push("step2");
275
- return "step2";
269
+ executionOrder.push('step2');
270
+ return 'step2';
276
271
  },
277
272
  },
278
273
  ],
279
274
  });
280
275
 
281
- const runId = await engine.startRun({ kind: "test.skip" });
276
+ const runId = await engine.startRun({ kind: 'test.skip' });
282
277
  const run = await engine.waitForRun(runId);
283
278
 
284
- expect(run.status).toBe("succeeded");
285
- expect(executionOrder).toEqual(["step1", "step2"]);
279
+ expect(run.status).toBe('succeeded');
280
+ expect(executionOrder).toEqual(['step1', 'step2']);
286
281
  });
287
282
 
288
283
  it('should retry step when onError is "retry"', async () => {
289
284
  let attempts = 0;
290
285
 
291
286
  engine.registerWorkflow({
292
- kind: "test.retry",
293
- name: "Test Retry",
287
+ kind: 'test.retry',
288
+ name: 'Test Retry',
294
289
  steps: [
295
290
  {
296
- key: "step1",
297
- name: "Step 1",
291
+ key: 'step1',
292
+ name: 'Step 1',
298
293
  handler: async () => {
299
294
  attempts++;
300
295
  if (attempts < 3) {
301
296
  throw new Error(`Attempt ${attempts} failed`);
302
297
  }
303
- return "success";
298
+ return 'success';
304
299
  },
305
- onError: "retry",
300
+ onError: 'retry',
306
301
  maxRetries: 5,
307
302
  retryDelay: 10,
308
303
  },
309
304
  ],
310
305
  });
311
306
 
312
- const runId = await engine.startRun({ kind: "test.retry" });
307
+ const runId = await engine.startRun({ kind: 'test.retry' });
313
308
  const run = await engine.waitForRun(runId);
314
309
 
315
- expect(run.status).toBe("succeeded");
310
+ expect(run.status).toBe('succeeded');
316
311
  expect(attempts).toBe(3);
317
312
  });
318
313
 
319
- it("should fail after exhausting retries", async () => {
314
+ it('should fail after exhausting retries', async () => {
320
315
  let attempts = 0;
321
316
 
322
317
  engine.registerWorkflow({
323
- kind: "test.retry.exhaust",
324
- name: "Test Retry Exhaust",
318
+ kind: 'test.retry.exhaust',
319
+ name: 'Test Retry Exhaust',
325
320
  steps: [
326
321
  {
327
- key: "step1",
328
- name: "Step 1",
322
+ key: 'step1',
323
+ name: 'Step 1',
329
324
  handler: async () => {
330
325
  attempts++;
331
326
  throw new Error(`Attempt ${attempts} failed`);
332
327
  },
333
- onError: "retry",
328
+ onError: 'retry',
334
329
  maxRetries: 2,
335
330
  retryDelay: 10,
336
331
  },
337
332
  ],
338
333
  });
339
334
 
340
- const runId = await engine.startRun({ kind: "test.retry.exhaust" });
335
+ const runId = await engine.startRun({ kind: 'test.retry.exhaust' });
341
336
  const run = await engine.waitForRun(runId);
342
337
 
343
- expect(run.status).toBe("failed");
338
+ expect(run.status).toBe('failed');
344
339
  expect(attempts).toBe(3); // 1 initial + 2 retries
345
340
  });
346
341
  });
347
342
 
348
- describe("skipIf condition", () => {
349
- it("should skip step when skipIf returns true", async () => {
343
+ describe('skipIf condition', () => {
344
+ it('should skip step when skipIf returns true', async () => {
350
345
  const executionOrder: string[] = [];
351
346
 
352
347
  engine.registerWorkflow({
353
- kind: "test.skipif",
354
- name: "Test SkipIf",
348
+ kind: 'test.skipif',
349
+ name: 'Test SkipIf',
355
350
  steps: [
356
351
  {
357
- key: "step1",
358
- name: "Step 1",
352
+ key: 'step1',
353
+ name: 'Step 1',
359
354
  handler: async () => {
360
- executionOrder.push("step1");
355
+ executionOrder.push('step1');
361
356
  return { skip: true };
362
357
  },
363
358
  },
364
359
  {
365
- key: "step2",
366
- name: "Step 2",
360
+ key: 'step2',
361
+ name: 'Step 2',
367
362
  handler: async () => {
368
- executionOrder.push("step2");
369
- return "step2";
363
+ executionOrder.push('step2');
364
+ return 'step2';
370
365
  },
371
- skipIf: async (ctx) =>
372
- (ctx.results.step1 as { skip: boolean }).skip,
366
+ skipIf: async (ctx) => (ctx.results.step1 as { skip: boolean }).skip,
373
367
  },
374
368
  {
375
- key: "step3",
376
- name: "Step 3",
369
+ key: 'step3',
370
+ name: 'Step 3',
377
371
  handler: async () => {
378
- executionOrder.push("step3");
379
- return "step3";
372
+ executionOrder.push('step3');
373
+ return 'step3';
380
374
  },
381
375
  },
382
376
  ],
383
377
  });
384
378
 
385
- const runId = await engine.startRun({ kind: "test.skipif" });
379
+ const runId = await engine.startRun({ kind: 'test.skipif' });
386
380
  const run = await engine.waitForRun(runId);
387
381
 
388
- expect(run.status).toBe("succeeded");
389
- expect(executionOrder).toEqual(["step1", "step3"]);
382
+ expect(run.status).toBe('succeeded');
383
+ expect(executionOrder).toEqual(['step1', 'step3']);
390
384
  });
391
385
  });
392
386
 
393
- describe("events", () => {
394
- it("should emit events during workflow execution", async () => {
387
+ describe('events', () => {
388
+ it('should emit events during workflow execution', async () => {
395
389
  const receivedEvents: WorkflowEvent[] = [];
396
390
 
397
391
  engine.registerWorkflow({
398
- kind: "test.events",
399
- name: "Test Events",
392
+ kind: 'test.events',
393
+ name: 'Test Events',
400
394
  steps: [
401
395
  {
402
- key: "step1",
403
- name: "Step 1",
404
- handler: async () => "result",
396
+ key: 'step1',
397
+ name: 'Step 1',
398
+ handler: async () => 'result',
405
399
  },
406
400
  ],
407
401
  });
@@ -410,32 +404,32 @@ describe("WorkflowEngine", () => {
410
404
  receivedEvents.push(event);
411
405
  });
412
406
 
413
- const runId = await engine.startRun({ kind: "test.events" });
407
+ const runId = await engine.startRun({ kind: 'test.events' });
414
408
  await engine.waitForRun(runId);
415
409
  unsubscribe();
416
410
 
417
- const eventTypes = receivedEvents.map((e) => e.eventType);
418
- expect(eventTypes).toContain("run.created");
419
- expect(eventTypes).toContain("run.started");
420
- expect(eventTypes).toContain("step.started");
421
- expect(eventTypes).toContain("step.completed");
422
- expect(eventTypes).toContain("run.completed");
411
+ const eventTypes = receivedEvents.map(e => e.eventType);
412
+ expect(eventTypes).toContain('run.created');
413
+ expect(eventTypes).toContain('run.started');
414
+ expect(eventTypes).toContain('step.started');
415
+ expect(eventTypes).toContain('step.completed');
416
+ expect(eventTypes).toContain('run.completed');
423
417
  });
424
418
 
425
- it("should emit custom events via context.emit", async () => {
419
+ it('should emit custom events via context.emit', async () => {
426
420
  const receivedEvents: WorkflowEvent[] = [];
427
421
 
428
422
  engine.registerWorkflow({
429
- kind: "test.custom.events",
430
- name: "Test Custom Events",
423
+ kind: 'test.custom.events',
424
+ name: 'Test Custom Events',
431
425
  steps: [
432
426
  {
433
- key: "step1",
434
- name: "Step 1",
427
+ key: 'step1',
428
+ name: 'Step 1',
435
429
  handler: async (ctx) => {
436
- ctx.emit("custom.progress", { percent: 50 });
437
- ctx.emit("custom.progress", { percent: 100 });
438
- return "done";
430
+ ctx.emit('custom.progress', { percent: 50 });
431
+ ctx.emit('custom.progress', { percent: 100 });
432
+ return 'done';
439
433
  },
440
434
  },
441
435
  ],
@@ -445,68 +439,66 @@ describe("WorkflowEngine", () => {
445
439
  receivedEvents.push(event);
446
440
  });
447
441
 
448
- const runId = await engine.startRun({ kind: "test.custom.events" });
442
+ const runId = await engine.startRun({ kind: 'test.custom.events' });
449
443
  await engine.waitForRun(runId);
450
444
  unsubscribe();
451
445
 
452
- const customEvents = receivedEvents.filter(
453
- (e) => e.eventType === "custom.progress",
454
- );
446
+ const customEvents = receivedEvents.filter(e => e.eventType === 'custom.progress');
455
447
  expect(customEvents).toHaveLength(2);
456
448
  expect(customEvents[0].payload).toEqual({ percent: 50 });
457
449
  expect(customEvents[1].payload).toEqual({ percent: 100 });
458
450
  });
459
451
  });
460
452
 
461
- describe("cancelRun", () => {
462
- it("should cancel a running workflow", async () => {
453
+ describe('cancelRun', () => {
454
+ it('should cancel a running workflow', async () => {
463
455
  engine.registerWorkflow({
464
- kind: "test.cancel",
465
- name: "Test Cancel",
456
+ kind: 'test.cancel',
457
+ name: 'Test Cancel',
466
458
  steps: [
467
459
  {
468
- key: "step1",
469
- name: "Step 1",
460
+ key: 'step1',
461
+ name: 'Step 1',
470
462
  handler: async () => {
471
463
  // Simulate long-running step
472
- await new Promise((resolve) => setTimeout(resolve, 1000));
473
- return "done";
464
+ await new Promise(resolve => setTimeout(resolve, 1000));
465
+ return 'done';
474
466
  },
475
467
  },
476
468
  ],
477
469
  });
478
470
 
479
- const runId = await engine.startRun({ kind: "test.cancel" });
471
+ const runId = await engine.startRun({ kind: 'test.cancel' });
480
472
 
481
473
  // Cancel after a short delay
482
- await new Promise((resolve) => setTimeout(resolve, 50));
474
+ await new Promise(resolve => setTimeout(resolve, 50));
483
475
  await engine.cancelRun(runId);
484
476
 
485
477
  const run = await engine.waitForRun(runId, { timeout: 2000 });
486
- expect(run.status).toBe("canceled");
478
+ expect(run.status).toBe('canceled');
487
479
  });
488
480
  });
489
481
 
490
- describe("hooks", () => {
491
- it("should call beforeRun and afterRun hooks", async () => {
482
+ describe('hooks', () => {
483
+ it('should call beforeRun and afterRun hooks', async () => {
492
484
  const hookCalls: string[] = [];
493
485
 
494
486
  engine.registerWorkflow({
495
- kind: "test.hooks",
496
- name: "Test Hooks",
487
+ kind: 'test.hooks',
488
+ name: 'Test Hooks',
497
489
  steps: [
498
490
  {
499
- key: "step1",
500
- name: "Step 1",
491
+ key: 'step1',
492
+ name: 'Step 1',
501
493
  handler: async () => {
502
- hookCalls.push("step1");
503
- return "done";
494
+ hookCalls.push('step1');
495
+ return 'done';
504
496
  },
505
497
  },
506
498
  ],
507
499
  hooks: {
508
500
  beforeRun: async () => {
509
- hookCalls.push("beforeRun");
501
+ hookCalls.push('beforeRun');
510
502
  },
511
503
  afterRun: async (_, result) => {
512
504
  hookCalls.push(`afterRun:${result.status}`);
@@ -514,28 +506,28 @@ describe("WorkflowEngine", () => {
514
506
  },
515
507
  });
516
508
 
517
- const runId = await engine.startRun({ kind: "test.hooks" });
509
+ const runId = await engine.startRun({ kind: 'test.hooks' });
518
510
  await engine.waitForRun(runId);
519
511
 
520
- expect(hookCalls).toEqual(["beforeRun", "step1", "afterRun:succeeded"]);
512
+ expect(hookCalls).toEqual(['beforeRun', 'step1', 'afterRun:succeeded']);
521
513
  });
522
514
 
523
- it("should call beforeStep and afterStep hooks", async () => {
515
+ it('should call beforeStep and afterStep hooks', async () => {
524
516
  const hookCalls: string[] = [];
525
517
 
526
518
  engine.registerWorkflow({
527
- kind: "test.step.hooks",
528
- name: "Test Step Hooks",
519
+ kind: 'test.step.hooks',
520
+ name: 'Test Step Hooks',
529
521
  steps: [
530
522
  {
531
- key: "step1",
532
- name: "Step 1",
533
- handler: async () => "result1",
523
+ key: 'step1',
524
+ name: 'Step 1',
525
+ handler: async () => 'result1',
534
526
  },
535
527
  {
536
- key: "step2",
537
- name: "Step 2",
538
- handler: async () => "result2",
528
+ key: 'step2',
529
+ name: 'Step 2',
530
+ handler: async () => 'result2',
539
531
  },
540
532
  ],
541
533
  hooks: {
@@ -548,159 +540,157 @@ describe("WorkflowEngine", () => {
548
540
  },
549
541
  });
550
542
 
551
- const runId = await engine.startRun({ kind: "test.step.hooks" });
543
+ const runId = await engine.startRun({ kind: 'test.step.hooks' });
552
544
  await engine.waitForRun(runId);
553
545
 
554
546
  expect(hookCalls).toEqual([
555
- "before:step1",
556
- "after:step1",
557
- "before:step2",
558
- "after:step2",
547
+ 'before:step1',
548
+ 'after:step1',
549
+ 'before:step2',
550
+ 'after:step2',
559
551
  ]);
560
552
  });
561
553
  });
562
554
 
563
- describe("storage", () => {
564
- it("should persist run records", async () => {
555
+ describe('storage', () => {
556
+ it('should persist run records', async () => {
565
557
  engine.registerWorkflow({
566
- kind: "test.storage",
567
- name: "Test Storage",
558
+ kind: 'test.storage',
559
+ name: 'Test Storage',
568
560
  steps: [
569
561
  {
570
- key: "step1",
571
- name: "Step 1",
572
- handler: async () => "result",
562
+ key: 'step1',
563
+ name: 'Step 1',
564
+ handler: async () => 'result',
573
565
  },
574
566
  ],
575
567
  });
576
568
 
577
569
  const runId = await engine.startRun({
578
- kind: "test.storage",
579
- input: { foo: "bar" },
580
- metadata: { userId: "user1" },
570
+ kind: 'test.storage',
571
+ input: { foo: 'bar' },
572
+ metadata: { userId: 'user1' },
581
573
  });
582
574
  await engine.waitForRun(runId);
583
575
 
584
576
  const run = await storage.getRun(runId);
585
577
  expect(run).toBeDefined();
586
- expect(run?.kind).toBe("test.storage");
587
- expect(run?.status).toBe("succeeded");
588
- expect(run?.input).toEqual({ foo: "bar" });
589
- expect(run?.metadata).toEqual({ userId: "user1" });
578
+ expect(run?.kind).toBe('test.storage');
579
+ expect(run?.status).toBe('succeeded');
580
+ expect(run?.input).toEqual({ foo: 'bar' });
581
+ expect(run?.metadata).toEqual({ userId: 'user1' });
590
582
  });
591
583
 
592
- it("should persist step records", async () => {
584
+ it('should persist step records', async () => {
593
585
  engine.registerWorkflow({
594
- kind: "test.step.storage",
595
- name: "Test Step Storage",
586
+ kind: 'test.step.storage',
587
+ name: 'Test Step Storage',
596
588
  steps: [
597
589
  {
598
- key: "step1",
599
- name: "Step 1",
600
- handler: async () => "result1",
590
+ key: 'step1',
591
+ name: 'Step 1',
592
+ handler: async () => 'result1',
601
593
  },
602
594
  {
603
- key: "step2",
604
- name: "Step 2",
605
- handler: async () => "result2",
595
+ key: 'step2',
596
+ name: 'Step 2',
597
+ handler: async () => 'result2',
606
598
  },
607
599
  ],
608
600
  });
609
601
 
610
- const runId = await engine.startRun({ kind: "test.step.storage" });
602
+ const runId = await engine.startRun({ kind: 'test.step.storage' });
611
603
  await engine.waitForRun(runId);
612
604
 
613
605
  const steps = await storage.getStepsForRun(runId);
614
606
  expect(steps).toHaveLength(2);
615
- expect(steps[0].stepKey).toBe("step1");
616
- expect(steps[0].status).toBe("succeeded");
617
- expect(steps[1].stepKey).toBe("step2");
618
- expect(steps[1].status).toBe("succeeded");
607
+ expect(steps[0].stepKey).toBe('step1');
608
+ expect(steps[0].status).toBe('succeeded');
609
+ expect(steps[1].stepKey).toBe('step2');
610
+ expect(steps[1].status).toBe('succeeded');
619
611
  });
620
612
  });
621
613
 
622
- describe("workflow timeout", () => {
623
- it("should timeout a long-running workflow", async () => {
614
+ describe('workflow timeout', () => {
615
+ it('should timeout a long-running workflow', async () => {
624
616
  engine.registerWorkflow({
625
- kind: "test.timeout",
626
- name: "Test Timeout",
617
+ kind: 'test.timeout',
618
+ name: 'Test Timeout',
627
619
  timeout: 100, // 100ms timeout
628
620
  steps: [
629
621
  {
630
- key: "slow_step",
631
- name: "Slow Step",
622
+ key: 'slow_step',
623
+ name: 'Slow Step',
632
624
  handler: async () => {
633
- await new Promise((resolve) => setTimeout(resolve, 500));
634
- return "done";
625
+ await new Promise(resolve => setTimeout(resolve, 500));
626
+ return 'done';
635
627
  },
636
628
  },
637
629
  ],
638
630
  });
639
631
 
640
- const runId = await engine.startRun({ kind: "test.timeout" });
632
+ const runId = await engine.startRun({ kind: 'test.timeout' });
641
633
  const run = await engine.waitForRun(runId, { timeout: 2000 });
642
634
 
643
- expect(run.status).toBe("failed");
644
- expect(run.error?.code).toBe("WORKFLOW_TIMEOUT");
635
+ expect(run.status).toBe('failed');
636
+ expect(run.error?.code).toBe('WORKFLOW_TIMEOUT');
645
637
  });
646
638
 
647
- it("should complete before timeout if fast enough", async () => {
639
+ it('should complete before timeout if fast enough', async () => {
648
640
  engine.registerWorkflow({
649
- kind: "test.fast",
650
- name: "Test Fast",
641
+ kind: 'test.fast',
642
+ name: 'Test Fast',
651
643
  timeout: 1000, // 1s timeout
652
644
  steps: [
653
645
  {
654
- key: "fast_step",
655
- name: "Fast Step",
646
+ key: 'fast_step',
647
+ name: 'Fast Step',
656
648
  handler: async () => {
657
- await new Promise((resolve) => setTimeout(resolve, 10));
658
- return "done";
649
+ await new Promise(resolve => setTimeout(resolve, 10));
650
+ return 'done';
659
651
  },
660
652
  },
661
653
  ],
662
654
  });
663
655
 
664
- const runId = await engine.startRun({ kind: "test.fast" });
656
+ const runId = await engine.startRun({ kind: 'test.fast' });
665
657
  const run = await engine.waitForRun(runId, { timeout: 2000 });
666
658
 
667
- expect(run.status).toBe("succeeded");
659
+ expect(run.status).toBe('succeeded');
668
660
  });
669
661
 
670
- it("should emit run.timeout event", async () => {
662
+ it('should emit run.timeout event', async () => {
671
663
  const receivedEvents: WorkflowEvent[] = [];
672
664
 
673
665
  engine.registerWorkflow({
674
- kind: "test.timeout.event",
675
- name: "Test Timeout Event",
666
+ kind: 'test.timeout.event',
667
+ name: 'Test Timeout Event',
676
668
  timeout: 50,
677
669
  steps: [
678
670
  {
679
- key: "slow_step",
680
- name: "Slow Step",
671
+ key: 'slow_step',
672
+ name: 'Slow Step',
681
673
  handler: async () => {
682
- await new Promise((resolve) => setTimeout(resolve, 500));
683
- return "done";
674
+ await new Promise(resolve => setTimeout(resolve, 500));
675
+ return 'done';
684
676
  },
685
677
  },
686
678
  ],
687
679
  });
688
680
 
689
- const runId = await engine.startRun({ kind: "test.timeout.event" });
681
+ const runId = await engine.startRun({ kind: 'test.timeout.event' });
690
682
  engine.subscribeToRun(runId, (event) => {
691
683
  receivedEvents.push(event);
692
684
  });
693
685
 
694
686
  await engine.waitForRun(runId, { timeout: 2000 });
695
687
 
696
- expect(receivedEvents.some((e) => e.eventType === "run.timeout")).toBe(
697
- true,
698
- );
688
+ expect(receivedEvents.some(e => e.eventType === 'run.timeout')).toBe(true);
699
689
  });
700
690
  });
701
691
 
702
- describe("concurrency control", () => {
703
- it("should limit concurrent runs with maxConcurrency", async () => {
692
+ describe('concurrency control', () => {
693
+ it('should limit concurrent runs with maxConcurrency', async () => {
704
694
  const concurrentEngine = new WorkflowEngine({
705
695
  storage: new MemoryStorageAdapter(),
706
696
  events: new MemoryEventTransport(),
@@ -711,15 +701,15 @@ describe("WorkflowEngine", () => {
711
701
  });
712
702
 
713
703
  concurrentEngine.registerWorkflow({
714
- kind: "test.concurrent",
715
- name: "Test Concurrent",
704
+ kind: 'test.concurrent',
705
+ name: 'Test Concurrent',
716
706
  steps: [
717
707
  {
718
- key: "step1",
719
- name: "Step 1",
708
+ key: 'step1',
709
+ name: 'Step 1',
720
710
  handler: async () => {
721
- await new Promise((resolve) => setTimeout(resolve, 100));
722
- return "done";
711
+ await new Promise(resolve => setTimeout(resolve, 100));
712
+ return 'done';
723
713
  },
724
714
  },
725
715
  ],
@@ -727,10 +717,10 @@ describe("WorkflowEngine", () => {
727
717
 
728
718
  // Start 4 runs
729
719
  const runIds = await Promise.all([
730
- concurrentEngine.startRun({ kind: "test.concurrent" }),
731
- concurrentEngine.startRun({ kind: "test.concurrent" }),
732
- concurrentEngine.startRun({ kind: "test.concurrent" }),
733
- concurrentEngine.startRun({ kind: "test.concurrent" }),
720
+ concurrentEngine.startRun({ kind: 'test.concurrent' }),
721
+ concurrentEngine.startRun({ kind: 'test.concurrent' }),
722
+ concurrentEngine.startRun({ kind: 'test.concurrent' }),
723
+ concurrentEngine.startRun({ kind: 'test.concurrent' }),
734
724
  ]);
735
725
 
736
726
  // Check that only 2 are active and 2 are queued
@@ -738,9 +728,7 @@ describe("WorkflowEngine", () => {
738
728
  expect(concurrentEngine.getQueuedRunCount()).toBe(2);
739
729
 
740
730
  // Wait for all to complete
741
- await Promise.all(
742
- runIds.map((id) => concurrentEngine.waitForRun(id, { timeout: 2000 })),
743
- );
731
+ await Promise.all(runIds.map(id => concurrentEngine.waitForRun(id, { timeout: 2000 })));
744
732
 
745
733
  expect(concurrentEngine.getActiveRunCount()).toBe(0);
746
734
  expect(concurrentEngine.getQueuedRunCount()).toBe(0);
@@ -748,7 +736,7 @@ describe("WorkflowEngine", () => {
748
736
  await concurrentEngine.shutdown();
749
737
  });
750
738
 
751
- it("should process queue in order when runs complete", async () => {
739
+ it('should process queue in order when runs complete', async () => {
752
740
  const completionOrder: string[] = [];
753
741
 
754
742
  const concurrentEngine = new WorkflowEngine({
@@ -761,15 +749,15 @@ describe("WorkflowEngine", () => {
761
749
  });
762
750
 
763
751
  concurrentEngine.registerWorkflow({
764
- kind: "test.queue.order",
765
- name: "Test Queue Order",
752
+ kind: 'test.queue.order',
753
+ name: 'Test Queue Order',
766
754
  steps: [
767
755
  {
768
- key: "step1",
769
- name: "Step 1",
756
+ key: 'step1',
757
+ name: 'Step 1',
770
758
  handler: async (ctx) => {
771
759
  completionOrder.push(ctx.input.id as string);
772
- return "done";
760
+ return 'done';
773
761
  },
774
762
  },
775
763
  ],
@@ -777,34 +765,23 @@ describe("WorkflowEngine", () => {
777
765
 
778
766
  // Start 3 runs in order
779
767
  const runIds = await Promise.all([
780
- concurrentEngine.startRun({
781
- kind: "test.queue.order",
782
- input: { id: "first" },
783
- }),
784
- concurrentEngine.startRun({
785
- kind: "test.queue.order",
786
- input: { id: "second" },
787
- }),
788
- concurrentEngine.startRun({
789
- kind: "test.queue.order",
790
- input: { id: "third" },
791
- }),
768
+ concurrentEngine.startRun({ kind: 'test.queue.order', input: { id: 'first' } }),
769
+ concurrentEngine.startRun({ kind: 'test.queue.order', input: { id: 'second' } }),
770
+ concurrentEngine.startRun({ kind: 'test.queue.order', input: { id: 'third' } }),
792
771
  ]);
793
772
 
794
773
  // Wait for all to complete
795
- await Promise.all(
796
- runIds.map((id) => concurrentEngine.waitForRun(id, { timeout: 2000 })),
797
- );
774
+ await Promise.all(runIds.map(id => concurrentEngine.waitForRun(id, { timeout: 2000 })));
798
775
 
799
776
  // Should complete in order (FIFO)
800
- expect(completionOrder).toEqual(["first", "second", "third"]);
777
+ expect(completionOrder).toEqual(['first', 'second', 'third']);
801
778
 
802
779
  await concurrentEngine.shutdown();
803
780
  });
804
781
  });
805
782
 
806
- describe("initialize", () => {
807
- it("should call storage.initialize if available", async () => {
783
+ describe('initialize', () => {
784
+ it('should call storage.initialize if available', async () => {
808
785
  const initFn = vi.fn();
809
786
  const customStorage = new MemoryStorageAdapter();
810
787
  (customStorage as any).initialize = initFn;
@@ -819,36 +796,36 @@ describe("WorkflowEngine", () => {
819
796
  expect(initFn).toHaveBeenCalledTimes(1);
820
797
  });
821
798
 
822
- it("should not throw if storage has no initialize", async () => {
799
+ it('should not throw if storage has no initialize', async () => {
823
800
  await expect(engine.initialize()).resolves.not.toThrow();
824
801
  });
825
802
  });
826
803
 
827
- describe("shutdown", () => {
828
- it("should cancel active runs and close resources", async () => {
804
+ describe('shutdown', () => {
805
+ it('should cancel active runs and close resources', async () => {
829
806
  engine.registerWorkflow({
830
- kind: "test.shutdown",
831
- name: "Test Shutdown",
807
+ kind: 'test.shutdown',
808
+ name: 'Test Shutdown',
832
809
  steps: [
833
810
  {
834
- key: "step1",
835
- name: "Step 1",
811
+ key: 'step1',
812
+ name: 'Step 1',
836
813
  handler: async () => {
837
- await new Promise((resolve) => setTimeout(resolve, 5000));
838
- return "done";
814
+ await new Promise(resolve => setTimeout(resolve, 5000));
815
+ return 'done';
839
816
  },
840
817
  },
841
818
  ],
842
819
  });
843
820
 
844
- await engine.startRun({ kind: "test.shutdown" });
821
+ await engine.startRun({ kind: 'test.shutdown' });
845
822
  expect(engine.getActiveRunCount()).toBe(1);
846
823
 
847
824
  await engine.shutdown();
848
825
  expect(engine.getActiveRunCount()).toBe(0);
849
826
  });
850
827
 
851
- it("should call events.close and storage.close if available", async () => {
828
+ it('should call events.close and storage.close if available', async () => {
852
829
  const closeFn = vi.fn();
853
830
  const storageCloseFn = vi.fn();
854
831
  const customEvents = new MemoryEventTransport();
@@ -868,125 +845,117 @@ describe("WorkflowEngine", () => {
868
845
  });
869
846
  });
870
847
 
871
- describe("waitForRun", () => {
872
- it("should throw RunNotFoundError if run does not exist", async () => {
873
- await expect(engine.waitForRun("nonexistent")).rejects.toThrow(
874
- "not found",
875
- );
848
+ describe('waitForRun', () => {
849
+ it('should throw RunNotFoundError if run does not exist', async () => {
850
+ await expect(engine.waitForRun('nonexistent')).rejects.toThrow('not found');
876
851
  });
877
852
 
878
- it("should timeout if run never completes", async () => {
853
+ it('should timeout if run never completes', async () => {
879
854
  engine.registerWorkflow({
880
- kind: "test.wait.timeout",
881
- name: "Test Wait Timeout",
855
+ kind: 'test.wait.timeout',
856
+ name: 'Test Wait Timeout',
882
857
  steps: [
883
858
  {
884
- key: "step1",
885
- name: "Step 1",
859
+ key: 'step1',
860
+ name: 'Step 1',
886
861
  handler: async () => {
887
- await new Promise((resolve) => setTimeout(resolve, 10000));
888
- return "done";
862
+ await new Promise(resolve => setTimeout(resolve, 10000));
863
+ return 'done';
889
864
  },
890
865
  },
891
866
  ],
892
867
  });
893
868
 
894
- const runId = await engine.startRun({ kind: "test.wait.timeout" });
869
+ const runId = await engine.startRun({ kind: 'test.wait.timeout' });
895
870
 
896
871
  await expect(
897
- engine.waitForRun(runId, { timeout: 100, pollInterval: 20 }),
898
- ).rejects.toThrow("Timeout waiting for run");
872
+ engine.waitForRun(runId, { timeout: 100, pollInterval: 20 })
873
+ ).rejects.toThrow('Timeout waiting for run');
899
874
 
900
875
  await engine.cancelRun(runId);
901
876
  });
902
877
  });
903
878
 
904
- describe("cancelRun", () => {
905
- it("should throw RunNotFoundError for non-existent run", async () => {
906
- await expect(engine.cancelRun("nonexistent")).rejects.toThrow(
907
- "not found",
908
- );
879
+ describe('cancelRun', () => {
880
+ it('should throw RunNotFoundError for non-existent run', async () => {
881
+ await expect(engine.cancelRun('nonexistent')).rejects.toThrow('not found');
909
882
  });
910
883
 
911
- it("should update status even if run is not actively running", async () => {
884
+ it('should update status even if run is not actively running', async () => {
912
885
  engine.registerWorkflow({
913
- kind: "test.cancel.inactive",
914
- name: "Test Cancel Inactive",
886
+ kind: 'test.cancel.inactive',
887
+ name: 'Test Cancel Inactive',
915
888
  steps: [
916
889
  {
917
- key: "step1",
918
- name: "Step 1",
919
- handler: async () => "done",
890
+ key: 'step1',
891
+ name: 'Step 1',
892
+ handler: async () => 'done',
920
893
  },
921
894
  ],
922
895
  });
923
896
 
924
- const runId = await engine.startRun({ kind: "test.cancel.inactive" });
897
+ const runId = await engine.startRun({ kind: 'test.cancel.inactive' });
925
898
  await engine.waitForRun(runId);
926
899
 
927
900
  // Run is already complete — cancelRun is a no-op for terminal statuses
928
901
  await engine.cancelRun(runId);
929
902
  const run = await engine.getRunStatus(runId);
930
- expect(run?.status).toBe("succeeded");
903
+ expect(run?.status).toBe('succeeded');
931
904
  });
932
905
  });
933
906
 
934
- describe("resumeRun", () => {
935
- it("should throw RunNotFoundError for non-existent run", async () => {
936
- await expect(engine.resumeRun("nonexistent")).rejects.toThrow(
937
- "not found",
938
- );
907
+ describe('resumeRun', () => {
908
+ it('should throw RunNotFoundError for non-existent run', async () => {
909
+ await expect(engine.resumeRun('nonexistent')).rejects.toThrow('not found');
939
910
  });
940
911
 
941
- it("should throw if run status is not resumable", async () => {
912
+ it('should throw if run status is not resumable', async () => {
942
913
  engine.registerWorkflow({
943
- kind: "test.resume.completed",
944
- name: "Test",
945
- steps: [{ key: "s1", name: "S1", handler: async () => "done" }],
914
+ kind: 'test.resume.completed',
915
+ name: 'Test',
916
+ steps: [
917
+ { key: 's1', name: 'S1', handler: async () => 'done' },
918
+ ],
946
919
  });
947
920
 
948
- const runId = await engine.startRun({ kind: "test.resume.completed" });
921
+ const runId = await engine.startRun({ kind: 'test.resume.completed' });
949
922
  await engine.waitForRun(runId);
950
923
 
951
- await expect(engine.resumeRun(runId)).rejects.toThrow(
952
- "Cannot resume run",
953
- );
924
+ await expect(engine.resumeRun(runId)).rejects.toThrow('Cannot resume run');
954
925
  });
955
926
 
956
- it("should throw if workflow is not registered", async () => {
927
+ it('should throw if workflow is not registered', async () => {
957
928
  // Create a run directly in storage with a kind that is not registered
958
929
  const run = await storage.createRun({
959
- kind: "unregistered.workflow",
960
- status: "running",
930
+ kind: 'unregistered.workflow',
931
+ status: 'running',
961
932
  input: {},
962
933
  metadata: {},
963
934
  context: {},
964
935
  });
965
936
 
966
- await expect(engine.resumeRun(run.id)).rejects.toThrow(
967
- "is not registered",
968
- );
937
+ await expect(engine.resumeRun(run.id)).rejects.toThrow('is not registered');
969
938
  });
970
939
 
971
- it("should skip resume if run is already active", async () => {
940
+ it('should skip resume if run is already active', async () => {
972
941
  engine.registerWorkflow({
973
- kind: "test.resume.active",
974
- name: "Test",
942
+ kind: 'test.resume.active',
943
+ name: 'Test',
975
944
  steps: [
976
945
  {
977
- key: "s1",
978
- name: "S1",
946
+ key: 's1',
947
+ name: 'S1',
979
948
  handler: async () => {
980
- await new Promise((resolve) => setTimeout(resolve, 500));
981
- return "done";
949
+ await new Promise(resolve => setTimeout(resolve, 500));
950
+ return 'done';
982
951
  },
983
952
  },
984
953
  ],
985
954
  });
986
955
 
987
- const runId = await engine.startRun({ kind: "test.resume.active" });
956
+ const runId = await engine.startRun({ kind: 'test.resume.active' });
988
957
  // Manually set status to running for resume check
989
- await storage.updateRun(runId, { status: "running" });
958
+ await storage.updateRun(runId, { status: 'running' });
990
959
 
991
960
  // Attempt resume while the run is still active
992
961
  const result = await engine.resumeRun(runId);
@@ -995,27 +964,27 @@ describe("WorkflowEngine", () => {
995
964
  await engine.waitForRun(runId, { timeout: 2000 });
996
965
  });
997
966
 
998
- it("should resume from checkpoint", async () => {
967
+ it('should resume from checkpoint', async () => {
999
968
  const executionOrder: string[] = [];
1000
969
 
1001
970
  engine.registerWorkflow({
1002
- kind: "test.resume.checkpoint",
1003
- name: "Test Resume",
971
+ kind: 'test.resume.checkpoint',
972
+ name: 'Test Resume',
1004
973
  steps: [
1005
974
  {
1006
- key: "step1",
1007
- name: "Step 1",
975
+ key: 'step1',
976
+ name: 'Step 1',
1008
977
  handler: async () => {
1009
- executionOrder.push("step1");
1010
- return "r1";
978
+ executionOrder.push('step1');
979
+ return 'r1';
1011
980
  },
1012
981
  },
1013
982
  {
1014
- key: "step2",
1015
- name: "Step 2",
983
+ key: 'step2',
984
+ name: 'Step 2',
1016
985
  handler: async () => {
1017
- executionOrder.push("step2");
1018
- return "r2";
986
+ executionOrder.push('step2');
987
+ return 'r2';
1019
988
  },
1020
989
  },
1021
990
  ],
@@ -1023,51 +992,54 @@ describe("WorkflowEngine", () => {
1023
992
 
1024
993
  // Create a run that looks interrupted with step1 already done
1025
994
  const run = await storage.createRun({
1026
- kind: "test.resume.checkpoint",
1027
- status: "running",
995
+ kind: 'test.resume.checkpoint',
996
+ status: 'running',
1028
997
  input: {},
1029
998
  metadata: {},
1030
- context: { step1: "r1" },
999
+ context: { step1: 'r1' },
1000
+ completedSteps: ['step1'],
1031
1001
  });
1032
1002
 
1033
1003
  const resumedId = await engine.resumeRun(run.id);
1034
1004
  expect(resumedId).toBe(run.id);
1035
1005
 
1036
1006
  const result = await engine.waitForRun(run.id, { timeout: 2000 });
1037
- expect(result.status).toBe("succeeded");
1007
+ expect(result.status).toBe('succeeded');
1038
1008
 
1039
1009
  // Only step2 should have executed
1040
- expect(executionOrder).toEqual(["step2"]);
1010
+ expect(executionOrder).toEqual(['step2']);
1041
1011
  });
1042
1012
  });
1043
1013
 
1044
- describe("resumeAllInterrupted", () => {
1045
- it("should resume all resumable runs", async () => {
1014
+ describe('resumeAllInterrupted', () => {
1015
+ it('should resume all resumable runs', async () => {
1046
1016
  engine.registerWorkflow({
1047
- kind: "test.resumeAll",
1048
- name: "Test",
1049
- steps: [{ key: "s1", name: "S1", handler: async () => "done" }],
1017
+ kind: 'test.resumeAll',
1018
+ name: 'Test',
1019
+ steps: [
1020
+ { key: 's1', name: 'S1', handler: async () => 'done' },
1021
+ ],
1050
1022
  });
1051
1023
 
1052
1024
  // Create two interrupted runs
1053
1025
  await storage.createRun({
1054
- kind: "test.resumeAll",
1055
- status: "running",
1026
+ kind: 'test.resumeAll',
1027
+ status: 'running',
1056
1028
  input: {},
1057
1029
  metadata: {},
1058
1030
  context: {},
1059
1031
  });
1060
1032
  await storage.createRun({
1061
- kind: "test.resumeAll",
1062
- status: "queued",
1033
+ kind: 'test.resumeAll',
1034
+ status: 'queued',
1063
1035
  input: {},
1064
1036
  metadata: {},
1065
1037
  context: {},
1066
1038
  });
1067
1039
  // One completed run (should be skipped)
1068
1040
  await storage.createRun({
1069
- kind: "test.resumeAll",
1070
- status: "succeeded",
1041
+ kind: 'test.resumeAll',
1042
+ status: 'succeeded',
1071
1043
  input: {},
1072
1044
  metadata: {},
1073
1045
  context: {},
@@ -1082,10 +1054,10 @@ describe("WorkflowEngine", () => {
1082
1054
  }
1083
1055
  });
1084
1056
 
1085
- it("should skip runs for unregistered workflows", async () => {
1057
+ it('should skip runs for unregistered workflows', async () => {
1086
1058
  await storage.createRun({
1087
- kind: "unregistered.workflow",
1088
- status: "running",
1059
+ kind: 'unregistered.workflow',
1060
+ status: 'running',
1089
1061
  input: {},
1090
1062
  metadata: {},
1091
1063
  context: {},
@@ -1096,86 +1068,62 @@ describe("WorkflowEngine", () => {
1096
1068
  });
1097
1069
  });
1098
1070
 
1099
- describe("getResumableRuns", () => {
1100
- it("should return only queued and running runs", async () => {
1101
- await storage.createRun({
1102
- kind: "a",
1103
- status: "queued",
1104
- input: {},
1105
- metadata: {},
1106
- context: {},
1107
- });
1108
- await storage.createRun({
1109
- kind: "a",
1110
- status: "running",
1111
- input: {},
1112
- metadata: {},
1113
- context: {},
1114
- });
1115
- await storage.createRun({
1116
- kind: "a",
1117
- status: "succeeded",
1118
- input: {},
1119
- metadata: {},
1120
- context: {},
1121
- });
1122
- await storage.createRun({
1123
- kind: "a",
1124
- status: "failed",
1125
- input: {},
1126
- metadata: {},
1127
- context: {},
1128
- });
1071
+ describe('getResumableRuns', () => {
1072
+ it('should return only queued and running runs', async () => {
1073
+ await storage.createRun({ kind: 'a', status: 'queued', input: {}, metadata: {}, context: {} });
1074
+ await storage.createRun({ kind: 'a', status: 'running', input: {}, metadata: {}, context: {} });
1075
+ await storage.createRun({ kind: 'a', status: 'succeeded', input: {}, metadata: {}, context: {} });
1076
+ await storage.createRun({ kind: 'a', status: 'failed', input: {}, metadata: {}, context: {} });
1129
1077
 
1130
1078
  const resumable = await engine.getResumableRuns();
1131
1079
  expect(resumable).toHaveLength(2);
1132
- expect(
1133
- resumable.every((r) => ["queued", "running"].includes(r.status)),
1134
- ).toBe(true);
1080
+ expect(resumable.every(r => ['queued', 'running'].includes(r.status))).toBe(true);
1135
1081
  });
1136
1082
  });
1137
1083
 
1138
- describe("delayed execution", () => {
1139
- it("should delay workflow start", async () => {
1084
+ describe('delayed execution', () => {
1085
+ it('should delay workflow start', async () => {
1140
1086
  engine.registerWorkflow({
1141
- kind: "test.delay",
1142
- name: "Test Delay",
1087
+ kind: 'test.delay',
1088
+ name: 'Test Delay',
1143
1089
  steps: [
1144
1090
  {
1145
- key: "step1",
1146
- name: "Step 1",
1147
- handler: async () => "done",
1091
+ key: 'step1',
1092
+ name: 'Step 1',
1093
+ handler: async () => 'done',
1148
1094
  },
1149
1095
  ],
1150
1096
  });
1151
1097
 
1152
1098
  const start = Date.now();
1153
- const runId = await engine.startRun({ kind: "test.delay", delay: 100 });
1099
+ const runId = await engine.startRun({ kind: 'test.delay', delay: 100 });
1154
1100
  const run = await engine.waitForRun(runId, { timeout: 2000 });
1155
1101
 
1156
- expect(run.status).toBe("succeeded");
1102
+ expect(run.status).toBe('succeeded');
1157
1103
  expect(Date.now() - start).toBeGreaterThanOrEqual(90);
1158
1104
  });
1159
1105
  });
1160
1106
 
1161
- describe("getStorage / getEvents", () => {
1162
- it("should return storage and events adapters", () => {
1107
+ describe('getStorage / getEvents', () => {
1108
+ it('should return storage and events adapters', () => {
1163
1109
  expect(engine.getStorage()).toBe(storage);
1164
1110
  expect(engine.getEvents()).toBe(events);
1165
1111
  });
1166
1112
  });
1167
1113
 
1168
- describe("subscribeToRun", () => {
1169
- it("should receive events for specific run", async () => {
1114
+ describe('subscribeToRun', () => {
1115
+ it('should receive events for specific run', async () => {
1170
1116
  const receivedEvents: WorkflowEvent[] = [];
1171
1117
 
1172
1118
  engine.registerWorkflow({
1173
- kind: "test.sub",
1174
- name: "Test",
1175
- steps: [{ key: "s1", name: "S1", handler: async () => "done" }],
1119
+ kind: 'test.sub',
1120
+ name: 'Test',
1121
+ steps: [
1122
+ { key: 's1', name: 'S1', handler: async () => 'done' },
1123
+ ],
1176
1124
  });
1177
1125
 
1178
- const runId = await engine.startRun({ kind: "test.sub" });
1126
+ const runId = await engine.startRun({ kind: 'test.sub' });
1179
1127
  const unsub = engine.subscribeToRun(runId, (event) => {
1180
1128
  receivedEvents.push(event);
1181
1129
  });
@@ -1184,19 +1132,19 @@ describe("WorkflowEngine", () => {
1184
1132
  unsub();
1185
1133
 
1186
1134
  expect(receivedEvents.length).toBeGreaterThan(0);
1187
- expect(receivedEvents.every((e) => e.runId === runId)).toBe(true);
1135
+ expect(receivedEvents.every(e => e.runId === runId)).toBe(true);
1188
1136
  });
1189
1137
  });
1190
1138
 
1191
- describe("default constructor", () => {
1192
- it("should use defaults when no config is provided", () => {
1139
+ describe('default constructor', () => {
1140
+ it('should use defaults when no config is provided', () => {
1193
1141
  const defaultEngine = new WorkflowEngine();
1194
1142
  expect(defaultEngine.getStorage()).toBeInstanceOf(MemoryStorageAdapter);
1195
1143
  });
1196
1144
  });
1197
1145
 
1198
- describe("priority queues", () => {
1199
- it("should process high priority runs first", async () => {
1146
+ describe('priority queues', () => {
1147
+ it('should process high priority runs first', async () => {
1200
1148
  const completionOrder: string[] = [];
1201
1149
 
1202
1150
  const priorityEngine = new WorkflowEngine({
@@ -1209,15 +1157,15 @@ describe("WorkflowEngine", () => {
1209
1157
  });
1210
1158
 
1211
1159
  priorityEngine.registerWorkflow({
1212
- kind: "test.priority",
1213
- name: "Test Priority",
1160
+ kind: 'test.priority',
1161
+ name: 'Test Priority',
1214
1162
  steps: [
1215
1163
  {
1216
- key: "step1",
1217
- name: "Step 1",
1164
+ key: 'step1',
1165
+ name: 'Step 1',
1218
1166
  handler: async (ctx) => {
1219
1167
  completionOrder.push(ctx.input.id as string);
1220
- return "done";
1168
+ return 'done';
1221
1169
  },
1222
1170
  },
1223
1171
  ],
@@ -1225,28 +1173,28 @@ describe("WorkflowEngine", () => {
1225
1173
 
1226
1174
  // Start first run (occupies the single slot)
1227
1175
  const firstId = await priorityEngine.startRun({
1228
- kind: "test.priority",
1229
- input: { id: "first" },
1176
+ kind: 'test.priority',
1177
+ input: { id: 'first' },
1230
1178
  priority: 0,
1231
1179
  });
1232
1180
 
1233
1181
  // Queue 3 runs with different priorities
1234
1182
  // Low priority first, then high priority
1235
1183
  const lowId = await priorityEngine.startRun({
1236
- kind: "test.priority",
1237
- input: { id: "low" },
1184
+ kind: 'test.priority',
1185
+ input: { id: 'low' },
1238
1186
  priority: 0,
1239
1187
  });
1240
1188
 
1241
1189
  const highId = await priorityEngine.startRun({
1242
- kind: "test.priority",
1243
- input: { id: "high" },
1190
+ kind: 'test.priority',
1191
+ input: { id: 'high' },
1244
1192
  priority: 10,
1245
1193
  });
1246
1194
 
1247
1195
  const mediumId = await priorityEngine.startRun({
1248
- kind: "test.priority",
1249
- input: { id: "medium" },
1196
+ kind: 'test.priority',
1197
+ input: { id: 'medium' },
1250
1198
  priority: 5,
1251
1199
  });
1252
1200
 
@@ -1260,12 +1208,12 @@ describe("WorkflowEngine", () => {
1260
1208
 
1261
1209
  // First should complete first (already running)
1262
1210
  // Then high priority, medium, low
1263
- expect(completionOrder).toEqual(["first", "high", "medium", "low"]);
1211
+ expect(completionOrder).toEqual(['first', 'high', 'medium', 'low']);
1264
1212
 
1265
1213
  await priorityEngine.shutdown();
1266
1214
  });
1267
1215
 
1268
- it("should emit run.queued and run.dequeued events", async () => {
1216
+ it('should emit run.queued and run.dequeued events', async () => {
1269
1217
  const queuedEvents: WorkflowEvent[] = [];
1270
1218
  const dequeuedEvents: WorkflowEvent[] = [];
1271
1219
 
@@ -1279,15 +1227,15 @@ describe("WorkflowEngine", () => {
1279
1227
  });
1280
1228
 
1281
1229
  priorityEngine.registerWorkflow({
1282
- kind: "test.queue.events",
1283
- name: "Test Queue Events",
1230
+ kind: 'test.queue.events',
1231
+ name: 'Test Queue Events',
1284
1232
  steps: [
1285
1233
  {
1286
- key: "step1",
1287
- name: "Step 1",
1234
+ key: 'step1',
1235
+ name: 'Step 1',
1288
1236
  handler: async () => {
1289
- await new Promise((resolve) => setTimeout(resolve, 50));
1290
- return "done";
1237
+ await new Promise(resolve => setTimeout(resolve, 50));
1238
+ return 'done';
1291
1239
  },
1292
1240
  },
1293
1241
  ],
@@ -1295,21 +1243,17 @@ describe("WorkflowEngine", () => {
1295
1243
 
1296
1244
  // Subscribe to all events
1297
1245
  priorityEngine.subscribeToAll((event) => {
1298
- if (event.eventType === "run.queued") {
1246
+ if (event.eventType === 'run.queued') {
1299
1247
  queuedEvents.push(event);
1300
1248
  }
1301
- if (event.eventType === "run.dequeued") {
1249
+ if (event.eventType === 'run.dequeued') {
1302
1250
  dequeuedEvents.push(event);
1303
1251
  }
1304
1252
  });
1305
1253
 
1306
1254
  // Start 2 runs (first will run, second will queue)
1307
- const firstId = await priorityEngine.startRun({
1308
- kind: "test.queue.events",
1309
- });
1310
- const secondId = await priorityEngine.startRun({
1311
- kind: "test.queue.events",
1312
- });
1255
+ const firstId = await priorityEngine.startRun({ kind: 'test.queue.events' });
1256
+ const secondId = await priorityEngine.startRun({ kind: 'test.queue.events' });
1313
1257
 
1314
1258
  // Wait for both to complete
1315
1259
  await Promise.all([
@@ -1327,95 +1271,165 @@ describe("WorkflowEngine", () => {
1327
1271
  });
1328
1272
  });
1329
1273
 
1330
- describe("orchestrator: raceWithAbort pre-aborted signal", () => {
1331
- it("should throw WorkflowCanceledError immediately when signal is already aborted", async () => {
1274
+ describe('processQueue resilience (audit 2026-04-27 C3)', () => {
1275
+ it('continues processing remaining queued items when one item fails to dispatch', async () => {
1276
+ // Drive a dispatch failure by injecting an event transport whose
1277
+ // run.dequeued emit throws once. Pre-fix, the throw escaped processQueue
1278
+ // and the remaining queued items were stranded; post-fix, the loop logs,
1279
+ // records telemetry, and advances to the next item.
1280
+ const baseEvents = new MemoryEventTransport();
1281
+ let dequeueCount = 0;
1282
+ const flakyEvents: typeof baseEvents = Object.create(baseEvents);
1283
+ flakyEvents.emit = (event) => {
1284
+ if (event.eventType === 'run.dequeued') {
1285
+ dequeueCount++;
1286
+ if (dequeueCount === 1) {
1287
+ throw new Error('boom: dequeue handler exploded');
1288
+ }
1289
+ }
1290
+ return baseEvents.emit.call(baseEvents, event);
1291
+ };
1292
+ flakyEvents.subscribe = baseEvents.subscribe.bind(baseEvents);
1293
+ flakyEvents.subscribeAll = baseEvents.subscribeAll.bind(baseEvents);
1294
+
1295
+ const customStorage = new MemoryStorageAdapter();
1296
+ const eng = new WorkflowEngine({
1297
+ storage: customStorage,
1298
+ events: flakyEvents,
1299
+ logger: new SilentLogger(),
1300
+ settings: { maxConcurrency: 1 },
1301
+ });
1302
+
1303
+ const completionOrder: string[] = [];
1304
+ eng.registerWorkflow({
1305
+ kind: 'test.queue.resilient',
1306
+ name: 'Test Queue Resilient',
1307
+ steps: [
1308
+ {
1309
+ key: 's1',
1310
+ name: 'S1',
1311
+ handler: async (ctx) => {
1312
+ completionOrder.push(ctx.input.id as string);
1313
+ return 'done';
1314
+ },
1315
+ },
1316
+ ],
1317
+ });
1318
+
1319
+ // First runs immediately (no dequeue event emitted for it).
1320
+ const firstId = await eng.startRun({ kind: 'test.queue.resilient', input: { id: 'first' } });
1321
+ // These two get queued behind it; their run.dequeued events fire when
1322
+ // the queue advances.
1323
+ const secondId = await eng.startRun({ kind: 'test.queue.resilient', input: { id: 'second' } });
1324
+ const thirdId = await eng.startRun({ kind: 'test.queue.resilient', input: { id: 'third' } });
1325
+
1326
+ // First and third must complete; second's dispatch fails and is recorded
1327
+ // as failed instead of stranding the queue.
1328
+ await eng.waitForRun(firstId, { timeout: 2000 });
1329
+ await eng.waitForRun(thirdId, { timeout: 2000 });
1330
+
1331
+ expect(completionOrder).toContain('first');
1332
+ expect(completionOrder).toContain('third');
1333
+ expect(completionOrder).not.toContain('second');
1334
+
1335
+ // Telemetry: the dropped run should be marked failed in storage.
1336
+ const secondRun = await customStorage.getRun(secondId);
1337
+ expect(secondRun?.status).toBe('failed');
1338
+ expect(secondRun?.error?.code).toBe('QUEUE_DISPATCH_FAILED');
1339
+
1340
+ await eng.shutdown();
1341
+ });
1342
+ });
1343
+
1344
+ describe('orchestrator: raceWithAbort pre-aborted signal', () => {
1345
+ it('should throw WorkflowCanceledError immediately when signal is already aborted', async () => {
1332
1346
  engine.registerWorkflow({
1333
- kind: "test.preabort",
1334
- name: "Test Pre-Abort",
1347
+ kind: 'test.preabort',
1348
+ name: 'Test Pre-Abort',
1335
1349
  steps: [
1336
1350
  {
1337
- key: "step1",
1338
- name: "Step 1",
1351
+ key: 'step1',
1352
+ name: 'Step 1',
1339
1353
  handler: async () => {
1340
- await new Promise((resolve) => setTimeout(resolve, 50));
1341
- return "done";
1354
+ await new Promise(resolve => setTimeout(resolve, 50));
1355
+ return 'done';
1342
1356
  },
1343
1357
  },
1344
1358
  ],
1345
1359
  });
1346
1360
 
1347
- const runId = await engine.startRun({ kind: "test.preabort" });
1361
+ const runId = await engine.startRun({ kind: 'test.preabort' });
1348
1362
 
1349
1363
  // Cancel immediately (before step starts executing in the microtask)
1350
1364
  engine.cancelRun(runId);
1351
1365
 
1352
1366
  const run = await engine.waitForRun(runId, { timeout: 2000 });
1353
- expect(run.status).toBe("canceled");
1367
+ expect(run.status).toBe('canceled');
1354
1368
  });
1355
1369
  });
1356
1370
 
1357
- describe("orchestrator: afterRun hook error swallowing", () => {
1358
- it("should swallow afterRun hook error on failure path and still return failed result", async () => {
1371
+ describe('orchestrator: afterRun hook error swallowing', () => {
1372
+ it('should swallow afterRun hook error on failure path and still return failed result', async () => {
1359
1373
  engine.registerWorkflow({
1360
- kind: "test.hook.afterrun.error",
1361
- name: "Test afterRun Hook Error",
1374
+ kind: 'test.hook.afterrun.error',
1375
+ name: 'Test afterRun Hook Error',
1362
1376
  steps: [
1363
1377
  {
1364
- key: "fail-step",
1365
- name: "Failing Step",
1378
+ key: 'fail-step',
1379
+ name: 'Failing Step',
1366
1380
  handler: async () => {
1367
- throw new Error("Step failure");
1381
+ throw new Error('Step failure');
1368
1382
  },
1369
1383
  },
1370
1384
  ],
1371
1385
  hooks: {
1372
1386
  afterRun: async () => {
1373
- throw new Error("afterRun hook exploded");
1387
+ throw new Error('afterRun hook exploded');
1374
1388
  },
1375
1389
  },
1376
1390
  });
1377
1391
 
1378
- const runId = await engine.startRun({ kind: "test.hook.afterrun.error" });
1392
+ const runId = await engine.startRun({ kind: 'test.hook.afterrun.error' });
1379
1393
  const run = await engine.waitForRun(runId, { timeout: 2000 });
1380
1394
 
1381
1395
  // The workflow should fail with the original step error, not the hook error
1382
- expect(run.status).toBe("failed");
1383
- expect(run.error?.message).toContain("Step failure");
1396
+ expect(run.status).toBe('failed');
1397
+ expect(run.error?.message).toContain('Step failure');
1384
1398
  });
1385
1399
  });
1386
1400
 
1387
- describe("orchestrator: executeWithTimeout abort-during-step", () => {
1388
- it("should cancel a step with per-step timeout when workflow-level abort fires", async () => {
1401
+ describe('orchestrator: executeWithTimeout abort-during-step', () => {
1402
+ it('should cancel a step with per-step timeout when workflow-level abort fires', async () => {
1389
1403
  let stepStarted = false;
1390
1404
 
1391
1405
  engine.registerWorkflow({
1392
- kind: "test.timeout.abort",
1393
- name: "Test Timeout Abort",
1406
+ kind: 'test.timeout.abort',
1407
+ name: 'Test Timeout Abort',
1394
1408
  steps: [
1395
1409
  {
1396
- key: "long-step",
1397
- name: "Long Step",
1410
+ key: 'long-step',
1411
+ name: 'Long Step',
1398
1412
  timeout: 60000, // 60s per-step timeout
1399
1413
  handler: async () => {
1400
1414
  stepStarted = true;
1401
1415
  // This step takes a long time
1402
- await new Promise((resolve) => setTimeout(resolve, 10000));
1403
- return "done";
1416
+ await new Promise(resolve => setTimeout(resolve, 10000));
1417
+ return 'done';
1404
1418
  },
1405
1419
  },
1406
1420
  ],
1407
1421
  });
1408
1422
 
1409
- const runId = await engine.startRun({ kind: "test.timeout.abort" });
1423
+ const runId = await engine.startRun({ kind: 'test.timeout.abort' });
1410
1424
 
1411
1425
  // Wait until the step has started, then cancel
1412
- await new Promise((resolve) => setTimeout(resolve, 50));
1426
+ await new Promise(resolve => setTimeout(resolve, 50));
1413
1427
  expect(stepStarted).toBe(true);
1414
1428
 
1415
1429
  engine.cancelRun(runId);
1416
1430
 
1417
1431
  const run = await engine.waitForRun(runId, { timeout: 2000 });
1418
- expect(run.status).toBe("canceled");
1432
+ expect(run.status).toBe('canceled');
1419
1433
  });
1420
1434
  });
1421
1435
  });