@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,16 +1,12 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import {
3
- CronScheduler,
4
- type CronSchedulerConfig,
5
- type SchedulePersistence,
6
- } from "./cron";
7
- import { WorkflowEngine } from "../core/engine";
8
- import { MemoryStorageAdapter } from "../storage/memory";
9
- import { MemoryEventTransport } from "../events/memory";
10
- import { SilentLogger } from "../utils/logger";
11
- import type { WorkflowSchedule } from "./types";
12
-
13
- describe("CronScheduler", () => {
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { CronScheduler, type CronSchedulerConfig, type SchedulePersistence } from './cron';
3
+ import { WorkflowEngine } from '../core/engine';
4
+ import { MemoryStorageAdapter } from '../storage/memory';
5
+ import { MemoryEventTransport } from '../events/memory';
6
+ import { SilentLogger } from '../utils/logger';
7
+ import type { WorkflowSchedule } from './types';
8
+
9
+ describe('CronScheduler', () => {
14
10
  let engine: WorkflowEngine;
15
11
  let scheduler: CronScheduler;
16
12
 
@@ -24,13 +20,13 @@ describe("CronScheduler", () => {
24
20
 
25
21
  // Register a test workflow
26
22
  engine.registerWorkflow({
27
- kind: "test.workflow",
28
- name: "Test Workflow",
23
+ kind: 'test.workflow',
24
+ name: 'Test Workflow',
29
25
  steps: [
30
26
  {
31
- key: "step1",
32
- name: "Step 1",
33
- handler: async (ctx) => ({ value: ctx.input.value ?? "default" }),
27
+ key: 'step1',
28
+ name: 'Step 1',
29
+ handler: async (ctx) => ({ value: ctx.input.value ?? 'default' }),
34
30
  },
35
31
  ],
36
32
  });
@@ -48,64 +44,64 @@ describe("CronScheduler", () => {
48
44
  await engine.shutdown();
49
45
  });
50
46
 
51
- describe("addSchedule", () => {
52
- it("should add a cron schedule", async () => {
47
+ describe('addSchedule', () => {
48
+ it('should add a cron schedule', async () => {
53
49
  const schedule = await scheduler.addSchedule({
54
- workflowKind: "test.workflow",
55
- triggerType: "cron",
56
- cronExpression: "0 * * * *", // Every hour
50
+ workflowKind: 'test.workflow',
51
+ triggerType: 'cron',
52
+ cronExpression: '0 * * * *', // Every hour
57
53
  enabled: true,
58
54
  });
59
55
 
60
56
  expect(schedule.id).toBeDefined();
61
- expect(schedule.workflowKind).toBe("test.workflow");
62
- expect(schedule.triggerType).toBe("cron");
63
- expect(schedule.cronExpression).toBe("0 * * * *");
57
+ expect(schedule.workflowKind).toBe('test.workflow');
58
+ expect(schedule.triggerType).toBe('cron');
59
+ expect(schedule.cronExpression).toBe('0 * * * *');
64
60
  expect(schedule.nextRunAt).toBeInstanceOf(Date);
65
61
  });
66
62
 
67
- it("should add a workflow completion trigger", async () => {
63
+ it('should add a workflow completion trigger', async () => {
68
64
  const schedule = await scheduler.addSchedule({
69
- workflowKind: "notification.send",
70
- triggerType: "workflow_completed",
71
- triggerOnWorkflowKind: "order.process",
72
- triggerOnStatus: ["succeeded"],
65
+ workflowKind: 'notification.send',
66
+ triggerType: 'workflow_completed',
67
+ triggerOnWorkflowKind: 'order.process',
68
+ triggerOnStatus: ['succeeded'],
73
69
  enabled: true,
74
70
  });
75
71
 
76
72
  expect(schedule.id).toBeDefined();
77
- expect(schedule.triggerType).toBe("workflow_completed");
78
- expect(schedule.triggerOnWorkflowKind).toBe("order.process");
73
+ expect(schedule.triggerType).toBe('workflow_completed');
74
+ expect(schedule.triggerOnWorkflowKind).toBe('order.process');
79
75
  });
80
76
 
81
- it("should reject invalid cron expression", async () => {
77
+ it('should reject invalid cron expression', async () => {
82
78
  await expect(
83
79
  scheduler.addSchedule({
84
- workflowKind: "test.workflow",
85
- triggerType: "cron",
86
- cronExpression: "invalid cron",
80
+ workflowKind: 'test.workflow',
81
+ triggerType: 'cron',
82
+ cronExpression: 'invalid cron',
87
83
  enabled: true,
88
- }),
89
- ).rejects.toThrow("Invalid cron expression");
84
+ })
85
+ ).rejects.toThrow('Invalid cron expression');
90
86
  });
91
87
 
92
- it("should require triggerOnWorkflowKind for workflow_completed triggers", async () => {
88
+ it('should require triggerOnWorkflowKind for workflow_completed triggers', async () => {
93
89
  await expect(
94
90
  scheduler.addSchedule({
95
- workflowKind: "test.workflow",
96
- triggerType: "workflow_completed",
91
+ workflowKind: 'test.workflow',
92
+ triggerType: 'workflow_completed',
97
93
  enabled: true,
98
- }),
99
- ).rejects.toThrow("triggerOnWorkflowKind is required");
94
+ })
95
+ ).rejects.toThrow('triggerOnWorkflowKind is required');
100
96
  });
101
97
  });
102
98
 
103
- describe("removeSchedule", () => {
104
- it("should remove a schedule", async () => {
99
+ describe('removeSchedule', () => {
100
+ it('should remove a schedule', async () => {
105
101
  const schedule = await scheduler.addSchedule({
106
- workflowKind: "test.workflow",
107
- triggerType: "cron",
108
- cronExpression: "0 * * * *",
102
+ workflowKind: 'test.workflow',
103
+ triggerType: 'cron',
104
+ cronExpression: '0 * * * *',
109
105
  enabled: true,
110
106
  });
111
107
 
@@ -115,19 +111,19 @@ describe("CronScheduler", () => {
115
111
  expect(schedules).toHaveLength(0);
116
112
  });
117
113
 
118
- it("should throw for non-existent schedule", async () => {
119
- await expect(scheduler.removeSchedule("nonexistent")).rejects.toThrow(
120
- "Schedule not found",
114
+ it('should throw for non-existent schedule', async () => {
115
+ await expect(scheduler.removeSchedule('nonexistent')).rejects.toThrow(
116
+ 'Schedule not found'
121
117
  );
122
118
  });
123
119
  });
124
120
 
125
- describe("updateSchedule", () => {
126
- it("should update schedule properties", async () => {
121
+ describe('updateSchedule', () => {
122
+ it('should update schedule properties', async () => {
127
123
  const schedule = await scheduler.addSchedule({
128
- workflowKind: "test.workflow",
129
- triggerType: "cron",
130
- cronExpression: "0 * * * *",
124
+ workflowKind: 'test.workflow',
125
+ triggerType: 'cron',
126
+ cronExpression: '0 * * * *',
131
127
  enabled: true,
132
128
  });
133
129
 
@@ -137,38 +133,38 @@ describe("CronScheduler", () => {
137
133
  expect(updated?.enabled).toBe(false);
138
134
  });
139
135
 
140
- it("should update cron expression and recalculate next run", async () => {
136
+ it('should update cron expression and recalculate next run', async () => {
141
137
  const schedule = await scheduler.addSchedule({
142
- workflowKind: "test.workflow",
143
- triggerType: "cron",
144
- cronExpression: "0 * * * *",
138
+ workflowKind: 'test.workflow',
139
+ triggerType: 'cron',
140
+ cronExpression: '0 * * * *',
145
141
  enabled: true,
146
142
  });
147
143
 
148
144
  const originalNextRun = schedule.nextRunAt;
149
145
 
150
146
  await scheduler.updateSchedule(schedule.id, {
151
- cronExpression: "30 * * * *",
147
+ cronExpression: '30 * * * *',
152
148
  });
153
149
 
154
150
  const updated = scheduler.getSchedule(schedule.id);
155
- expect(updated?.cronExpression).toBe("30 * * * *");
151
+ expect(updated?.cronExpression).toBe('30 * * * *');
156
152
  expect(updated?.nextRunAt).not.toEqual(originalNextRun);
157
153
  });
158
154
  });
159
155
 
160
- describe("getSchedules", () => {
161
- it("should return all schedules", async () => {
156
+ describe('getSchedules', () => {
157
+ it('should return all schedules', async () => {
162
158
  await scheduler.addSchedule({
163
- workflowKind: "test.workflow",
164
- triggerType: "cron",
165
- cronExpression: "0 * * * *",
159
+ workflowKind: 'test.workflow',
160
+ triggerType: 'cron',
161
+ cronExpression: '0 * * * *',
166
162
  enabled: true,
167
163
  });
168
164
 
169
165
  await scheduler.addSchedule({
170
- workflowKind: "test.workflow",
171
- triggerType: "manual",
166
+ workflowKind: 'test.workflow',
167
+ triggerType: 'manual',
172
168
  enabled: true,
173
169
  });
174
170
 
@@ -177,13 +173,13 @@ describe("CronScheduler", () => {
177
173
  });
178
174
  });
179
175
 
180
- describe("triggerNow", () => {
181
- it("should manually trigger a schedule", async () => {
176
+ describe('triggerNow', () => {
177
+ it('should manually trigger a schedule', async () => {
182
178
  const schedule = await scheduler.addSchedule({
183
- workflowKind: "test.workflow",
184
- triggerType: "cron",
185
- cronExpression: "0 0 1 1 *", // Once a year (won't trigger naturally)
186
- input: { value: "manual" },
179
+ workflowKind: 'test.workflow',
180
+ triggerType: 'cron',
181
+ cronExpression: '0 0 1 1 *', // Once a year (won't trigger naturally)
182
+ input: { value: 'manual' },
187
183
  enabled: true,
188
184
  });
189
185
 
@@ -193,27 +189,27 @@ describe("CronScheduler", () => {
193
189
 
194
190
  // Wait for workflow to complete
195
191
  const run = await engine.waitForRun(runId);
196
- expect(run.status).toBe("succeeded");
192
+ expect(run.status).toBe('succeeded');
197
193
  });
198
194
 
199
- it("should throw for non-existent schedule", async () => {
200
- await expect(scheduler.triggerNow("nonexistent")).rejects.toThrow(
201
- "Schedule not found",
195
+ it('should throw for non-existent schedule', async () => {
196
+ await expect(scheduler.triggerNow('nonexistent')).rejects.toThrow(
197
+ 'Schedule not found'
202
198
  );
203
199
  });
204
200
  });
205
201
 
206
- describe("cron execution", () => {
207
- it("should execute scheduled workflow when cron time is reached", async () => {
202
+ describe('cron execution', () => {
203
+ it('should execute scheduled workflow when cron time is reached', async () => {
208
204
  // Use fake timers
209
205
  vi.useFakeTimers();
210
206
 
211
207
  // Create a schedule that runs every minute
212
208
  await scheduler.addSchedule({
213
- workflowKind: "test.workflow",
214
- triggerType: "cron",
215
- cronExpression: "* * * * *", // Every minute
216
- input: { value: "scheduled" },
209
+ workflowKind: 'test.workflow',
210
+ triggerType: 'cron',
211
+ cronExpression: '* * * * *', // Every minute
212
+ input: { value: 'scheduled' },
217
213
  enabled: true,
218
214
  });
219
215
 
@@ -227,6 +223,14 @@ describe("CronScheduler", () => {
227
223
  // Run pending timers for poll interval
228
224
  await vi.runOnlyPendingTimersAsync();
229
225
 
226
+ // Stop the scheduler and shut down the engine while fake timers are
227
+ // still active so that any setImmediate/setTimeout handles created
228
+ // under fake timers are cleared with the fake clear* functions.
229
+ // Clearing fake-timer handles with real clearImmediate corrupts
230
+ // Node 20/22's internal timer state and breaks subsequent tests.
231
+ await scheduler.stop();
232
+ await engine.shutdown();
233
+
230
234
  vi.useRealTimers();
231
235
 
232
236
  // Note: With fake timers, the actual workflow may not complete
@@ -234,16 +238,16 @@ describe("CronScheduler", () => {
234
238
  });
235
239
  });
236
240
 
237
- describe("workflow completion triggers", () => {
238
- it("should trigger workflow on completion of another workflow", async () => {
241
+ describe('workflow completion triggers', () => {
242
+ it('should trigger workflow on completion of another workflow', async () => {
239
243
  // Register a trigger workflow
240
244
  engine.registerWorkflow({
241
- kind: "trigger.workflow",
242
- name: "Trigger Workflow",
245
+ kind: 'trigger.workflow',
246
+ name: 'Trigger Workflow',
243
247
  steps: [
244
248
  {
245
- key: "trigger",
246
- name: "Trigger",
249
+ key: 'trigger',
250
+ name: 'Trigger',
247
251
  handler: async () => ({ triggered: true }),
248
252
  },
249
253
  ],
@@ -251,14 +255,14 @@ describe("CronScheduler", () => {
251
255
 
252
256
  // Register a dependent workflow
253
257
  engine.registerWorkflow({
254
- kind: "dependent.workflow",
255
- name: "Dependent Workflow",
258
+ kind: 'dependent.workflow',
259
+ name: 'Dependent Workflow',
256
260
  steps: [
257
261
  {
258
- key: "dependent",
259
- name: "Dependent",
262
+ key: 'dependent',
263
+ name: 'Dependent',
260
264
  handler: async (ctx) => ({
261
- parentRunId: ctx.metadata.triggerRunId,
265
+ parentRunId: ctx.metadata.triggerRunId
262
266
  }),
263
267
  },
264
268
  ],
@@ -266,42 +270,42 @@ describe("CronScheduler", () => {
266
270
 
267
271
  // Add a completion trigger
268
272
  await scheduler.addSchedule({
269
- workflowKind: "dependent.workflow",
270
- triggerType: "workflow_completed",
271
- triggerOnWorkflowKind: "trigger.workflow",
272
- triggerOnStatus: ["succeeded"],
273
+ workflowKind: 'dependent.workflow',
274
+ triggerType: 'workflow_completed',
275
+ triggerOnWorkflowKind: 'trigger.workflow',
276
+ triggerOnStatus: ['succeeded'],
273
277
  enabled: true,
274
278
  });
275
279
 
276
280
  await scheduler.start();
277
281
 
278
282
  // Start the trigger workflow
279
- const triggerRunId = await engine.startRun({ kind: "trigger.workflow" });
283
+ const triggerRunId = await engine.startRun({ kind: 'trigger.workflow' });
280
284
 
281
285
  // Wait for trigger workflow to complete
282
286
  await engine.waitForRun(triggerRunId);
283
287
 
284
288
  // Give time for the completion trigger to fire and dependent workflow to start
285
- await new Promise((resolve) => setTimeout(resolve, 200));
289
+ await new Promise(resolve => setTimeout(resolve, 200));
286
290
 
287
291
  // Check that the dependent workflow was started
288
292
  const storage = engine.getStorage();
289
- const { items } = await storage.listRuns({ kind: "dependent.workflow" });
293
+ const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
290
294
 
291
295
  expect(items.length).toBeGreaterThanOrEqual(1);
292
296
  });
293
297
 
294
- it("should not trigger on non-matching status", async () => {
298
+ it('should not trigger on non-matching status', async () => {
295
299
  // Register a failing trigger workflow
296
300
  engine.registerWorkflow({
297
- kind: "failing.workflow",
298
- name: "Failing Workflow",
301
+ kind: 'failing.workflow',
302
+ name: 'Failing Workflow',
299
303
  steps: [
300
304
  {
301
- key: "fail",
302
- name: "Fail",
305
+ key: 'fail',
306
+ name: 'Fail',
303
307
  handler: async () => {
304
- throw new Error("Intentional failure");
308
+ throw new Error('Intentional failure');
305
309
  },
306
310
  },
307
311
  ],
@@ -309,56 +313,56 @@ describe("CronScheduler", () => {
309
313
 
310
314
  // Add a completion trigger for success only
311
315
  await scheduler.addSchedule({
312
- workflowKind: "test.workflow",
313
- triggerType: "workflow_completed",
314
- triggerOnWorkflowKind: "failing.workflow",
315
- triggerOnStatus: ["succeeded"], // Only on success
316
+ workflowKind: 'test.workflow',
317
+ triggerType: 'workflow_completed',
318
+ triggerOnWorkflowKind: 'failing.workflow',
319
+ triggerOnStatus: ['succeeded'], // Only on success
316
320
  enabled: true,
317
321
  });
318
322
 
319
323
  await scheduler.start();
320
324
 
321
325
  // Start the failing workflow
322
- const runId = await engine.startRun({ kind: "failing.workflow" });
326
+ const runId = await engine.startRun({ kind: 'failing.workflow' });
323
327
 
324
328
  // Wait for it to fail
325
329
  await engine.waitForRun(runId);
326
330
 
327
331
  // Give time for any triggers
328
- await new Promise((resolve) => setTimeout(resolve, 200));
332
+ await new Promise(resolve => setTimeout(resolve, 200));
329
333
 
330
334
  // Check that test.workflow was NOT started
331
335
  const storage = engine.getStorage();
332
- const { items } = await storage.listRuns({ kind: "test.workflow" });
336
+ const { items } = await storage.listRuns({ kind: 'test.workflow' });
333
337
 
334
338
  expect(items).toHaveLength(0);
335
339
  });
336
340
  });
337
341
 
338
- describe("start/stop", () => {
339
- it("should start and stop without error", async () => {
342
+ describe('start/stop', () => {
343
+ it('should start and stop without error', async () => {
340
344
  await scheduler.start();
341
345
  await scheduler.stop();
342
346
  });
343
347
 
344
- it("should warn when starting twice", async () => {
348
+ it('should warn when starting twice', async () => {
345
349
  await scheduler.start();
346
350
  await scheduler.start(); // Should just warn
347
351
  await scheduler.stop();
348
352
  });
349
353
 
350
- it("should handle stopping when not started", async () => {
354
+ it('should handle stopping when not started', async () => {
351
355
  await scheduler.stop(); // Should do nothing
352
356
  });
353
357
  });
354
358
 
355
- describe("persistence", () => {
356
- it("should load schedules from persistence on start", async () => {
359
+ describe('persistence', () => {
360
+ it('should load schedules from persistence on start', async () => {
357
361
  const mockSchedule: WorkflowSchedule = {
358
- id: "persisted-1",
359
- workflowKind: "test.workflow",
360
- triggerType: "cron",
361
- cronExpression: "0 * * * *",
362
+ id: 'persisted-1',
363
+ workflowKind: 'test.workflow',
364
+ triggerType: 'cron',
365
+ cronExpression: '0 * * * *',
362
366
  enabled: true,
363
367
  };
364
368
 
@@ -381,12 +385,12 @@ describe("CronScheduler", () => {
381
385
 
382
386
  const schedules = await schedulerWithPersistence.getSchedules();
383
387
  expect(schedules).toHaveLength(1);
384
- expect(schedules[0].id).toBe("persisted-1");
388
+ expect(schedules[0].id).toBe('persisted-1');
385
389
 
386
390
  await schedulerWithPersistence.stop();
387
391
  });
388
392
 
389
- it("should save schedule to persistence when added", async () => {
393
+ it('should save schedule to persistence when added', async () => {
390
394
  const mockPersistence: SchedulePersistence = {
391
395
  loadSchedules: vi.fn().mockResolvedValue([]),
392
396
  saveSchedule: vi.fn().mockResolvedValue(undefined),
@@ -401,9 +405,9 @@ describe("CronScheduler", () => {
401
405
  });
402
406
 
403
407
  await schedulerWithPersistence.addSchedule({
404
- workflowKind: "test.workflow",
405
- triggerType: "cron",
406
- cronExpression: "0 * * * *",
408
+ workflowKind: 'test.workflow',
409
+ triggerType: 'cron',
410
+ cronExpression: '0 * * * *',
407
411
  enabled: true,
408
412
  });
409
413
 
@@ -411,14 +415,9 @@ describe("CronScheduler", () => {
411
415
  });
412
416
  });
413
417
 
414
- describe("updateNextRunTime parse-error path", () => {
415
- it("should set nextRunAt to undefined and log error for malformed cron expression update", async () => {
416
- const logger = {
417
- debug: vi.fn(),
418
- info: vi.fn(),
419
- warn: vi.fn(),
420
- error: vi.fn(),
421
- };
418
+ describe('updateNextRunTime parse-error path', () => {
419
+ it('should set nextRunAt to undefined and log error for malformed cron expression update', async () => {
420
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
422
421
  const errorScheduler = new CronScheduler({
423
422
  engine,
424
423
  logger,
@@ -427,9 +426,9 @@ describe("CronScheduler", () => {
427
426
 
428
427
  // Add a valid schedule first
429
428
  const schedule = await errorScheduler.addSchedule({
430
- workflowKind: "test.workflow",
431
- triggerType: "cron",
432
- cronExpression: "0 * * * *",
429
+ workflowKind: 'test.workflow',
430
+ triggerType: 'cron',
431
+ cronExpression: '0 * * * *',
433
432
  enabled: true,
434
433
  });
435
434
 
@@ -443,10 +442,10 @@ describe("CronScheduler", () => {
443
442
  const mockPersistence: SchedulePersistence = {
444
443
  loadSchedules: vi.fn().mockResolvedValue([
445
444
  {
446
- id: "bad-cron",
447
- workflowKind: "test.workflow",
448
- triggerType: "cron" as const,
449
- cronExpression: "99 99 99 99 99",
445
+ id: 'bad-cron',
446
+ workflowKind: 'test.workflow',
447
+ triggerType: 'cron' as const,
448
+ cronExpression: '99 99 99 99 99',
450
449
  enabled: true,
451
450
  },
452
451
  ]),
@@ -465,12 +464,12 @@ describe("CronScheduler", () => {
465
464
  await badScheduler.start();
466
465
 
467
466
  // The schedule should have been loaded with nextRunAt = undefined due to parse error
468
- const loaded = badScheduler.getSchedule("bad-cron");
467
+ const loaded = badScheduler.getSchedule('bad-cron');
469
468
  expect(loaded).toBeDefined();
470
469
  expect(loaded!.nextRunAt).toBeUndefined();
471
470
  expect(logger.error).toHaveBeenCalledWith(
472
- expect.stringContaining("Failed to parse cron expression"),
473
- expect.anything(),
471
+ expect.stringContaining('Invalid cron expression'),
472
+ expect.anything()
474
473
  );
475
474
 
476
475
  await badScheduler.stop();