@multiplier-labs/stepflow 0.3.3 → 0.3.6
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/README.md +9 -0
- package/dist/{chunk-GYPNJBDP.js → chunk-D5HW7VEG.js} +269 -48
- package/dist/chunk-D5HW7VEG.js.map +1 -0
- package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-4VOKMGP6.js → chunk-JUPZEBR3.js} +519 -522
- package/dist/chunk-JUPZEBR3.js.map +1 -0
- package/dist/events/index.d.ts +58 -3
- package/dist/events/index.js +1 -1
- package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
- package/dist/index.d.ts +2 -12
- package/dist/index.js +764 -485
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +313 -47
- package/dist/storage/index.js +4 -2
- package/package.json +27 -13
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
- package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
- package/.github/CODEOWNERS +0 -15
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/security.yml +0 -29
- package/CHANGELOG.md +0 -34
- package/SECURITY.md +0 -40
- package/dist/chunk-4VOKMGP6.js.map +0 -1
- package/dist/chunk-GYPNJBDP.js.map +0 -1
- package/dist/chunk-KHTUWLVZ.js.map +0 -1
- package/dist/chunk-ML35PIHX.js.map +0 -1
- package/docs/github-packages-setup.md +0 -97
- package/docs/stepflow-api-reference.md +0 -1661
- package/src/core/engine.test.ts +0 -1365
- package/src/core/engine.ts +0 -692
- package/src/core/index.ts +0 -7
- package/src/core/orchestrator.ts +0 -571
- package/src/core/types.ts +0 -220
- package/src/events/index.ts +0 -8
- package/src/events/memory.test.ts +0 -107
- package/src/events/memory.ts +0 -92
- package/src/events/socketio.test.ts +0 -378
- package/src/events/socketio.ts +0 -262
- package/src/events/types.ts +0 -118
- package/src/events/webhook.test.ts +0 -657
- package/src/events/webhook.ts +0 -564
- package/src/index.ts +0 -270
- package/src/planning/index.ts +0 -55
- package/src/planning/planner.ts +0 -576
- package/src/planning/planning.test.ts +0 -727
- package/src/planning/registry.test.ts +0 -398
- package/src/planning/registry.ts +0 -365
- package/src/planning/types.ts +0 -566
- package/src/scheduler/cron.test.ts +0 -471
- package/src/scheduler/cron.ts +0 -414
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/postgres-persistence.ts +0 -518
- package/src/scheduler/sqlite-persistence.test.ts +0 -235
- package/src/scheduler/sqlite-persistence.ts +0 -261
- package/src/scheduler/types.ts +0 -73
- package/src/storage/index.ts +0 -27
- package/src/storage/memory.test.ts +0 -349
- package/src/storage/memory.ts +0 -227
- package/src/storage/postgres.ts +0 -1354
- package/src/storage/sqlite.test.ts +0 -466
- package/src/storage/sqlite.ts +0 -627
- package/src/storage/types.ts +0 -343
- package/src/utils/errors.test.ts +0 -150
- package/src/utils/errors.ts +0 -161
- package/src/utils/id.ts +0 -21
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.test.ts +0 -121
- package/src/utils/logger.ts +0 -96
- package/src/utils/postgres-deps.ts +0 -63
- package/src/utils/retry.test.ts +0 -177
- package/src/utils/retry.ts +0 -121
- package/tsconfig.json +0 -21
- package/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -9
|
@@ -1,471 +0,0 @@
|
|
|
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', () => {
|
|
10
|
-
let engine: WorkflowEngine;
|
|
11
|
-
let scheduler: CronScheduler;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
// Create a workflow engine with silent logger
|
|
15
|
-
engine = new WorkflowEngine({
|
|
16
|
-
storage: new MemoryStorageAdapter(),
|
|
17
|
-
events: new MemoryEventTransport(),
|
|
18
|
-
logger: new SilentLogger(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Register a test workflow
|
|
22
|
-
engine.registerWorkflow({
|
|
23
|
-
kind: 'test.workflow',
|
|
24
|
-
name: 'Test Workflow',
|
|
25
|
-
steps: [
|
|
26
|
-
{
|
|
27
|
-
key: 'step1',
|
|
28
|
-
name: 'Step 1',
|
|
29
|
-
handler: async (ctx) => ({ value: ctx.input.value ?? 'default' }),
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Create scheduler
|
|
35
|
-
scheduler = new CronScheduler({
|
|
36
|
-
engine,
|
|
37
|
-
logger: new SilentLogger(),
|
|
38
|
-
pollInterval: 100, // Fast polling for tests
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(async () => {
|
|
43
|
-
await scheduler.stop();
|
|
44
|
-
await engine.shutdown();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('addSchedule', () => {
|
|
48
|
-
it('should add a cron schedule', async () => {
|
|
49
|
-
const schedule = await scheduler.addSchedule({
|
|
50
|
-
workflowKind: 'test.workflow',
|
|
51
|
-
triggerType: 'cron',
|
|
52
|
-
cronExpression: '0 * * * *', // Every hour
|
|
53
|
-
enabled: true,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(schedule.id).toBeDefined();
|
|
57
|
-
expect(schedule.workflowKind).toBe('test.workflow');
|
|
58
|
-
expect(schedule.triggerType).toBe('cron');
|
|
59
|
-
expect(schedule.cronExpression).toBe('0 * * * *');
|
|
60
|
-
expect(schedule.nextRunAt).toBeInstanceOf(Date);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should add a workflow completion trigger', async () => {
|
|
64
|
-
const schedule = await scheduler.addSchedule({
|
|
65
|
-
workflowKind: 'notification.send',
|
|
66
|
-
triggerType: 'workflow_completed',
|
|
67
|
-
triggerOnWorkflowKind: 'order.process',
|
|
68
|
-
triggerOnStatus: ['succeeded'],
|
|
69
|
-
enabled: true,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
expect(schedule.id).toBeDefined();
|
|
73
|
-
expect(schedule.triggerType).toBe('workflow_completed');
|
|
74
|
-
expect(schedule.triggerOnWorkflowKind).toBe('order.process');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should reject invalid cron expression', async () => {
|
|
78
|
-
await expect(
|
|
79
|
-
scheduler.addSchedule({
|
|
80
|
-
workflowKind: 'test.workflow',
|
|
81
|
-
triggerType: 'cron',
|
|
82
|
-
cronExpression: 'invalid cron',
|
|
83
|
-
enabled: true,
|
|
84
|
-
})
|
|
85
|
-
).rejects.toThrow('Invalid cron expression');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should require triggerOnWorkflowKind for workflow_completed triggers', async () => {
|
|
89
|
-
await expect(
|
|
90
|
-
scheduler.addSchedule({
|
|
91
|
-
workflowKind: 'test.workflow',
|
|
92
|
-
triggerType: 'workflow_completed',
|
|
93
|
-
enabled: true,
|
|
94
|
-
})
|
|
95
|
-
).rejects.toThrow('triggerOnWorkflowKind is required');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('removeSchedule', () => {
|
|
100
|
-
it('should remove a schedule', async () => {
|
|
101
|
-
const schedule = await scheduler.addSchedule({
|
|
102
|
-
workflowKind: 'test.workflow',
|
|
103
|
-
triggerType: 'cron',
|
|
104
|
-
cronExpression: '0 * * * *',
|
|
105
|
-
enabled: true,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await scheduler.removeSchedule(schedule.id);
|
|
109
|
-
|
|
110
|
-
const schedules = await scheduler.getSchedules();
|
|
111
|
-
expect(schedules).toHaveLength(0);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should throw for non-existent schedule', async () => {
|
|
115
|
-
await expect(scheduler.removeSchedule('nonexistent')).rejects.toThrow(
|
|
116
|
-
'Schedule not found'
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('updateSchedule', () => {
|
|
122
|
-
it('should update schedule properties', async () => {
|
|
123
|
-
const schedule = await scheduler.addSchedule({
|
|
124
|
-
workflowKind: 'test.workflow',
|
|
125
|
-
triggerType: 'cron',
|
|
126
|
-
cronExpression: '0 * * * *',
|
|
127
|
-
enabled: true,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
await scheduler.updateSchedule(schedule.id, { enabled: false });
|
|
131
|
-
|
|
132
|
-
const updated = scheduler.getSchedule(schedule.id);
|
|
133
|
-
expect(updated?.enabled).toBe(false);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should update cron expression and recalculate next run', async () => {
|
|
137
|
-
const schedule = await scheduler.addSchedule({
|
|
138
|
-
workflowKind: 'test.workflow',
|
|
139
|
-
triggerType: 'cron',
|
|
140
|
-
cronExpression: '0 * * * *',
|
|
141
|
-
enabled: true,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const originalNextRun = schedule.nextRunAt;
|
|
145
|
-
|
|
146
|
-
await scheduler.updateSchedule(schedule.id, {
|
|
147
|
-
cronExpression: '30 * * * *',
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const updated = scheduler.getSchedule(schedule.id);
|
|
151
|
-
expect(updated?.cronExpression).toBe('30 * * * *');
|
|
152
|
-
expect(updated?.nextRunAt).not.toEqual(originalNextRun);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('getSchedules', () => {
|
|
157
|
-
it('should return all schedules', async () => {
|
|
158
|
-
await scheduler.addSchedule({
|
|
159
|
-
workflowKind: 'test.workflow',
|
|
160
|
-
triggerType: 'cron',
|
|
161
|
-
cronExpression: '0 * * * *',
|
|
162
|
-
enabled: true,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
await scheduler.addSchedule({
|
|
166
|
-
workflowKind: 'test.workflow',
|
|
167
|
-
triggerType: 'manual',
|
|
168
|
-
enabled: true,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const schedules = await scheduler.getSchedules();
|
|
172
|
-
expect(schedules).toHaveLength(2);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('triggerNow', () => {
|
|
177
|
-
it('should manually trigger a schedule', async () => {
|
|
178
|
-
const schedule = await scheduler.addSchedule({
|
|
179
|
-
workflowKind: 'test.workflow',
|
|
180
|
-
triggerType: 'cron',
|
|
181
|
-
cronExpression: '0 0 1 1 *', // Once a year (won't trigger naturally)
|
|
182
|
-
input: { value: 'manual' },
|
|
183
|
-
enabled: true,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const runId = await scheduler.triggerNow(schedule.id);
|
|
187
|
-
|
|
188
|
-
expect(runId).toBeDefined();
|
|
189
|
-
|
|
190
|
-
// Wait for workflow to complete
|
|
191
|
-
const run = await engine.waitForRun(runId);
|
|
192
|
-
expect(run.status).toBe('succeeded');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('should throw for non-existent schedule', async () => {
|
|
196
|
-
await expect(scheduler.triggerNow('nonexistent')).rejects.toThrow(
|
|
197
|
-
'Schedule not found'
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe('cron execution', () => {
|
|
203
|
-
it('should execute scheduled workflow when cron time is reached', async () => {
|
|
204
|
-
// Use fake timers
|
|
205
|
-
vi.useFakeTimers();
|
|
206
|
-
|
|
207
|
-
// Create a schedule that runs every minute
|
|
208
|
-
await scheduler.addSchedule({
|
|
209
|
-
workflowKind: 'test.workflow',
|
|
210
|
-
triggerType: 'cron',
|
|
211
|
-
cronExpression: '* * * * *', // Every minute
|
|
212
|
-
input: { value: 'scheduled' },
|
|
213
|
-
enabled: true,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
await scheduler.start();
|
|
217
|
-
|
|
218
|
-
// Advance time past the next minute boundary
|
|
219
|
-
const now = new Date();
|
|
220
|
-
const msUntilNextMinute = (60 - now.getSeconds()) * 1000 + 1000;
|
|
221
|
-
vi.advanceTimersByTime(msUntilNextMinute);
|
|
222
|
-
|
|
223
|
-
// Run pending timers for poll interval
|
|
224
|
-
await vi.runOnlyPendingTimersAsync();
|
|
225
|
-
|
|
226
|
-
vi.useRealTimers();
|
|
227
|
-
|
|
228
|
-
// Note: With fake timers, the actual workflow may not complete
|
|
229
|
-
// This test verifies the scheduler logic, not full execution
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe('workflow completion triggers', () => {
|
|
234
|
-
it('should trigger workflow on completion of another workflow', async () => {
|
|
235
|
-
// Register a trigger workflow
|
|
236
|
-
engine.registerWorkflow({
|
|
237
|
-
kind: 'trigger.workflow',
|
|
238
|
-
name: 'Trigger Workflow',
|
|
239
|
-
steps: [
|
|
240
|
-
{
|
|
241
|
-
key: 'trigger',
|
|
242
|
-
name: 'Trigger',
|
|
243
|
-
handler: async () => ({ triggered: true }),
|
|
244
|
-
},
|
|
245
|
-
],
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Register a dependent workflow
|
|
249
|
-
engine.registerWorkflow({
|
|
250
|
-
kind: 'dependent.workflow',
|
|
251
|
-
name: 'Dependent Workflow',
|
|
252
|
-
steps: [
|
|
253
|
-
{
|
|
254
|
-
key: 'dependent',
|
|
255
|
-
name: 'Dependent',
|
|
256
|
-
handler: async (ctx) => ({
|
|
257
|
-
parentRunId: ctx.metadata.triggerRunId
|
|
258
|
-
}),
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Add a completion trigger
|
|
264
|
-
await scheduler.addSchedule({
|
|
265
|
-
workflowKind: 'dependent.workflow',
|
|
266
|
-
triggerType: 'workflow_completed',
|
|
267
|
-
triggerOnWorkflowKind: 'trigger.workflow',
|
|
268
|
-
triggerOnStatus: ['succeeded'],
|
|
269
|
-
enabled: true,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await scheduler.start();
|
|
273
|
-
|
|
274
|
-
// Start the trigger workflow
|
|
275
|
-
const triggerRunId = await engine.startRun({ kind: 'trigger.workflow' });
|
|
276
|
-
|
|
277
|
-
// Wait for trigger workflow to complete
|
|
278
|
-
await engine.waitForRun(triggerRunId);
|
|
279
|
-
|
|
280
|
-
// Give time for the completion trigger to fire and dependent workflow to start
|
|
281
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
282
|
-
|
|
283
|
-
// Check that the dependent workflow was started
|
|
284
|
-
const storage = engine.getStorage();
|
|
285
|
-
const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
|
|
286
|
-
|
|
287
|
-
expect(items.length).toBeGreaterThanOrEqual(1);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should not trigger on non-matching status', async () => {
|
|
291
|
-
// Register a failing trigger workflow
|
|
292
|
-
engine.registerWorkflow({
|
|
293
|
-
kind: 'failing.workflow',
|
|
294
|
-
name: 'Failing Workflow',
|
|
295
|
-
steps: [
|
|
296
|
-
{
|
|
297
|
-
key: 'fail',
|
|
298
|
-
name: 'Fail',
|
|
299
|
-
handler: async () => {
|
|
300
|
-
throw new Error('Intentional failure');
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// Add a completion trigger for success only
|
|
307
|
-
await scheduler.addSchedule({
|
|
308
|
-
workflowKind: 'test.workflow',
|
|
309
|
-
triggerType: 'workflow_completed',
|
|
310
|
-
triggerOnWorkflowKind: 'failing.workflow',
|
|
311
|
-
triggerOnStatus: ['succeeded'], // Only on success
|
|
312
|
-
enabled: true,
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
await scheduler.start();
|
|
316
|
-
|
|
317
|
-
// Start the failing workflow
|
|
318
|
-
const runId = await engine.startRun({ kind: 'failing.workflow' });
|
|
319
|
-
|
|
320
|
-
// Wait for it to fail
|
|
321
|
-
await engine.waitForRun(runId);
|
|
322
|
-
|
|
323
|
-
// Give time for any triggers
|
|
324
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
325
|
-
|
|
326
|
-
// Check that test.workflow was NOT started
|
|
327
|
-
const storage = engine.getStorage();
|
|
328
|
-
const { items } = await storage.listRuns({ kind: 'test.workflow' });
|
|
329
|
-
|
|
330
|
-
expect(items).toHaveLength(0);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
describe('start/stop', () => {
|
|
335
|
-
it('should start and stop without error', async () => {
|
|
336
|
-
await scheduler.start();
|
|
337
|
-
await scheduler.stop();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('should warn when starting twice', async () => {
|
|
341
|
-
await scheduler.start();
|
|
342
|
-
await scheduler.start(); // Should just warn
|
|
343
|
-
await scheduler.stop();
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should handle stopping when not started', async () => {
|
|
347
|
-
await scheduler.stop(); // Should do nothing
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
describe('persistence', () => {
|
|
352
|
-
it('should load schedules from persistence on start', async () => {
|
|
353
|
-
const mockSchedule: WorkflowSchedule = {
|
|
354
|
-
id: 'persisted-1',
|
|
355
|
-
workflowKind: 'test.workflow',
|
|
356
|
-
triggerType: 'cron',
|
|
357
|
-
cronExpression: '0 * * * *',
|
|
358
|
-
enabled: true,
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const mockPersistence: SchedulePersistence = {
|
|
362
|
-
loadSchedules: vi.fn().mockResolvedValue([mockSchedule]),
|
|
363
|
-
saveSchedule: vi.fn().mockResolvedValue(undefined),
|
|
364
|
-
updateSchedule: vi.fn().mockResolvedValue(undefined),
|
|
365
|
-
deleteSchedule: vi.fn().mockResolvedValue(undefined),
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const schedulerWithPersistence = new CronScheduler({
|
|
369
|
-
engine,
|
|
370
|
-
logger: new SilentLogger(),
|
|
371
|
-
persistence: mockPersistence,
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
await schedulerWithPersistence.start();
|
|
375
|
-
|
|
376
|
-
expect(mockPersistence.loadSchedules).toHaveBeenCalled();
|
|
377
|
-
|
|
378
|
-
const schedules = await schedulerWithPersistence.getSchedules();
|
|
379
|
-
expect(schedules).toHaveLength(1);
|
|
380
|
-
expect(schedules[0].id).toBe('persisted-1');
|
|
381
|
-
|
|
382
|
-
await schedulerWithPersistence.stop();
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('should save schedule to persistence when added', async () => {
|
|
386
|
-
const mockPersistence: SchedulePersistence = {
|
|
387
|
-
loadSchedules: vi.fn().mockResolvedValue([]),
|
|
388
|
-
saveSchedule: vi.fn().mockResolvedValue(undefined),
|
|
389
|
-
updateSchedule: vi.fn().mockResolvedValue(undefined),
|
|
390
|
-
deleteSchedule: vi.fn().mockResolvedValue(undefined),
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const schedulerWithPersistence = new CronScheduler({
|
|
394
|
-
engine,
|
|
395
|
-
logger: new SilentLogger(),
|
|
396
|
-
persistence: mockPersistence,
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
await schedulerWithPersistence.addSchedule({
|
|
400
|
-
workflowKind: 'test.workflow',
|
|
401
|
-
triggerType: 'cron',
|
|
402
|
-
cronExpression: '0 * * * *',
|
|
403
|
-
enabled: true,
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
expect(mockPersistence.saveSchedule).toHaveBeenCalled();
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
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() };
|
|
413
|
-
const errorScheduler = new CronScheduler({
|
|
414
|
-
engine,
|
|
415
|
-
logger,
|
|
416
|
-
pollInterval: 100,
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Add a valid schedule first
|
|
420
|
-
const schedule = await errorScheduler.addSchedule({
|
|
421
|
-
workflowKind: 'test.workflow',
|
|
422
|
-
triggerType: 'cron',
|
|
423
|
-
cronExpression: '0 * * * *',
|
|
424
|
-
enabled: true,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
expect(schedule.nextRunAt).toBeInstanceOf(Date);
|
|
428
|
-
|
|
429
|
-
// Now directly update the internal schedule with a malformed expression
|
|
430
|
-
// to trigger the error path in updateNextRunTime
|
|
431
|
-
// We need to bypass the validation in updateSchedule by manipulating the internal state.
|
|
432
|
-
// The updateSchedule method validates and throws, but updateNextRunTime is called
|
|
433
|
-
// during start() when loading from persistence — so we test via start() loading bad data.
|
|
434
|
-
const mockPersistence: SchedulePersistence = {
|
|
435
|
-
loadSchedules: vi.fn().mockResolvedValue([
|
|
436
|
-
{
|
|
437
|
-
id: 'bad-cron',
|
|
438
|
-
workflowKind: 'test.workflow',
|
|
439
|
-
triggerType: 'cron' as const,
|
|
440
|
-
cronExpression: '99 99 99 99 99',
|
|
441
|
-
enabled: true,
|
|
442
|
-
},
|
|
443
|
-
]),
|
|
444
|
-
saveSchedule: vi.fn().mockResolvedValue(undefined),
|
|
445
|
-
updateSchedule: vi.fn().mockResolvedValue(undefined),
|
|
446
|
-
deleteSchedule: vi.fn().mockResolvedValue(undefined),
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const badScheduler = new CronScheduler({
|
|
450
|
-
engine,
|
|
451
|
-
logger,
|
|
452
|
-
persistence: mockPersistence,
|
|
453
|
-
pollInterval: 100,
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
await badScheduler.start();
|
|
457
|
-
|
|
458
|
-
// The schedule should have been loaded with nextRunAt = undefined due to parse error
|
|
459
|
-
const loaded = badScheduler.getSchedule('bad-cron');
|
|
460
|
-
expect(loaded).toBeDefined();
|
|
461
|
-
expect(loaded!.nextRunAt).toBeUndefined();
|
|
462
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
463
|
-
expect.stringContaining('Failed to parse cron expression'),
|
|
464
|
-
expect.anything()
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
await badScheduler.stop();
|
|
468
|
-
await errorScheduler.stop();
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
});
|