ai-workflows 2.1.1 → 2.1.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.
Files changed (78) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +165 -3
  3. package/CHANGELOG.md +10 -1
  4. package/LICENSE +21 -0
  5. package/README.md +303 -184
  6. package/dist/barrier.d.ts +153 -0
  7. package/dist/barrier.d.ts.map +1 -0
  8. package/dist/barrier.js +339 -0
  9. package/dist/barrier.js.map +1 -0
  10. package/dist/cascade-context.d.ts +149 -0
  11. package/dist/cascade-context.d.ts.map +1 -0
  12. package/dist/cascade-context.js +324 -0
  13. package/dist/cascade-context.js.map +1 -0
  14. package/dist/cascade-executor.d.ts +196 -0
  15. package/dist/cascade-executor.d.ts.map +1 -0
  16. package/dist/cascade-executor.js +384 -0
  17. package/dist/cascade-executor.js.map +1 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +4 -1
  20. package/dist/context.js.map +1 -1
  21. package/dist/dependency-graph.d.ts +157 -0
  22. package/dist/dependency-graph.d.ts.map +1 -0
  23. package/dist/dependency-graph.js +382 -0
  24. package/dist/dependency-graph.js.map +1 -0
  25. package/dist/every.d.ts +31 -2
  26. package/dist/every.d.ts.map +1 -1
  27. package/dist/every.js +63 -32
  28. package/dist/every.js.map +1 -1
  29. package/dist/graph/index.d.ts +8 -0
  30. package/dist/graph/index.d.ts.map +1 -0
  31. package/dist/graph/index.js +8 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/graph/topological-sort.d.ts +121 -0
  34. package/dist/graph/topological-sort.d.ts.map +1 -0
  35. package/dist/graph/topological-sort.js +292 -0
  36. package/dist/graph/topological-sort.js.map +1 -0
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/on.d.ts +35 -10
  42. package/dist/on.d.ts.map +1 -1
  43. package/dist/on.js +52 -18
  44. package/dist/on.js.map +1 -1
  45. package/dist/timer-registry.d.ts +52 -0
  46. package/dist/timer-registry.d.ts.map +1 -0
  47. package/dist/timer-registry.js +120 -0
  48. package/dist/timer-registry.js.map +1 -0
  49. package/dist/types.d.ts +88 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +17 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/workflow.d.ts.map +1 -1
  54. package/dist/workflow.js +15 -11
  55. package/dist/workflow.js.map +1 -1
  56. package/package.json +11 -11
  57. package/src/barrier.ts +466 -0
  58. package/src/cascade-context.ts +488 -0
  59. package/src/cascade-executor.ts +587 -0
  60. package/src/context.ts +12 -7
  61. package/src/dependency-graph.ts +518 -0
  62. package/src/every.ts +104 -35
  63. package/src/graph/index.ts +19 -0
  64. package/src/graph/topological-sort.ts +414 -0
  65. package/src/index.ts +78 -0
  66. package/src/on.ts +81 -25
  67. package/src/timer-registry.ts +145 -0
  68. package/src/types.ts +121 -0
  69. package/src/workflow.ts +23 -16
  70. package/test/barrier-join.test.ts +434 -0
  71. package/test/barrier-unhandled-rejections.test.ts +359 -0
  72. package/test/cascade-context.test.ts +390 -0
  73. package/test/cascade-executor.test.ts +859 -0
  74. package/test/dependency-graph.test.ts +512 -0
  75. package/test/graph/topological-sort.test.ts +586 -0
  76. package/test/schedule-timer-cleanup.test.ts +344 -0
  77. package/test/send-race-conditions.test.ts +410 -0
  78. package/test/type-safety-every.test.ts +303 -0
@@ -0,0 +1,586 @@
1
+ /**
2
+ * Tests for Topological Sort Implementation
3
+ *
4
+ * TDD RED Phase: These tests define the expected behavior for topological sorting
5
+ * of workflow step dependencies.
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest'
9
+ import {
10
+ topologicalSort,
11
+ topologicalSortKahn,
12
+ topologicalSortDFS,
13
+ getExecutionLevels,
14
+ CycleDetectedError,
15
+ type SortableNode,
16
+ type ExecutionLevel,
17
+ type TopologicalSortResult,
18
+ } from '../../src/graph/topological-sort.js'
19
+
20
+ describe('Topological Sort', () => {
21
+ describe('topologicalSort() - main function', () => {
22
+ it('should sort a simple linear graph', () => {
23
+ const nodes: SortableNode[] = [
24
+ { id: 'A', dependencies: [] },
25
+ { id: 'B', dependencies: ['A'] },
26
+ { id: 'C', dependencies: ['B'] },
27
+ ]
28
+
29
+ const result = topologicalSort(nodes)
30
+
31
+ expect(result.order).toEqual(['A', 'B', 'C'])
32
+ expect(result.hasCycle).toBe(false)
33
+ })
34
+
35
+ it('should handle nodes with no dependencies', () => {
36
+ const nodes: SortableNode[] = [
37
+ { id: 'A', dependencies: [] },
38
+ { id: 'B', dependencies: [] },
39
+ { id: 'C', dependencies: [] },
40
+ ]
41
+
42
+ const result = topologicalSort(nodes)
43
+
44
+ // All should appear (order may vary for equal nodes)
45
+ expect(result.order).toHaveLength(3)
46
+ expect(result.order).toContain('A')
47
+ expect(result.order).toContain('B')
48
+ expect(result.order).toContain('C')
49
+ expect(result.hasCycle).toBe(false)
50
+ })
51
+
52
+ it('should handle diamond dependency pattern', () => {
53
+ // A -> B, A -> C, B -> D, C -> D
54
+ const nodes: SortableNode[] = [
55
+ { id: 'A', dependencies: [] },
56
+ { id: 'B', dependencies: ['A'] },
57
+ { id: 'C', dependencies: ['A'] },
58
+ { id: 'D', dependencies: ['B', 'C'] },
59
+ ]
60
+
61
+ const result = topologicalSort(nodes)
62
+
63
+ expect(result.hasCycle).toBe(false)
64
+ // A must come before B and C
65
+ expect(result.order.indexOf('A')).toBeLessThan(result.order.indexOf('B'))
66
+ expect(result.order.indexOf('A')).toBeLessThan(result.order.indexOf('C'))
67
+ // B and C must come before D
68
+ expect(result.order.indexOf('B')).toBeLessThan(result.order.indexOf('D'))
69
+ expect(result.order.indexOf('C')).toBeLessThan(result.order.indexOf('D'))
70
+ })
71
+
72
+ it('should provide stable ordering for equal priority nodes', () => {
73
+ const nodes: SortableNode[] = [
74
+ { id: 'A', dependencies: [] },
75
+ { id: 'B', dependencies: [] },
76
+ { id: 'C', dependencies: [] },
77
+ ]
78
+
79
+ // Run multiple times to verify stability
80
+ const result1 = topologicalSort(nodes)
81
+ const result2 = topologicalSort(nodes)
82
+ const result3 = topologicalSort(nodes)
83
+
84
+ expect(result1.order).toEqual(result2.order)
85
+ expect(result2.order).toEqual(result3.order)
86
+ })
87
+
88
+ it('should return empty array for empty input', () => {
89
+ const result = topologicalSort([])
90
+
91
+ expect(result.order).toEqual([])
92
+ expect(result.hasCycle).toBe(false)
93
+ })
94
+
95
+ it('should handle single node', () => {
96
+ const nodes: SortableNode[] = [{ id: 'A', dependencies: [] }]
97
+
98
+ const result = topologicalSort(nodes)
99
+
100
+ expect(result.order).toEqual(['A'])
101
+ expect(result.hasCycle).toBe(false)
102
+ })
103
+
104
+ it('should handle complex multi-level dependencies', () => {
105
+ // Level 0: A, B
106
+ // Level 1: C (depends on A), D (depends on B)
107
+ // Level 2: E (depends on C, D), F (depends on D)
108
+ // Level 3: G (depends on E, F)
109
+ const nodes: SortableNode[] = [
110
+ { id: 'A', dependencies: [] },
111
+ { id: 'B', dependencies: [] },
112
+ { id: 'C', dependencies: ['A'] },
113
+ { id: 'D', dependencies: ['B'] },
114
+ { id: 'E', dependencies: ['C', 'D'] },
115
+ { id: 'F', dependencies: ['D'] },
116
+ { id: 'G', dependencies: ['E', 'F'] },
117
+ ]
118
+
119
+ const result = topologicalSort(nodes)
120
+
121
+ expect(result.hasCycle).toBe(false)
122
+ // Verify all dependency constraints
123
+ expect(result.order.indexOf('A')).toBeLessThan(result.order.indexOf('C'))
124
+ expect(result.order.indexOf('B')).toBeLessThan(result.order.indexOf('D'))
125
+ expect(result.order.indexOf('C')).toBeLessThan(result.order.indexOf('E'))
126
+ expect(result.order.indexOf('D')).toBeLessThan(result.order.indexOf('E'))
127
+ expect(result.order.indexOf('D')).toBeLessThan(result.order.indexOf('F'))
128
+ expect(result.order.indexOf('E')).toBeLessThan(result.order.indexOf('G'))
129
+ expect(result.order.indexOf('F')).toBeLessThan(result.order.indexOf('G'))
130
+ })
131
+ })
132
+
133
+ describe('Kahn\'s Algorithm Implementation', () => {
134
+ it('should sort using Kahn\'s algorithm', () => {
135
+ const nodes: SortableNode[] = [
136
+ { id: 'A', dependencies: [] },
137
+ { id: 'B', dependencies: ['A'] },
138
+ { id: 'C', dependencies: ['B'] },
139
+ ]
140
+
141
+ const result = topologicalSortKahn(nodes)
142
+
143
+ expect(result.order).toEqual(['A', 'B', 'C'])
144
+ expect(result.hasCycle).toBe(false)
145
+ })
146
+
147
+ it('should detect cycles and return partial result', () => {
148
+ // A -> B -> C -> A (cycle)
149
+ const nodes: SortableNode[] = [
150
+ { id: 'A', dependencies: ['C'] },
151
+ { id: 'B', dependencies: ['A'] },
152
+ { id: 'C', dependencies: ['B'] },
153
+ ]
154
+
155
+ const result = topologicalSortKahn(nodes)
156
+
157
+ expect(result.hasCycle).toBe(true)
158
+ expect(result.order.length).toBeLessThan(3) // Partial result
159
+ })
160
+
161
+ it('should provide in-degree information', () => {
162
+ const nodes: SortableNode[] = [
163
+ { id: 'A', dependencies: [] },
164
+ { id: 'B', dependencies: ['A'] },
165
+ { id: 'C', dependencies: ['A', 'B'] },
166
+ ]
167
+
168
+ const result = topologicalSortKahn(nodes)
169
+
170
+ // A has in-degree 0, B has in-degree 1, C has in-degree 2
171
+ expect(result.metadata?.inDegrees?.['A']).toBe(0)
172
+ expect(result.metadata?.inDegrees?.['B']).toBe(1)
173
+ expect(result.metadata?.inDegrees?.['C']).toBe(2)
174
+ })
175
+ })
176
+
177
+ describe('DFS-based Topological Sort', () => {
178
+ it('should sort using DFS algorithm', () => {
179
+ const nodes: SortableNode[] = [
180
+ { id: 'A', dependencies: [] },
181
+ { id: 'B', dependencies: ['A'] },
182
+ { id: 'C', dependencies: ['B'] },
183
+ ]
184
+
185
+ const result = topologicalSortDFS(nodes)
186
+
187
+ expect(result.order).toEqual(['A', 'B', 'C'])
188
+ expect(result.hasCycle).toBe(false)
189
+ })
190
+
191
+ it('should detect cycles during DFS', () => {
192
+ const nodes: SortableNode[] = [
193
+ { id: 'A', dependencies: ['C'] },
194
+ { id: 'B', dependencies: ['A'] },
195
+ { id: 'C', dependencies: ['B'] },
196
+ ]
197
+
198
+ const result = topologicalSortDFS(nodes)
199
+
200
+ expect(result.hasCycle).toBe(true)
201
+ })
202
+
203
+ it('should provide cycle path in metadata when cycle detected', () => {
204
+ const nodes: SortableNode[] = [
205
+ { id: 'A', dependencies: ['C'] },
206
+ { id: 'B', dependencies: ['A'] },
207
+ { id: 'C', dependencies: ['B'] },
208
+ ]
209
+
210
+ const result = topologicalSortDFS(nodes)
211
+
212
+ expect(result.hasCycle).toBe(true)
213
+ expect(result.cyclePath).toBeDefined()
214
+ expect(result.cyclePath!.length).toBeGreaterThan(0)
215
+ })
216
+ })
217
+
218
+ describe('Cycle Detection Tests', () => {
219
+ it('should detect direct cycle (A -> B -> A)', () => {
220
+ const nodes: SortableNode[] = [
221
+ { id: 'A', dependencies: ['B'] },
222
+ { id: 'B', dependencies: ['A'] },
223
+ ]
224
+
225
+ const result = topologicalSort(nodes)
226
+
227
+ expect(result.hasCycle).toBe(true)
228
+ expect(result.cyclePath).toBeDefined()
229
+ })
230
+
231
+ it('should detect indirect cycle (A -> B -> C -> A)', () => {
232
+ const nodes: SortableNode[] = [
233
+ { id: 'A', dependencies: ['C'] },
234
+ { id: 'B', dependencies: ['A'] },
235
+ { id: 'C', dependencies: ['B'] },
236
+ ]
237
+
238
+ const result = topologicalSort(nodes)
239
+
240
+ expect(result.hasCycle).toBe(true)
241
+ expect(result.cyclePath).toBeDefined()
242
+ // Cycle path should contain all nodes in the cycle
243
+ expect(result.cyclePath!.length).toBeGreaterThanOrEqual(3)
244
+ })
245
+
246
+ it('should detect self-referencing cycle (A -> A)', () => {
247
+ const nodes: SortableNode[] = [{ id: 'A', dependencies: ['A'] }]
248
+
249
+ const result = topologicalSort(nodes)
250
+
251
+ expect(result.hasCycle).toBe(true)
252
+ expect(result.cyclePath).toContain('A')
253
+ })
254
+
255
+ it('should provide appropriate error message with cycle path', () => {
256
+ const nodes: SortableNode[] = [
257
+ { id: 'A', dependencies: ['C'] },
258
+ { id: 'B', dependencies: ['A'] },
259
+ { id: 'C', dependencies: ['B'] },
260
+ ]
261
+
262
+ expect(() => {
263
+ const result = topologicalSort(nodes, { throwOnCycle: true })
264
+ }).toThrow(CycleDetectedError)
265
+ })
266
+
267
+ it('should include cycle path in CycleDetectedError', () => {
268
+ const nodes: SortableNode[] = [
269
+ { id: 'A', dependencies: ['C'] },
270
+ { id: 'B', dependencies: ['A'] },
271
+ { id: 'C', dependencies: ['B'] },
272
+ ]
273
+
274
+ try {
275
+ topologicalSort(nodes, { throwOnCycle: true })
276
+ expect.fail('Should have thrown CycleDetectedError')
277
+ } catch (error) {
278
+ expect(error).toBeInstanceOf(CycleDetectedError)
279
+ const cycleError = error as CycleDetectedError
280
+ expect(cycleError.cyclePath).toBeDefined()
281
+ expect(cycleError.cyclePath.length).toBeGreaterThanOrEqual(3)
282
+ expect(cycleError.message).toContain('->') // Shows path format
283
+ }
284
+ })
285
+
286
+ it('should not detect cycle in valid DAG with shared dependencies', () => {
287
+ // Diamond pattern: A -> B, A -> C, B -> D, C -> D
288
+ const nodes: SortableNode[] = [
289
+ { id: 'A', dependencies: [] },
290
+ { id: 'B', dependencies: ['A'] },
291
+ { id: 'C', dependencies: ['A'] },
292
+ { id: 'D', dependencies: ['B', 'C'] },
293
+ ]
294
+
295
+ const result = topologicalSort(nodes)
296
+
297
+ expect(result.hasCycle).toBe(false)
298
+ })
299
+ })
300
+
301
+ describe('Execution Order Validation Tests', () => {
302
+ it('should ensure dependencies execute before dependents', () => {
303
+ const nodes: SortableNode[] = [
304
+ { id: 'step1', dependencies: [] },
305
+ { id: 'step2', dependencies: ['step1'] },
306
+ { id: 'step3', dependencies: ['step2'] },
307
+ { id: 'step4', dependencies: ['step3'] },
308
+ ]
309
+
310
+ const result = topologicalSort(nodes)
311
+
312
+ // For every node, all its dependencies should appear earlier in the order
313
+ for (const node of nodes) {
314
+ const nodeIndex = result.order.indexOf(node.id)
315
+ for (const dep of node.dependencies) {
316
+ const depIndex = result.order.indexOf(dep)
317
+ expect(depIndex).toBeLessThan(nodeIndex)
318
+ }
319
+ }
320
+ })
321
+
322
+ it('should identify parallel-safe ordering', () => {
323
+ const nodes: SortableNode[] = [
324
+ { id: 'A', dependencies: [] },
325
+ { id: 'B', dependencies: [] },
326
+ { id: 'C', dependencies: ['A'] },
327
+ { id: 'D', dependencies: ['B'] },
328
+ ]
329
+
330
+ const levels = getExecutionLevels(nodes)
331
+
332
+ // Level 0: A, B (can run in parallel)
333
+ // Level 1: C, D (can run in parallel after level 0)
334
+ expect(levels).toHaveLength(2)
335
+ expect(levels[0].nodes).toContain('A')
336
+ expect(levels[0].nodes).toContain('B')
337
+ expect(levels[1].nodes).toContain('C')
338
+ expect(levels[1].nodes).toContain('D')
339
+ })
340
+
341
+ it('should provide deterministic order for reproducibility', () => {
342
+ const nodes: SortableNode[] = [
343
+ { id: 'Z', dependencies: [] },
344
+ { id: 'A', dependencies: [] },
345
+ { id: 'M', dependencies: [] },
346
+ { id: 'B', dependencies: ['Z', 'A'] },
347
+ ]
348
+
349
+ // Run 10 times to verify determinism
350
+ const results: string[][] = []
351
+ for (let i = 0; i < 10; i++) {
352
+ const result = topologicalSort(nodes)
353
+ results.push(result.order)
354
+ }
355
+
356
+ // All results should be identical
357
+ for (let i = 1; i < results.length; i++) {
358
+ expect(results[i]).toEqual(results[0])
359
+ }
360
+ })
361
+
362
+ it('should sort equal-priority nodes alphabetically for stability', () => {
363
+ const nodes: SortableNode[] = [
364
+ { id: 'C', dependencies: [] },
365
+ { id: 'A', dependencies: [] },
366
+ { id: 'B', dependencies: [] },
367
+ ]
368
+
369
+ const result = topologicalSort(nodes)
370
+
371
+ // Should be sorted alphabetically since all have same priority (level 0)
372
+ expect(result.order).toEqual(['A', 'B', 'C'])
373
+ })
374
+ })
375
+
376
+ describe('getExecutionLevels() - Parallel Execution Groups', () => {
377
+ it('should group nodes by execution level', () => {
378
+ const nodes: SortableNode[] = [
379
+ { id: 'A', dependencies: [] },
380
+ { id: 'B', dependencies: ['A'] },
381
+ { id: 'C', dependencies: ['B'] },
382
+ ]
383
+
384
+ const levels = getExecutionLevels(nodes)
385
+
386
+ expect(levels).toHaveLength(3)
387
+ expect(levels[0]).toEqual({ level: 0, nodes: ['A'] })
388
+ expect(levels[1]).toEqual({ level: 1, nodes: ['B'] })
389
+ expect(levels[2]).toEqual({ level: 2, nodes: ['C'] })
390
+ })
391
+
392
+ it('should identify parallelizable batches', () => {
393
+ const nodes: SortableNode[] = [
394
+ { id: 'A', dependencies: [] },
395
+ { id: 'B', dependencies: [] },
396
+ { id: 'C', dependencies: [] },
397
+ { id: 'D', dependencies: ['A', 'B', 'C'] },
398
+ ]
399
+
400
+ const levels = getExecutionLevels(nodes)
401
+
402
+ expect(levels).toHaveLength(2)
403
+ expect(levels[0].level).toBe(0)
404
+ expect(levels[0].nodes).toHaveLength(3)
405
+ expect(levels[1].level).toBe(1)
406
+ expect(levels[1].nodes).toEqual(['D'])
407
+ })
408
+
409
+ it('should return correct levels for complex graph', () => {
410
+ // Level 0: A, B
411
+ // Level 1: C, D (C depends on A, D depends on B)
412
+ // Level 2: E (depends on C and D)
413
+ const nodes: SortableNode[] = [
414
+ { id: 'A', dependencies: [] },
415
+ { id: 'B', dependencies: [] },
416
+ { id: 'C', dependencies: ['A'] },
417
+ { id: 'D', dependencies: ['B'] },
418
+ { id: 'E', dependencies: ['C', 'D'] },
419
+ ]
420
+
421
+ const levels = getExecutionLevels(nodes)
422
+
423
+ expect(levels).toHaveLength(3)
424
+
425
+ const level0 = levels.find((l) => l.level === 0)!
426
+ const level1 = levels.find((l) => l.level === 1)!
427
+ const level2 = levels.find((l) => l.level === 2)!
428
+
429
+ expect(level0.nodes.sort()).toEqual(['A', 'B'])
430
+ expect(level1.nodes.sort()).toEqual(['C', 'D'])
431
+ expect(level2.nodes).toEqual(['E'])
432
+ })
433
+
434
+ it('should handle empty input', () => {
435
+ const levels = getExecutionLevels([])
436
+
437
+ expect(levels).toEqual([])
438
+ })
439
+
440
+ it('should sort nodes within each level for determinism', () => {
441
+ const nodes: SortableNode[] = [
442
+ { id: 'Z', dependencies: [] },
443
+ { id: 'A', dependencies: [] },
444
+ { id: 'M', dependencies: [] },
445
+ ]
446
+
447
+ const levels = getExecutionLevels(nodes)
448
+
449
+ expect(levels[0].nodes).toEqual(['A', 'M', 'Z'])
450
+ })
451
+
452
+ it('should throw on cycle when getting execution levels', () => {
453
+ const nodes: SortableNode[] = [
454
+ { id: 'A', dependencies: ['B'] },
455
+ { id: 'B', dependencies: ['A'] },
456
+ ]
457
+
458
+ expect(() => getExecutionLevels(nodes)).toThrow(CycleDetectedError)
459
+ })
460
+ })
461
+
462
+ describe('Edge Cases', () => {
463
+ it('should handle nodes with unknown dependencies (missing nodes)', () => {
464
+ const nodes: SortableNode[] = [
465
+ { id: 'A', dependencies: [] },
466
+ { id: 'B', dependencies: ['A', 'MISSING'] }, // MISSING doesn't exist
467
+ ]
468
+
469
+ // Should either throw or ignore missing dependencies
470
+ // Implementation choice: throw MissingNodeError
471
+ expect(() => topologicalSort(nodes, { strict: true })).toThrow()
472
+ })
473
+
474
+ it('should handle duplicate dependencies gracefully', () => {
475
+ const nodes: SortableNode[] = [
476
+ { id: 'A', dependencies: [] },
477
+ { id: 'B', dependencies: ['A', 'A', 'A'] }, // Duplicate deps
478
+ ]
479
+
480
+ const result = topologicalSort(nodes)
481
+
482
+ expect(result.order).toEqual(['A', 'B'])
483
+ expect(result.hasCycle).toBe(false)
484
+ })
485
+
486
+ it('should handle very deep dependency chains', () => {
487
+ const nodes: SortableNode[] = []
488
+ const depth = 100
489
+
490
+ for (let i = 0; i < depth; i++) {
491
+ nodes.push({
492
+ id: `node${i}`,
493
+ dependencies: i > 0 ? [`node${i - 1}`] : [],
494
+ })
495
+ }
496
+
497
+ const result = topologicalSort(nodes)
498
+
499
+ expect(result.hasCycle).toBe(false)
500
+ expect(result.order).toHaveLength(depth)
501
+ expect(result.order[0]).toBe('node0')
502
+ expect(result.order[depth - 1]).toBe(`node${depth - 1}`)
503
+ })
504
+
505
+ it('should handle wide graphs efficiently', () => {
506
+ const nodes: SortableNode[] = [{ id: 'root', dependencies: [] }]
507
+
508
+ // Add 100 nodes all depending on root
509
+ for (let i = 0; i < 100; i++) {
510
+ nodes.push({ id: `child${i}`, dependencies: ['root'] })
511
+ }
512
+
513
+ const result = topologicalSort(nodes)
514
+
515
+ expect(result.hasCycle).toBe(false)
516
+ expect(result.order).toHaveLength(101)
517
+ expect(result.order[0]).toBe('root')
518
+ })
519
+ })
520
+
521
+ describe('Options and Configuration', () => {
522
+ it('should support throwOnCycle option', () => {
523
+ const nodes: SortableNode[] = [
524
+ { id: 'A', dependencies: ['B'] },
525
+ { id: 'B', dependencies: ['A'] },
526
+ ]
527
+
528
+ // With throwOnCycle: false (default)
529
+ const result = topologicalSort(nodes, { throwOnCycle: false })
530
+ expect(result.hasCycle).toBe(true)
531
+
532
+ // With throwOnCycle: true
533
+ expect(() => topologicalSort(nodes, { throwOnCycle: true })).toThrow(
534
+ CycleDetectedError
535
+ )
536
+ })
537
+
538
+ it('should support algorithm selection', () => {
539
+ const nodes: SortableNode[] = [
540
+ { id: 'A', dependencies: [] },
541
+ { id: 'B', dependencies: ['A'] },
542
+ ]
543
+
544
+ const kahnResult = topologicalSort(nodes, { algorithm: 'kahn' })
545
+ const dfsResult = topologicalSort(nodes, { algorithm: 'dfs' })
546
+
547
+ expect(kahnResult.order).toEqual(['A', 'B'])
548
+ expect(dfsResult.order).toEqual(['A', 'B'])
549
+ })
550
+
551
+ it('should support strict mode for missing dependencies', () => {
552
+ const nodes: SortableNode[] = [
553
+ { id: 'A', dependencies: [] },
554
+ { id: 'B', dependencies: ['MISSING'] },
555
+ ]
556
+
557
+ // Strict mode should throw
558
+ expect(() => topologicalSort(nodes, { strict: true })).toThrow()
559
+
560
+ // Non-strict should ignore missing deps
561
+ const result = topologicalSort(nodes, { strict: false })
562
+ expect(result.order).toContain('A')
563
+ expect(result.order).toContain('B')
564
+ })
565
+ })
566
+ })
567
+
568
+ describe('CycleDetectedError', () => {
569
+ it('should contain cycle path', () => {
570
+ const error = new CycleDetectedError(['A', 'B', 'C', 'A'])
571
+
572
+ expect(error.cyclePath).toEqual(['A', 'B', 'C', 'A'])
573
+ })
574
+
575
+ it('should format message with cycle path', () => {
576
+ const error = new CycleDetectedError(['A', 'B', 'C', 'A'])
577
+
578
+ expect(error.message).toContain('A -> B -> C -> A')
579
+ })
580
+
581
+ it('should have correct error name', () => {
582
+ const error = new CycleDetectedError(['A', 'B', 'A'])
583
+
584
+ expect(error.name).toBe('CycleDetectedError')
585
+ })
586
+ })