@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.
- package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -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-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- 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 +322 -56
- package/dist/storage/index.js +4 -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 +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- 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 +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- 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-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- 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 +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- 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
|
|
|
@@ -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(
|
|
238
|
-
it(
|
|
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:
|
|
242
|
-
name:
|
|
245
|
+
kind: 'trigger.workflow',
|
|
246
|
+
name: 'Trigger Workflow',
|
|
243
247
|
steps: [
|
|
244
248
|
{
|
|
245
|
-
key:
|
|
246
|
-
name:
|
|
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:
|
|
255
|
-
name:
|
|
258
|
+
kind: 'dependent.workflow',
|
|
259
|
+
name: 'Dependent Workflow',
|
|
256
260
|
steps: [
|
|
257
261
|
{
|
|
258
|
-
key:
|
|
259
|
-
name:
|
|
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:
|
|
270
|
-
triggerType:
|
|
271
|
-
triggerOnWorkflowKind:
|
|
272
|
-
triggerOnStatus: [
|
|
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:
|
|
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(
|
|
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:
|
|
293
|
+
const { items } = await storage.listRuns({ kind: 'dependent.workflow' });
|
|
290
294
|
|
|
291
295
|
expect(items.length).toBeGreaterThanOrEqual(1);
|
|
292
296
|
});
|
|
293
297
|
|
|
294
|
-
it(
|
|
298
|
+
it('should not trigger on non-matching status', async () => {
|
|
295
299
|
// Register a failing trigger workflow
|
|
296
300
|
engine.registerWorkflow({
|
|
297
|
-
kind:
|
|
298
|
-
name:
|
|
301
|
+
kind: 'failing.workflow',
|
|
302
|
+
name: 'Failing Workflow',
|
|
299
303
|
steps: [
|
|
300
304
|
{
|
|
301
|
-
key:
|
|
302
|
-
name:
|
|
305
|
+
key: 'fail',
|
|
306
|
+
name: 'Fail',
|
|
303
307
|
handler: async () => {
|
|
304
|
-
throw new Error(
|
|
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:
|
|
313
|
-
triggerType:
|
|
314
|
-
triggerOnWorkflowKind:
|
|
315
|
-
triggerOnStatus: [
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
339
|
-
it(
|
|
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(
|
|
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(
|
|
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(
|
|
356
|
-
it(
|
|
359
|
+
describe('persistence', () => {
|
|
360
|
+
it('should load schedules from persistence on start', async () => {
|
|
357
361
|
const mockSchedule: WorkflowSchedule = {
|
|
358
|
-
id:
|
|
359
|
-
workflowKind:
|
|
360
|
-
triggerType:
|
|
361
|
-
cronExpression:
|
|
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(
|
|
388
|
+
expect(schedules[0].id).toBe('persisted-1');
|
|
385
389
|
|
|
386
390
|
await schedulerWithPersistence.stop();
|
|
387
391
|
});
|
|
388
392
|
|
|
389
|
-
it(
|
|
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:
|
|
405
|
-
triggerType:
|
|
406
|
-
cronExpression:
|
|
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(
|
|
415
|
-
it(
|
|
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:
|
|
431
|
-
triggerType:
|
|
432
|
-
cronExpression:
|
|
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:
|
|
447
|
-
workflowKind:
|
|
448
|
-
triggerType:
|
|
449
|
-
cronExpression:
|
|
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(
|
|
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(
|
|
473
|
-
expect.anything()
|
|
471
|
+
expect.stringContaining('Invalid cron expression'),
|
|
472
|
+
expect.anything()
|
|
474
473
|
);
|
|
475
474
|
|
|
476
475
|
await badScheduler.stop();
|