@multiplier-labs/stepflow 0.3.4 → 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.
Files changed (99) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-U3PCVK6O.js → chunk-D5HW7VEG.js} +4 -2
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-5XJBAPXF.js → chunk-JUPZEBR3.js} +3 -1
  5. package/dist/{chunk-5XJBAPXF.js.map → chunk-JUPZEBR3.js.map} +1 -1
  6. package/dist/events/index.d.ts +2 -0
  7. package/dist/events/index.js +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/storage/index.js +1 -1
  10. package/package.json +23 -12
  11. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  12. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  13. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  14. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  15. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  16. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  17. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  18. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  19. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  20. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  21. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  22. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  23. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  24. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  25. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  26. package/.github/CODEOWNERS +0 -15
  27. package/.github/dependabot.yml +0 -23
  28. package/.github/workflows/ci.yml +0 -38
  29. package/.github/workflows/publish.yml +0 -42
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-U3PCVK6O.js.map +0 -1
  34. package/docs/github-packages-setup.md +0 -97
  35. package/docs/stepflow-api-reference.md +0 -1663
  36. package/src/core/engine/concurrency.ts +0 -229
  37. package/src/core/engine/lifecycle.ts +0 -176
  38. package/src/core/engine.test.ts +0 -1435
  39. package/src/core/engine.ts +0 -598
  40. package/src/core/index.ts +0 -7
  41. package/src/core/orchestrator/persistence.ts +0 -165
  42. package/src/core/orchestrator/plan-resolution.ts +0 -140
  43. package/src/core/orchestrator/state-transitions.ts +0 -109
  44. package/src/core/orchestrator/step-runner.ts +0 -427
  45. package/src/core/orchestrator.ts +0 -252
  46. package/src/core/types.ts +0 -220
  47. package/src/events/index.ts +0 -8
  48. package/src/events/memory.test.ts +0 -107
  49. package/src/events/memory.ts +0 -92
  50. package/src/events/socketio.test.ts +0 -378
  51. package/src/events/socketio.ts +0 -262
  52. package/src/events/types.ts +0 -118
  53. package/src/events/webhook.test.ts +0 -949
  54. package/src/events/webhook.ts +0 -807
  55. package/src/index.ts +0 -270
  56. package/src/planning/evaluate-condition.test.ts +0 -153
  57. package/src/planning/evaluate-condition.ts +0 -105
  58. package/src/planning/index.ts +0 -55
  59. package/src/planning/planner.ts +0 -494
  60. package/src/planning/planning.test.ts +0 -727
  61. package/src/planning/registry.test.ts +0 -398
  62. package/src/planning/registry.ts +0 -283
  63. package/src/planning/types.ts +0 -566
  64. package/src/scheduler/cron.test.ts +0 -479
  65. package/src/scheduler/cron.ts +0 -438
  66. package/src/scheduler/index.ts +0 -8
  67. package/src/scheduler/postgres-persistence.ts +0 -530
  68. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  69. package/src/scheduler/sqlite-persistence.ts +0 -286
  70. package/src/scheduler/types.ts +0 -73
  71. package/src/storage/index.ts +0 -27
  72. package/src/storage/memory.test.ts +0 -349
  73. package/src/storage/memory.ts +0 -227
  74. package/src/storage/postgres-core.ts +0 -550
  75. package/src/storage/postgres-migration.test.ts +0 -69
  76. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  77. package/src/storage/postgres.ts +0 -835
  78. package/src/storage/sqlite-core.ts +0 -151
  79. package/src/storage/sqlite.test.ts +0 -642
  80. package/src/storage/sqlite.ts +0 -717
  81. package/src/storage/types.ts +0 -343
  82. package/src/utils/errors.test.ts +0 -150
  83. package/src/utils/errors.ts +0 -161
  84. package/src/utils/id.ts +0 -21
  85. package/src/utils/index.ts +0 -10
  86. package/src/utils/logger.test.ts +0 -121
  87. package/src/utils/logger.ts +0 -96
  88. package/src/utils/pinned-fetch.test.ts +0 -84
  89. package/src/utils/pinned-fetch.ts +0 -118
  90. package/src/utils/postgres-deps.ts +0 -79
  91. package/src/utils/retry.test.ts +0 -177
  92. package/src/utils/retry.ts +0 -121
  93. package/src/utils/safe-json.test.ts +0 -166
  94. package/src/utils/safe-json.ts +0 -107
  95. package/src/utils/webhook-signature.test.ts +0 -111
  96. package/src/utils/webhook-signature.ts +0 -74
  97. package/tsconfig.json +0 -21
  98. package/tsup.config.ts +0 -14
  99. package/vitest.config.ts +0 -9
@@ -1,479 +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
- // 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
-
234
- vi.useRealTimers();
235
-
236
- // Note: With fake timers, the actual workflow may not complete
237
- // This test verifies the scheduler logic, not full execution
238
- });
239
- });
240
-
241
- describe('workflow completion triggers', () => {
242
- it('should trigger workflow on completion of another workflow', async () => {
243
- // Register a trigger workflow
244
- engine.registerWorkflow({
245
- kind: 'trigger.workflow',
246
- name: 'Trigger Workflow',
247
- steps: [
248
- {
249
- key: 'trigger',
250
- name: 'Trigger',
251
- handler: async () => ({ triggered: true }),
252
- },
253
- ],
254
- });
255
-
256
- // Register a dependent workflow
257
- engine.registerWorkflow({
258
- kind: 'dependent.workflow',
259
- name: 'Dependent Workflow',
260
- steps: [
261
- {
262
- key: 'dependent',
263
- name: 'Dependent',
264
- handler: async (ctx) => ({
265
- parentRunId: ctx.metadata.triggerRunId
266
- }),
267
- },
268
- ],
269
- });
270
-
271
- // Add a completion trigger
272
- await scheduler.addSchedule({
273
- workflowKind: 'dependent.workflow',
274
- triggerType: 'workflow_completed',
275
- triggerOnWorkflowKind: 'trigger.workflow',
276
- triggerOnStatus: ['succeeded'],
277
- enabled: true,
278
- });
279
-
280
- await scheduler.start();
281
-
282
- // Start the trigger workflow
283
- const triggerRunId = await engine.startRun({ kind: 'trigger.workflow' });
284
-
285
- // Wait for trigger workflow to complete
286
- await engine.waitForRun(triggerRunId);
287
-
288
- // Give time for the completion trigger to fire and dependent workflow to start
289
- await new Promise(resolve => setTimeout(resolve, 200));
290
-
291
- // Check that the dependent workflow was started
292
- const storage = engine.getStorage();
293
- const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
294
-
295
- expect(items.length).toBeGreaterThanOrEqual(1);
296
- });
297
-
298
- it('should not trigger on non-matching status', async () => {
299
- // Register a failing trigger workflow
300
- engine.registerWorkflow({
301
- kind: 'failing.workflow',
302
- name: 'Failing Workflow',
303
- steps: [
304
- {
305
- key: 'fail',
306
- name: 'Fail',
307
- handler: async () => {
308
- throw new Error('Intentional failure');
309
- },
310
- },
311
- ],
312
- });
313
-
314
- // Add a completion trigger for success only
315
- await scheduler.addSchedule({
316
- workflowKind: 'test.workflow',
317
- triggerType: 'workflow_completed',
318
- triggerOnWorkflowKind: 'failing.workflow',
319
- triggerOnStatus: ['succeeded'], // Only on success
320
- enabled: true,
321
- });
322
-
323
- await scheduler.start();
324
-
325
- // Start the failing workflow
326
- const runId = await engine.startRun({ kind: 'failing.workflow' });
327
-
328
- // Wait for it to fail
329
- await engine.waitForRun(runId);
330
-
331
- // Give time for any triggers
332
- await new Promise(resolve => setTimeout(resolve, 200));
333
-
334
- // Check that test.workflow was NOT started
335
- const storage = engine.getStorage();
336
- const { items } = await storage.listRuns({ kind: 'test.workflow' });
337
-
338
- expect(items).toHaveLength(0);
339
- });
340
- });
341
-
342
- describe('start/stop', () => {
343
- it('should start and stop without error', async () => {
344
- await scheduler.start();
345
- await scheduler.stop();
346
- });
347
-
348
- it('should warn when starting twice', async () => {
349
- await scheduler.start();
350
- await scheduler.start(); // Should just warn
351
- await scheduler.stop();
352
- });
353
-
354
- it('should handle stopping when not started', async () => {
355
- await scheduler.stop(); // Should do nothing
356
- });
357
- });
358
-
359
- describe('persistence', () => {
360
- it('should load schedules from persistence on start', async () => {
361
- const mockSchedule: WorkflowSchedule = {
362
- id: 'persisted-1',
363
- workflowKind: 'test.workflow',
364
- triggerType: 'cron',
365
- cronExpression: '0 * * * *',
366
- enabled: true,
367
- };
368
-
369
- const mockPersistence: SchedulePersistence = {
370
- loadSchedules: vi.fn().mockResolvedValue([mockSchedule]),
371
- saveSchedule: vi.fn().mockResolvedValue(undefined),
372
- updateSchedule: vi.fn().mockResolvedValue(undefined),
373
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
374
- };
375
-
376
- const schedulerWithPersistence = new CronScheduler({
377
- engine,
378
- logger: new SilentLogger(),
379
- persistence: mockPersistence,
380
- });
381
-
382
- await schedulerWithPersistence.start();
383
-
384
- expect(mockPersistence.loadSchedules).toHaveBeenCalled();
385
-
386
- const schedules = await schedulerWithPersistence.getSchedules();
387
- expect(schedules).toHaveLength(1);
388
- expect(schedules[0].id).toBe('persisted-1');
389
-
390
- await schedulerWithPersistence.stop();
391
- });
392
-
393
- it('should save schedule to persistence when added', async () => {
394
- const mockPersistence: SchedulePersistence = {
395
- loadSchedules: vi.fn().mockResolvedValue([]),
396
- saveSchedule: vi.fn().mockResolvedValue(undefined),
397
- updateSchedule: vi.fn().mockResolvedValue(undefined),
398
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
399
- };
400
-
401
- const schedulerWithPersistence = new CronScheduler({
402
- engine,
403
- logger: new SilentLogger(),
404
- persistence: mockPersistence,
405
- });
406
-
407
- await schedulerWithPersistence.addSchedule({
408
- workflowKind: 'test.workflow',
409
- triggerType: 'cron',
410
- cronExpression: '0 * * * *',
411
- enabled: true,
412
- });
413
-
414
- expect(mockPersistence.saveSchedule).toHaveBeenCalled();
415
- });
416
- });
417
-
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() };
421
- const errorScheduler = new CronScheduler({
422
- engine,
423
- logger,
424
- pollInterval: 100,
425
- });
426
-
427
- // Add a valid schedule first
428
- const schedule = await errorScheduler.addSchedule({
429
- workflowKind: 'test.workflow',
430
- triggerType: 'cron',
431
- cronExpression: '0 * * * *',
432
- enabled: true,
433
- });
434
-
435
- expect(schedule.nextRunAt).toBeInstanceOf(Date);
436
-
437
- // Now directly update the internal schedule with a malformed expression
438
- // to trigger the error path in updateNextRunTime
439
- // We need to bypass the validation in updateSchedule by manipulating the internal state.
440
- // The updateSchedule method validates and throws, but updateNextRunTime is called
441
- // during start() when loading from persistence — so we test via start() loading bad data.
442
- const mockPersistence: SchedulePersistence = {
443
- loadSchedules: vi.fn().mockResolvedValue([
444
- {
445
- id: 'bad-cron',
446
- workflowKind: 'test.workflow',
447
- triggerType: 'cron' as const,
448
- cronExpression: '99 99 99 99 99',
449
- enabled: true,
450
- },
451
- ]),
452
- saveSchedule: vi.fn().mockResolvedValue(undefined),
453
- updateSchedule: vi.fn().mockResolvedValue(undefined),
454
- deleteSchedule: vi.fn().mockResolvedValue(undefined),
455
- };
456
-
457
- const badScheduler = new CronScheduler({
458
- engine,
459
- logger,
460
- persistence: mockPersistence,
461
- pollInterval: 100,
462
- });
463
-
464
- await badScheduler.start();
465
-
466
- // The schedule should have been loaded with nextRunAt = undefined due to parse error
467
- const loaded = badScheduler.getSchedule('bad-cron');
468
- expect(loaded).toBeDefined();
469
- expect(loaded!.nextRunAt).toBeUndefined();
470
- expect(logger.error).toHaveBeenCalledWith(
471
- expect.stringContaining('Invalid cron expression'),
472
- expect.anything()
473
- );
474
-
475
- await badScheduler.stop();
476
- await errorScheduler.stop();
477
- });
478
- });
479
- });