dag.test.js (4036B)
1 // test/dag.test.js - dependency graph traversal 2 // 3 // These cover the two scheduling defects carried over from the prototype: 4 // serialized dispatch, and a failure cascade that only skipped direct 5 // dependents. 6 7 import test from 'node:test'; 8 import assert from 'node:assert/strict'; 9 import { topologicalOrder, depths, transitiveDependents, runnable, CycleError } from '../src/lib/pipeline/dag.js'; 10 11 // a -> b -> d, a -> c -> d, plus an unrelated island. 12 const GRAPH = [ 13 { name: 'a', needs: [] }, 14 { name: 'b', needs: ['a'] }, 15 { name: 'c', needs: ['a'] }, 16 { name: 'd', needs: ['b', 'c'] }, 17 { name: 'island', needs: [] }, 18 ]; 19 20 function states(overrides = {}) { 21 const map = new Map(GRAPH.map((j) => [j.name, 'queued'])); 22 for (const [k, v] of Object.entries(overrides)) map.set(k, v); 23 return map; 24 } 25 26 test('topological order places dependencies first', () => { 27 const order = topologicalOrder(GRAPH).map((j) => j.name); 28 assert.ok(order.indexOf('a') < order.indexOf('b')); 29 assert.ok(order.indexOf('a') < order.indexOf('c')); 30 assert.ok(order.indexOf('b') < order.indexOf('d')); 31 assert.ok(order.indexOf('c') < order.indexOf('d')); 32 }); 33 34 test('topological order is deterministic', () => { 35 const first = topologicalOrder(GRAPH).map((j) => j.name); 36 const shuffled = [...GRAPH].reverse(); 37 assert.deepEqual(topologicalOrder(shuffled).map((j) => j.name), first); 38 }); 39 40 test('a cycle is detected and named', () => { 41 const cyclic = [ 42 { name: 'x', needs: ['z'] }, 43 { name: 'y', needs: ['x'] }, 44 { name: 'z', needs: ['y'] }, 45 ]; 46 assert.throws(() => topologicalOrder(cyclic), CycleError); 47 try { 48 topologicalOrder(cyclic); 49 } catch (e) { 50 // The reported path returns to where it started. 51 assert.equal(e.cycle[0], e.cycle[e.cycle.length - 1]); 52 } 53 }); 54 55 test('depth reflects the longest path, not insertion order', () => { 56 const d = depths(GRAPH); 57 assert.equal(d.get('a'), 0); 58 assert.equal(d.get('island'), 0); 59 assert.equal(d.get('b'), 1); 60 assert.equal(d.get('d'), 2); 61 }); 62 63 test('independent tasks are runnable at the same time', () => { 64 // The prototype could only ever return one task here. 65 const ready = runnable(GRAPH, states()).map((j) => j.name); 66 assert.deepEqual(ready.sort(), ['a', 'island']); 67 }); 68 69 test('a task becomes runnable only once every dependency succeeds', () => { 70 assert.deepEqual( 71 runnable(GRAPH, states({ a: 'success', b: 'success' })).map((j) => j.name).sort(), 72 ['c', 'island'] 73 ); 74 assert.deepEqual( 75 runnable(GRAPH, states({ a: 'success', b: 'success', c: 'success', island: 'success' })).map((j) => j.name), 76 ['d'] 77 ); 78 }); 79 80 test('a running dependency does not release its dependents', () => { 81 assert.equal(runnable(GRAPH, states({ a: 'running' })).some((j) => j.name === 'b'), false); 82 }); 83 84 test('failure skips transitive dependents, not just direct ones', () => { 85 // This is the prototype bug: it would have skipped only b and c, leaving 86 // d queued and dispatchable with its inputs missing. 87 const skipped = transitiveDependents(GRAPH, ['a']); 88 assert.deepEqual([...skipped].sort(), ['b', 'c', 'd']); 89 assert.equal(skipped.has('island'), false); 90 }); 91 92 test('a skipped dependency never releases a dependent', () => { 93 assert.equal(runnable(GRAPH, states({ a: 'failed' })).some((j) => j.name === 'b'), false); 94 assert.equal(runnable(GRAPH, states({ a: 'skipped' })).some((j) => j.name === 'b'), false); 95 }); 96 97 test('allowed failures can satisfy a dependency when asked to', () => { 98 const ready = runnable(GRAPH, states({ a: 'failed' }), { satisfied: ['success', 'failed'] }); 99 assert.deepEqual(ready.map((j) => j.name).sort(), ['b', 'c', 'island']); 100 }); 101 102 test('transitive dependents of a leaf is empty', () => { 103 assert.equal(transitiveDependents(GRAPH, ['d']).size, 0); 104 }); 105 106 test('graph helpers tolerate dependencies outside the set', () => { 107 const partial = [{ name: 'only', needs: ['absent'] }]; 108 assert.deepEqual(topologicalOrder(partial).map((j) => j.name), ['only']); 109 assert.equal(depths(partial).get('only'), 0); 110 });