@multiplier-labs/stepflow 0.3.1 → 0.3.3
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.
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +11 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
- package/dist/chunk-4VOKMGP6.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
- package/dist/chunk-GYPNJBDP.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
- package/dist/chunk-KHTUWLVZ.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
- package/dist/chunk-ML35PIHX.js.map +1 -0
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
- package/dist/index.d.ts +14 -12
- package/dist/index.js +40 -82
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +13 -13
- package/dist/storage/index.js +2 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +2 -3
- package/package.json +7 -6
- package/src/core/engine.test.ts +466 -522
- package/src/core/engine.ts +42 -67
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator.ts +64 -83
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +197 -235
- package/src/events/webhook.ts +44 -71
- package/src/index.ts +19 -19
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +66 -91
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +39 -43
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +141 -150
- package/src/scheduler/cron.ts +32 -67
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +71 -99
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +12 -19
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres.ts +197 -393
- package/src/storage/sqlite.test.ts +154 -281
- package/src/storage/sqlite.ts +50 -85
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +4 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/postgres-deps.ts +4 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
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:
|
|
28
|
-
name:
|
|
23
|
+
kind: 'test.workflow',
|
|
24
|
+
name: 'Test Workflow',
|
|
29
25
|
steps: [
|
|
30
26
|
{
|
|
31
|
-
key:
|
|
32
|
-
name:
|
|
33
|
-
handler: async (ctx) => ({ value: ctx.input.value ??
|
|
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(
|
|
52
|
-
it(
|
|
47
|
+
describe('addSchedule', () => {
|
|
48
|
+
it('should add a cron schedule', async () => {
|
|
53
49
|
const schedule = await scheduler.addSchedule({
|
|
54
|
-
workflowKind:
|
|
55
|
-
triggerType:
|
|
56
|
-
cronExpression:
|
|
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(
|
|
62
|
-
expect(schedule.triggerType).toBe(
|
|
63
|
-
expect(schedule.cronExpression).toBe(
|
|
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(
|
|
63
|
+
it('should add a workflow completion trigger', async () => {
|
|
68
64
|
const schedule = await scheduler.addSchedule({
|
|
69
|
-
workflowKind:
|
|
70
|
-
triggerType:
|
|
71
|
-
triggerOnWorkflowKind:
|
|
72
|
-
triggerOnStatus: [
|
|
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(
|
|
78
|
-
expect(schedule.triggerOnWorkflowKind).toBe(
|
|
73
|
+
expect(schedule.triggerType).toBe('workflow_completed');
|
|
74
|
+
expect(schedule.triggerOnWorkflowKind).toBe('order.process');
|
|
79
75
|
});
|
|
80
76
|
|
|
81
|
-
it(
|
|
77
|
+
it('should reject invalid cron expression', async () => {
|
|
82
78
|
await expect(
|
|
83
79
|
scheduler.addSchedule({
|
|
84
|
-
workflowKind:
|
|
85
|
-
triggerType:
|
|
86
|
-
cronExpression:
|
|
80
|
+
workflowKind: 'test.workflow',
|
|
81
|
+
triggerType: 'cron',
|
|
82
|
+
cronExpression: 'invalid cron',
|
|
87
83
|
enabled: true,
|
|
88
|
-
})
|
|
89
|
-
).rejects.toThrow(
|
|
84
|
+
})
|
|
85
|
+
).rejects.toThrow('Invalid cron expression');
|
|
90
86
|
});
|
|
91
87
|
|
|
92
|
-
it(
|
|
88
|
+
it('should require triggerOnWorkflowKind for workflow_completed triggers', async () => {
|
|
93
89
|
await expect(
|
|
94
90
|
scheduler.addSchedule({
|
|
95
|
-
workflowKind:
|
|
96
|
-
triggerType:
|
|
91
|
+
workflowKind: 'test.workflow',
|
|
92
|
+
triggerType: 'workflow_completed',
|
|
97
93
|
enabled: true,
|
|
98
|
-
})
|
|
99
|
-
).rejects.toThrow(
|
|
94
|
+
})
|
|
95
|
+
).rejects.toThrow('triggerOnWorkflowKind is required');
|
|
100
96
|
});
|
|
101
97
|
});
|
|
102
98
|
|
|
103
|
-
describe(
|
|
104
|
-
it(
|
|
99
|
+
describe('removeSchedule', () => {
|
|
100
|
+
it('should remove a schedule', async () => {
|
|
105
101
|
const schedule = await scheduler.addSchedule({
|
|
106
|
-
workflowKind:
|
|
107
|
-
triggerType:
|
|
108
|
-
cronExpression:
|
|
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(
|
|
119
|
-
await expect(scheduler.removeSchedule(
|
|
120
|
-
|
|
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(
|
|
126
|
-
it(
|
|
121
|
+
describe('updateSchedule', () => {
|
|
122
|
+
it('should update schedule properties', async () => {
|
|
127
123
|
const schedule = await scheduler.addSchedule({
|
|
128
|
-
workflowKind:
|
|
129
|
-
triggerType:
|
|
130
|
-
cronExpression:
|
|
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(
|
|
136
|
+
it('should update cron expression and recalculate next run', async () => {
|
|
141
137
|
const schedule = await scheduler.addSchedule({
|
|
142
|
-
workflowKind:
|
|
143
|
-
triggerType:
|
|
144
|
-
cronExpression:
|
|
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:
|
|
147
|
+
cronExpression: '30 * * * *',
|
|
152
148
|
});
|
|
153
149
|
|
|
154
150
|
const updated = scheduler.getSchedule(schedule.id);
|
|
155
|
-
expect(updated?.cronExpression).toBe(
|
|
151
|
+
expect(updated?.cronExpression).toBe('30 * * * *');
|
|
156
152
|
expect(updated?.nextRunAt).not.toEqual(originalNextRun);
|
|
157
153
|
});
|
|
158
154
|
});
|
|
159
155
|
|
|
160
|
-
describe(
|
|
161
|
-
it(
|
|
156
|
+
describe('getSchedules', () => {
|
|
157
|
+
it('should return all schedules', async () => {
|
|
162
158
|
await scheduler.addSchedule({
|
|
163
|
-
workflowKind:
|
|
164
|
-
triggerType:
|
|
165
|
-
cronExpression:
|
|
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:
|
|
171
|
-
triggerType:
|
|
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(
|
|
181
|
-
it(
|
|
176
|
+
describe('triggerNow', () => {
|
|
177
|
+
it('should manually trigger a schedule', async () => {
|
|
182
178
|
const schedule = await scheduler.addSchedule({
|
|
183
|
-
workflowKind:
|
|
184
|
-
triggerType:
|
|
185
|
-
cronExpression:
|
|
186
|
-
input: { value:
|
|
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(
|
|
192
|
+
expect(run.status).toBe('succeeded');
|
|
197
193
|
});
|
|
198
194
|
|
|
199
|
-
it(
|
|
200
|
-
await expect(scheduler.triggerNow(
|
|
201
|
-
|
|
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(
|
|
207
|
-
it(
|
|
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:
|
|
214
|
-
triggerType:
|
|
215
|
-
cronExpression:
|
|
216
|
-
input: { value:
|
|
209
|
+
workflowKind: 'test.workflow',
|
|
210
|
+
triggerType: 'cron',
|
|
211
|
+
cronExpression: '* * * * *', // Every minute
|
|
212
|
+
input: { value: 'scheduled' },
|
|
217
213
|
enabled: true,
|
|
218
214
|
});
|
|
219
215
|
|
|
@@ -234,16 +230,16 @@ describe("CronScheduler", () => {
|
|
|
234
230
|
});
|
|
235
231
|
});
|
|
236
232
|
|
|
237
|
-
describe(
|
|
238
|
-
it(
|
|
233
|
+
describe('workflow completion triggers', () => {
|
|
234
|
+
it('should trigger workflow on completion of another workflow', async () => {
|
|
239
235
|
// Register a trigger workflow
|
|
240
236
|
engine.registerWorkflow({
|
|
241
|
-
kind:
|
|
242
|
-
name:
|
|
237
|
+
kind: 'trigger.workflow',
|
|
238
|
+
name: 'Trigger Workflow',
|
|
243
239
|
steps: [
|
|
244
240
|
{
|
|
245
|
-
key:
|
|
246
|
-
name:
|
|
241
|
+
key: 'trigger',
|
|
242
|
+
name: 'Trigger',
|
|
247
243
|
handler: async () => ({ triggered: true }),
|
|
248
244
|
},
|
|
249
245
|
],
|
|
@@ -251,14 +247,14 @@ describe("CronScheduler", () => {
|
|
|
251
247
|
|
|
252
248
|
// Register a dependent workflow
|
|
253
249
|
engine.registerWorkflow({
|
|
254
|
-
kind:
|
|
255
|
-
name:
|
|
250
|
+
kind: 'dependent.workflow',
|
|
251
|
+
name: 'Dependent Workflow',
|
|
256
252
|
steps: [
|
|
257
253
|
{
|
|
258
|
-
key:
|
|
259
|
-
name:
|
|
254
|
+
key: 'dependent',
|
|
255
|
+
name: 'Dependent',
|
|
260
256
|
handler: async (ctx) => ({
|
|
261
|
-
parentRunId: ctx.metadata.triggerRunId
|
|
257
|
+
parentRunId: ctx.metadata.triggerRunId
|
|
262
258
|
}),
|
|
263
259
|
},
|
|
264
260
|
],
|
|
@@ -266,42 +262,42 @@ describe("CronScheduler", () => {
|
|
|
266
262
|
|
|
267
263
|
// Add a completion trigger
|
|
268
264
|
await scheduler.addSchedule({
|
|
269
|
-
workflowKind:
|
|
270
|
-
triggerType:
|
|
271
|
-
triggerOnWorkflowKind:
|
|
272
|
-
triggerOnStatus: [
|
|
265
|
+
workflowKind: 'dependent.workflow',
|
|
266
|
+
triggerType: 'workflow_completed',
|
|
267
|
+
triggerOnWorkflowKind: 'trigger.workflow',
|
|
268
|
+
triggerOnStatus: ['succeeded'],
|
|
273
269
|
enabled: true,
|
|
274
270
|
});
|
|
275
271
|
|
|
276
272
|
await scheduler.start();
|
|
277
273
|
|
|
278
274
|
// Start the trigger workflow
|
|
279
|
-
const triggerRunId = await engine.startRun({ kind:
|
|
275
|
+
const triggerRunId = await engine.startRun({ kind: 'trigger.workflow' });
|
|
280
276
|
|
|
281
277
|
// Wait for trigger workflow to complete
|
|
282
278
|
await engine.waitForRun(triggerRunId);
|
|
283
279
|
|
|
284
280
|
// Give time for the completion trigger to fire and dependent workflow to start
|
|
285
|
-
await new Promise(
|
|
281
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
286
282
|
|
|
287
283
|
// Check that the dependent workflow was started
|
|
288
284
|
const storage = engine.getStorage();
|
|
289
|
-
const { items } = await storage.listRuns({ kind:
|
|
285
|
+
const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
|
|
290
286
|
|
|
291
287
|
expect(items.length).toBeGreaterThanOrEqual(1);
|
|
292
288
|
});
|
|
293
289
|
|
|
294
|
-
it(
|
|
290
|
+
it('should not trigger on non-matching status', async () => {
|
|
295
291
|
// Register a failing trigger workflow
|
|
296
292
|
engine.registerWorkflow({
|
|
297
|
-
kind:
|
|
298
|
-
name:
|
|
293
|
+
kind: 'failing.workflow',
|
|
294
|
+
name: 'Failing Workflow',
|
|
299
295
|
steps: [
|
|
300
296
|
{
|
|
301
|
-
key:
|
|
302
|
-
name:
|
|
297
|
+
key: 'fail',
|
|
298
|
+
name: 'Fail',
|
|
303
299
|
handler: async () => {
|
|
304
|
-
throw new Error(
|
|
300
|
+
throw new Error('Intentional failure');
|
|
305
301
|
},
|
|
306
302
|
},
|
|
307
303
|
],
|
|
@@ -309,56 +305,56 @@ describe("CronScheduler", () => {
|
|
|
309
305
|
|
|
310
306
|
// Add a completion trigger for success only
|
|
311
307
|
await scheduler.addSchedule({
|
|
312
|
-
workflowKind:
|
|
313
|
-
triggerType:
|
|
314
|
-
triggerOnWorkflowKind:
|
|
315
|
-
triggerOnStatus: [
|
|
308
|
+
workflowKind: 'test.workflow',
|
|
309
|
+
triggerType: 'workflow_completed',
|
|
310
|
+
triggerOnWorkflowKind: 'failing.workflow',
|
|
311
|
+
triggerOnStatus: ['succeeded'], // Only on success
|
|
316
312
|
enabled: true,
|
|
317
313
|
});
|
|
318
314
|
|
|
319
315
|
await scheduler.start();
|
|
320
316
|
|
|
321
317
|
// Start the failing workflow
|
|
322
|
-
const runId = await engine.startRun({ kind:
|
|
318
|
+
const runId = await engine.startRun({ kind: 'failing.workflow' });
|
|
323
319
|
|
|
324
320
|
// Wait for it to fail
|
|
325
321
|
await engine.waitForRun(runId);
|
|
326
322
|
|
|
327
323
|
// Give time for any triggers
|
|
328
|
-
await new Promise(
|
|
324
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
329
325
|
|
|
330
326
|
// Check that test.workflow was NOT started
|
|
331
327
|
const storage = engine.getStorage();
|
|
332
|
-
const { items } = await storage.listRuns({ kind:
|
|
328
|
+
const { items } = await storage.listRuns({ kind: 'test.workflow' });
|
|
333
329
|
|
|
334
330
|
expect(items).toHaveLength(0);
|
|
335
331
|
});
|
|
336
332
|
});
|
|
337
333
|
|
|
338
|
-
describe(
|
|
339
|
-
it(
|
|
334
|
+
describe('start/stop', () => {
|
|
335
|
+
it('should start and stop without error', async () => {
|
|
340
336
|
await scheduler.start();
|
|
341
337
|
await scheduler.stop();
|
|
342
338
|
});
|
|
343
339
|
|
|
344
|
-
it(
|
|
340
|
+
it('should warn when starting twice', async () => {
|
|
345
341
|
await scheduler.start();
|
|
346
342
|
await scheduler.start(); // Should just warn
|
|
347
343
|
await scheduler.stop();
|
|
348
344
|
});
|
|
349
345
|
|
|
350
|
-
it(
|
|
346
|
+
it('should handle stopping when not started', async () => {
|
|
351
347
|
await scheduler.stop(); // Should do nothing
|
|
352
348
|
});
|
|
353
349
|
});
|
|
354
350
|
|
|
355
|
-
describe(
|
|
356
|
-
it(
|
|
351
|
+
describe('persistence', () => {
|
|
352
|
+
it('should load schedules from persistence on start', async () => {
|
|
357
353
|
const mockSchedule: WorkflowSchedule = {
|
|
358
|
-
id:
|
|
359
|
-
workflowKind:
|
|
360
|
-
triggerType:
|
|
361
|
-
cronExpression:
|
|
354
|
+
id: 'persisted-1',
|
|
355
|
+
workflowKind: 'test.workflow',
|
|
356
|
+
triggerType: 'cron',
|
|
357
|
+
cronExpression: '0 * * * *',
|
|
362
358
|
enabled: true,
|
|
363
359
|
};
|
|
364
360
|
|
|
@@ -381,12 +377,12 @@ describe("CronScheduler", () => {
|
|
|
381
377
|
|
|
382
378
|
const schedules = await schedulerWithPersistence.getSchedules();
|
|
383
379
|
expect(schedules).toHaveLength(1);
|
|
384
|
-
expect(schedules[0].id).toBe(
|
|
380
|
+
expect(schedules[0].id).toBe('persisted-1');
|
|
385
381
|
|
|
386
382
|
await schedulerWithPersistence.stop();
|
|
387
383
|
});
|
|
388
384
|
|
|
389
|
-
it(
|
|
385
|
+
it('should save schedule to persistence when added', async () => {
|
|
390
386
|
const mockPersistence: SchedulePersistence = {
|
|
391
387
|
loadSchedules: vi.fn().mockResolvedValue([]),
|
|
392
388
|
saveSchedule: vi.fn().mockResolvedValue(undefined),
|
|
@@ -401,9 +397,9 @@ describe("CronScheduler", () => {
|
|
|
401
397
|
});
|
|
402
398
|
|
|
403
399
|
await schedulerWithPersistence.addSchedule({
|
|
404
|
-
workflowKind:
|
|
405
|
-
triggerType:
|
|
406
|
-
cronExpression:
|
|
400
|
+
workflowKind: 'test.workflow',
|
|
401
|
+
triggerType: 'cron',
|
|
402
|
+
cronExpression: '0 * * * *',
|
|
407
403
|
enabled: true,
|
|
408
404
|
});
|
|
409
405
|
|
|
@@ -411,14 +407,9 @@ describe("CronScheduler", () => {
|
|
|
411
407
|
});
|
|
412
408
|
});
|
|
413
409
|
|
|
414
|
-
describe(
|
|
415
|
-
it(
|
|
416
|
-
const logger = {
|
|
417
|
-
debug: vi.fn(),
|
|
418
|
-
info: vi.fn(),
|
|
419
|
-
warn: vi.fn(),
|
|
420
|
-
error: vi.fn(),
|
|
421
|
-
};
|
|
410
|
+
describe('updateNextRunTime parse-error path', () => {
|
|
411
|
+
it('should set nextRunAt to undefined and log error for malformed cron expression update', async () => {
|
|
412
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
422
413
|
const errorScheduler = new CronScheduler({
|
|
423
414
|
engine,
|
|
424
415
|
logger,
|
|
@@ -427,9 +418,9 @@ describe("CronScheduler", () => {
|
|
|
427
418
|
|
|
428
419
|
// Add a valid schedule first
|
|
429
420
|
const schedule = await errorScheduler.addSchedule({
|
|
430
|
-
workflowKind:
|
|
431
|
-
triggerType:
|
|
432
|
-
cronExpression:
|
|
421
|
+
workflowKind: 'test.workflow',
|
|
422
|
+
triggerType: 'cron',
|
|
423
|
+
cronExpression: '0 * * * *',
|
|
433
424
|
enabled: true,
|
|
434
425
|
});
|
|
435
426
|
|
|
@@ -443,10 +434,10 @@ describe("CronScheduler", () => {
|
|
|
443
434
|
const mockPersistence: SchedulePersistence = {
|
|
444
435
|
loadSchedules: vi.fn().mockResolvedValue([
|
|
445
436
|
{
|
|
446
|
-
id:
|
|
447
|
-
workflowKind:
|
|
448
|
-
triggerType:
|
|
449
|
-
cronExpression:
|
|
437
|
+
id: 'bad-cron',
|
|
438
|
+
workflowKind: 'test.workflow',
|
|
439
|
+
triggerType: 'cron' as const,
|
|
440
|
+
cronExpression: '99 99 99 99 99',
|
|
450
441
|
enabled: true,
|
|
451
442
|
},
|
|
452
443
|
]),
|
|
@@ -465,12 +456,12 @@ describe("CronScheduler", () => {
|
|
|
465
456
|
await badScheduler.start();
|
|
466
457
|
|
|
467
458
|
// The schedule should have been loaded with nextRunAt = undefined due to parse error
|
|
468
|
-
const loaded = badScheduler.getSchedule(
|
|
459
|
+
const loaded = badScheduler.getSchedule('bad-cron');
|
|
469
460
|
expect(loaded).toBeDefined();
|
|
470
461
|
expect(loaded!.nextRunAt).toBeUndefined();
|
|
471
462
|
expect(logger.error).toHaveBeenCalledWith(
|
|
472
|
-
expect.stringContaining(
|
|
473
|
-
expect.anything()
|
|
463
|
+
expect.stringContaining('Failed to parse cron expression'),
|
|
464
|
+
expect.anything()
|
|
474
465
|
);
|
|
475
466
|
|
|
476
467
|
await badScheduler.stop();
|