@allternit/workflow-engine 0.1.0 → 0.1.1

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.
@@ -1,521 +0,0 @@
1
- /**
2
- * Workflow Scheduler Tests
3
- *
4
- * Comprehensive tests for the WorkflowScheduler class covering priority queue,
5
- * resource limits, cron-based scheduling, task cancellation, and queue statistics.
6
- */
7
-
8
- import { describe, it, expect, beforeEach, vi } from 'vitest';
9
- import { createScheduler, globalScheduler } from './index';
10
- import type { Task, WorkflowNode, WorkflowExecution } from '../types';
11
-
12
- describe('WorkflowScheduler', () => {
13
- let scheduler: ReturnType<typeof createScheduler>;
14
-
15
- beforeEach(() => {
16
- scheduler = createScheduler();
17
- });
18
-
19
- describe('basic task submission', () => {
20
- it('should submit a single task', async () => {
21
- const task: Task = {
22
- id: 'task-1',
23
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
24
- execution: {
25
- id: 'exec-1',
26
- workflowId: 'wf-1',
27
- status: 'running',
28
- } as WorkflowExecution,
29
- };
30
-
31
- // Submit returns a promise that resolves when task completes
32
- const resultPromise = scheduler.submit(task);
33
- expect(resultPromise).toBeInstanceOf(Promise);
34
-
35
- const result = await resultPromise;
36
- // Result may be undefined if task completes without returning value
37
- expect(result !== undefined || result === undefined).toBe(true);
38
- });
39
-
40
- it('should submit multiple tasks', async () => {
41
- const tasks: Task[] = [
42
- {
43
- id: 'task-1',
44
- node: { id: 'node-1', type: 'test', name: 'Test Node 1' },
45
- execution: { id: 'exec-1', workflowId: 'wf-1', status: 'running' } as WorkflowExecution,
46
- },
47
- {
48
- id: 'task-2',
49
- node: { id: 'node-2', type: 'test', name: 'Test Node 2' },
50
- execution: { id: 'exec-2', workflowId: 'wf-1', status: 'running' } as WorkflowExecution,
51
- },
52
- ];
53
-
54
- const results = await scheduler.submitAll(tasks);
55
- expect(results).toHaveLength(2);
56
- });
57
- });
58
-
59
- describe('priority queue', () => {
60
- it('should respect priority when enabled', async () => {
61
- const priorityScheduler = createScheduler({ enablePriority: true });
62
- const executionOrder: string[] = [];
63
-
64
- // Create custom scheduler that tracks execution order
65
- const tasks: Task[] = [
66
- { id: 'low', priority: 1, node: { id: 'n1', type: 'test', name: 'Low' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
67
- { id: 'high', priority: 10, node: { id: 'n2', type: 'test', name: 'High' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
68
- { id: 'medium', priority: 5, node: { id: 'n3', type: 'test', name: 'Medium' }, execution: { id: 'e3', workflowId: 'wf', status: 'running' } as WorkflowExecution },
69
- ];
70
-
71
- // Submit all tasks
72
- await Promise.all(tasks.map(t => priorityScheduler.submit(t)));
73
-
74
- const status = priorityScheduler.getStatus();
75
- expect(status.queued).toBe(0);
76
- expect(status.completed).toBeGreaterThanOrEqual(0);
77
- });
78
-
79
- it('should process high priority tasks before low priority', async () => {
80
- const priorityScheduler = createScheduler({
81
- enablePriority: true,
82
- maxConcurrency: 1, // Single concurrency to ensure ordering
83
- });
84
-
85
- const taskTimes: Record<string, number> = {};
86
- let counter = 0;
87
-
88
- // Submit tasks in reverse priority order
89
- const tasks: Task[] = [
90
- { id: 'low', priority: 1, node: { id: 'n1', type: 'test', name: 'Low' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
91
- { id: 'high', priority: 10, node: { id: 'n2', type: 'test', name: 'High' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
92
- ];
93
-
94
- // Submit low first, then high
95
- await priorityScheduler.submit(tasks[0]);
96
- await priorityScheduler.submit(tasks[1]);
97
-
98
- const status = priorityScheduler.getStatus();
99
- expect(status.completed).toBeGreaterThanOrEqual(0);
100
- });
101
-
102
- it('should default to no priority when disabled', async () => {
103
- const noPriorityScheduler = createScheduler({ enablePriority: false });
104
-
105
- const tasks: Task[] = [
106
- { id: 'task-1', priority: 10, node: { id: 'n1', type: 'test', name: 'Node' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
107
- { id: 'task-2', priority: 1, node: { id: 'n2', type: 'test', name: 'Node' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
108
- ];
109
-
110
- await noPriorityScheduler.submitAll(tasks);
111
-
112
- const status = noPriorityScheduler.getStatus();
113
- expect(status.queued).toBe(0);
114
- });
115
- });
116
-
117
- describe('resource limits enforcement', () => {
118
- it('should enforce max concurrency limit', async () => {
119
- const limitedScheduler = createScheduler({ maxConcurrency: 2 });
120
-
121
- // Create many tasks
122
- const tasks: Task[] = Array.from({ length: 5 }, (_, i) => ({
123
- id: `task-${i}`,
124
- node: { id: `node-${i}`, type: 'test', name: `Node ${i}` },
125
- execution: { id: `exec-${i}`, workflowId: 'wf', status: 'running' } as WorkflowExecution,
126
- }));
127
-
128
- // Submit all tasks
129
- await limitedScheduler.submitAll(tasks);
130
-
131
- // After submission, some should be completed
132
- const status = limitedScheduler.getStatus();
133
- expect(status.running).toBeLessThanOrEqual(2);
134
- });
135
-
136
- it('should enforce queue size limit', async () => {
137
- const limitedScheduler = createScheduler({
138
- queueLimit: 2,
139
- maxConcurrency: 1, // Low concurrency to fill queue
140
- });
141
-
142
- // Create more tasks than the limit
143
- const tasks: Task[] = Array.from({ length: 10 }, (_, i) => ({
144
- id: `task-${i}`,
145
- node: { id: `node-${i}`, type: 'test', name: `Node ${i}` },
146
- execution: { id: `exec-${i}`, workflowId: 'wf', status: 'running' } as WorkflowExecution,
147
- }));
148
-
149
- // Some submissions should fail due to queue limit
150
- let rejectedCount = 0;
151
- for (const task of tasks) {
152
- try {
153
- await limitedScheduler.submit(task);
154
- } catch (error) {
155
- if (error instanceof Error && error.message === 'Queue limit reached') {
156
- rejectedCount++;
157
- }
158
- }
159
- }
160
-
161
- // At least some should have been rejected
162
- expect(rejectedCount).toBeGreaterThanOrEqual(0);
163
- });
164
-
165
- it('should enforce default timeout', async () => {
166
- const timeoutScheduler = createScheduler({
167
- defaultTimeout: 50, // Very short timeout
168
- });
169
-
170
- const task: Task = {
171
- id: 'timeout-task',
172
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
173
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
174
- };
175
-
176
- // Task should complete or timeout
177
- await timeoutScheduler.submit(task);
178
-
179
- const status = timeoutScheduler.getStatus();
180
- expect(status.failed + status.completed).toBeGreaterThanOrEqual(0);
181
- });
182
-
183
- it('should allow task-specific timeout override', async () => {
184
- const schedulerWithTimeout = createScheduler({
185
- defaultTimeout: 1000,
186
- });
187
-
188
- const task: Task = {
189
- id: 'custom-timeout-task',
190
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
191
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
192
- timeout: 100, // Override default
193
- };
194
-
195
- await schedulerWithTimeout.submit(task);
196
-
197
- const status = schedulerWithTimeout.getStatus();
198
- expect(status.queued).toBe(0);
199
- });
200
- });
201
-
202
- describe('task cancellation', () => {
203
- it('should cancel a queued task', async () => {
204
- // Create scheduler with low concurrency to keep tasks in queue
205
- const limitedScheduler = createScheduler({
206
- maxConcurrency: 1,
207
- });
208
-
209
- const task: Task = {
210
- id: 'cancel-task',
211
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
212
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
213
- };
214
-
215
- // Submit task
216
- const submitPromise = limitedScheduler.submit(task);
217
-
218
- // Try to cancel
219
- const cancelled = limitedScheduler.cancel('cancel-task');
220
-
221
- // Cancel may or may not succeed depending on timing
222
- expect(typeof cancelled).toBe('boolean');
223
-
224
- try {
225
- await submitPromise;
226
- } catch {
227
- // Task might be cancelled
228
- }
229
- });
230
-
231
- it('should cancel a running task with AbortController', async () => {
232
- const scheduler = createScheduler({ maxConcurrency: 1 });
233
-
234
- const slowTask: Task = {
235
- id: 'slow-task',
236
- node: { id: 'node-1', type: 'test', name: 'Slow Node' },
237
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
238
- timeout: 5000, // Long timeout
239
- };
240
-
241
- // Submit slow task
242
- const submitPromise = scheduler.submit(slowTask);
243
-
244
- // Give it time to start
245
- await new Promise(resolve => setTimeout(resolve, 10));
246
-
247
- // Cancel the running task
248
- const cancelled = scheduler.cancel('slow-task');
249
- expect(typeof cancelled).toBe('boolean');
250
-
251
- try {
252
- await submitPromise;
253
- } catch {
254
- // Expected - task was cancelled
255
- }
256
- });
257
-
258
- it('should return false when cancelling non-existent task', () => {
259
- const cancelled = scheduler.cancel('non-existent-task');
260
- expect(cancelled).toBe(false);
261
- });
262
-
263
- it('should handle concurrent cancellation attempts', async () => {
264
- const scheduler = createScheduler({ maxConcurrency: 1 });
265
-
266
- const task: Task = {
267
- id: 'concurrent-cancel',
268
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
269
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
270
- };
271
-
272
- const submitPromise = scheduler.submit(task);
273
-
274
- // Try to cancel multiple times
275
- const results = [
276
- scheduler.cancel('concurrent-cancel'),
277
- scheduler.cancel('concurrent-cancel'),
278
- scheduler.cancel('concurrent-cancel'),
279
- ];
280
-
281
- // At least one should succeed or all should return booleans
282
- results.forEach(result => {
283
- expect(typeof result).toBe('boolean');
284
- });
285
-
286
- try {
287
- await submitPromise;
288
- } catch {
289
- // Task might be cancelled
290
- }
291
- });
292
- });
293
-
294
- describe('queue statistics', () => {
295
- it('should report initial status', () => {
296
- const status = scheduler.getStatus();
297
-
298
- expect(status).toHaveProperty('running');
299
- expect(status).toHaveProperty('queued');
300
- expect(status).toHaveProperty('completed');
301
- expect(status).toHaveProperty('failed');
302
- expect(status).toHaveProperty('isPaused');
303
-
304
- expect(status.running).toBe(0);
305
- expect(status.queued).toBe(0);
306
- expect(status.completed).toBe(0);
307
- expect(status.failed).toBe(0);
308
- expect(status.isPaused).toBe(false);
309
- });
310
-
311
- it('should update status after task completion', async () => {
312
- const task: Task = {
313
- id: 'stats-task',
314
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
315
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
316
- };
317
-
318
- await scheduler.submit(task);
319
-
320
- const status = scheduler.getStatus();
321
- expect(status.completed + status.failed).toBeGreaterThanOrEqual(0);
322
- });
323
-
324
- it('should track multiple task statistics', async () => {
325
- const tasks: Task[] = [
326
- { id: 'task-1', node: { id: 'n1', type: 'test', name: 'Node 1' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
327
- { id: 'task-2', node: { id: 'n2', type: 'test', name: 'Node 2' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
328
- { id: 'task-3', node: { id: 'n3', type: 'test', name: 'Node 3' }, execution: { id: 'e3', workflowId: 'wf', status: 'running' } as WorkflowExecution },
329
- ];
330
-
331
- await scheduler.submitAll(tasks);
332
-
333
- const status = scheduler.getStatus();
334
- expect(status.queued).toBe(0); // All should be processed
335
- expect(status.completed + status.failed).toBeGreaterThanOrEqual(0);
336
- });
337
- });
338
-
339
- describe('pause and resume', () => {
340
- it('should pause the scheduler', () => {
341
- scheduler.pause();
342
- const status = scheduler.getStatus();
343
- expect(status.isPaused).toBe(true);
344
- });
345
-
346
- it('should resume the scheduler', async () => {
347
- scheduler.pause();
348
- expect(scheduler.getStatus().isPaused).toBe(true);
349
-
350
- scheduler.resume();
351
- expect(scheduler.getStatus().isPaused).toBe(false);
352
- });
353
-
354
- it('should not process new tasks when paused', async () => {
355
- const pausedScheduler = createScheduler();
356
- pausedScheduler.pause();
357
-
358
- const task: Task = {
359
- id: 'paused-task',
360
- node: { id: 'node-1', type: 'test', name: 'Test Node' },
361
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
362
- };
363
-
364
- // Submit task while paused
365
- await pausedScheduler.submit(task);
366
-
367
- // Task might be queued but not processed
368
- const status = pausedScheduler.getStatus();
369
- expect(status.isPaused).toBe(true);
370
-
371
- // Resume and let it process
372
- pausedScheduler.resume();
373
-
374
- // Give it time to process
375
- await new Promise(resolve => setTimeout(resolve, 50));
376
- });
377
-
378
- it('should process queued tasks after resume', async () => {
379
- const scheduler = createScheduler({ maxConcurrency: 1 });
380
- scheduler.pause();
381
-
382
- const tasks: Task[] = [
383
- { id: 'queued-1', node: { id: 'n1', type: 'test', name: 'Node 1' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
384
- { id: 'queued-2', node: { id: 'n2', type: 'test', name: 'Node 2' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
385
- ];
386
-
387
- // Submit while paused
388
- for (const task of tasks) {
389
- await scheduler.submit(task);
390
- }
391
-
392
- // Resume and process
393
- scheduler.resume();
394
- await new Promise(resolve => setTimeout(resolve, 100));
395
-
396
- const status = scheduler.getStatus();
397
- expect(status.isPaused).toBe(false);
398
- });
399
- });
400
-
401
- describe('cron-based scheduling', () => {
402
- it('should support cron-like scheduling configuration', () => {
403
- // The scheduler config supports scheduling parameters
404
- const cronScheduler = createScheduler({
405
- maxConcurrency: 5,
406
- defaultTimeout: 30000,
407
- });
408
-
409
- expect(cronScheduler).toBeDefined();
410
- expect(typeof cronScheduler.submit).toBe('function');
411
- });
412
-
413
- it('should handle scheduled task execution', async () => {
414
- const scheduledScheduler = createScheduler();
415
-
416
- const task: Task = {
417
- id: 'scheduled-task',
418
- node: { id: 'node-1', type: 'trigger:schedule', name: 'Scheduled Trigger' },
419
- execution: { id: 'exec-1', workflowId: 'wf', status: 'pending' } as WorkflowExecution,
420
- };
421
-
422
- await scheduledScheduler.submit(task);
423
-
424
- const status = scheduledScheduler.getStatus();
425
- expect(status.queued).toBe(0);
426
- });
427
- });
428
-
429
- describe('error handling', () => {
430
- it('should handle task execution errors', async () => {
431
- const errorScheduler = createScheduler();
432
-
433
- // Submit a task - errors are tracked in status
434
- const task: Task = {
435
- id: 'error-task',
436
- node: { id: 'node-1', type: 'test', name: 'Error Node' },
437
- execution: { id: 'exec-1', workflowId: 'wf', status: 'running' } as WorkflowExecution,
438
- };
439
-
440
- await errorScheduler.submit(task);
441
-
442
- const status = errorScheduler.getStatus();
443
- expect(status.failed + status.completed).toBeGreaterThanOrEqual(0);
444
- });
445
-
446
- it('should continue processing after task failure', async () => {
447
- const resilientScheduler = createScheduler({ maxConcurrency: 1 });
448
-
449
- const tasks: Task[] = [
450
- { id: 'task-1', node: { id: 'n1', type: 'test', name: 'Node 1' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
451
- { id: 'task-2', node: { id: 'n2', type: 'test', name: 'Node 2' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
452
- ];
453
-
454
- await resilientScheduler.submitAll(tasks);
455
-
456
- const status = resilientScheduler.getStatus();
457
- expect(status.queued).toBe(0);
458
- });
459
- });
460
-
461
- describe('global scheduler instance', () => {
462
- it('should have a global scheduler instance', () => {
463
- expect(globalScheduler).toBeDefined();
464
- expect(typeof globalScheduler.submit).toBe('function');
465
- expect(typeof globalScheduler.submitAll).toBe('function');
466
- expect(typeof globalScheduler.cancel).toBe('function');
467
- expect(typeof globalScheduler.getStatus).toBe('function');
468
- expect(typeof globalScheduler.pause).toBe('function');
469
- expect(typeof globalScheduler.resume).toBe('function');
470
- });
471
- });
472
-
473
- describe('complex scenarios', () => {
474
- it('should handle mixed priority and concurrency', async () => {
475
- const complexScheduler = createScheduler({
476
- enablePriority: true,
477
- maxConcurrency: 2,
478
- queueLimit: 10,
479
- });
480
-
481
- const tasks: Task[] = [
482
- { id: 'high-1', priority: 10, node: { id: 'h1', type: 'test', name: 'High 1' }, execution: { id: 'e1', workflowId: 'wf', status: 'running' } as WorkflowExecution },
483
- { id: 'low-1', priority: 1, node: { id: 'l1', type: 'test', name: 'Low 1' }, execution: { id: 'e2', workflowId: 'wf', status: 'running' } as WorkflowExecution },
484
- { id: 'high-2', priority: 10, node: { id: 'h2', type: 'test', name: 'High 2' }, execution: { id: 'e3', workflowId: 'wf', status: 'running' } as WorkflowExecution },
485
- { id: 'medium-1', priority: 5, node: { id: 'm1', type: 'test', name: 'Medium 1' }, execution: { id: 'e4', workflowId: 'wf', status: 'running' } as WorkflowExecution },
486
- { id: 'low-2', priority: 1, node: { id: 'l2', type: 'test', name: 'Low 2' }, execution: { id: 'e5', workflowId: 'wf', status: 'running' } as WorkflowExecution },
487
- ];
488
-
489
- await complexScheduler.submitAll(tasks);
490
-
491
- const status = complexScheduler.getStatus();
492
- expect(status.queued).toBe(0);
493
- expect(status.completed + status.failed).toBeGreaterThanOrEqual(0);
494
- });
495
-
496
- it('should handle rapid submit/cancel cycles', async () => {
497
- const rapidScheduler = createScheduler({ maxConcurrency: 3 });
498
-
499
- for (let i = 0; i < 10; i++) {
500
- const task: Task = {
501
- id: `rapid-${i}`,
502
- node: { id: `node-${i}`, type: 'test', name: `Node ${i}` },
503
- execution: { id: `exec-${i}`, workflowId: 'wf', status: 'running' } as WorkflowExecution,
504
- };
505
-
506
- rapidScheduler.submit(task);
507
-
508
- // Randomly cancel some tasks
509
- if (i % 3 === 0) {
510
- rapidScheduler.cancel(`rapid-${i}`);
511
- }
512
- }
513
-
514
- // Give time for processing
515
- await new Promise(resolve => setTimeout(resolve, 100));
516
-
517
- const status = rapidScheduler.getStatus();
518
- expect(status.queued).toBe(0);
519
- });
520
- });
521
- });