import { describe, it, expect } from 'vitest'; import { DependencyGraph } from '../graph'; /** * Helper: build a graph from "cell <- precedent, precedent" lines. * Node names are arbitrary strings here (real code uses `keyOf`). */ function build(edges: Record): DependencyGraph { const g = new DependencyGraph(); for (const [cell, precs] of Object.entries(edges)) g.setPrecedents(cell, precs); return g; } describe('DependencyGraph.affected', () => { it('returns dirty cells plus their transitive dependents', () => { // A -> B -> C ; A -> D const g = build({ B: ['A'], C: ['B'], D: ['A'] }); expect([...g.affected(['A'])].sort()).toEqual(['A', 'B', 'C', 'D']); expect([...g.affected(['B'])].sort()).toEqual(['B', 'C']); }); }); describe('DependencyGraph.topoOrder', () => { it('orders precedents before dependents', () => { const g = build({ B: ['A'], C: ['A', 'B'], D: ['C'] }); const { order, circular } = g.topoOrder(g.affected(['A'])); expect(circular.size).toBe(0); const pos = (n: string) => order.indexOf(n); expect(pos('A')).toBeLessThan(pos('B')); expect(pos('B')).toBeLessThan(pos('C')); expect(pos('C')).toBeLessThan(pos('D')); }); it('detects a direct self-reference', () => { const g = build({ A: ['A'] }); const { circular } = g.topoOrder(g.affected(['A'])); expect([...circular]).toEqual(['A']); }); it('detects a 2-cycle and flags both nodes', () => { const g = build({ A: ['B'], B: ['A'] }); const { order, circular } = g.topoOrder(g.affected(['A'])); expect([...circular].sort()).toEqual(['A', 'B']); expect(order).toEqual([]); }); it('detects a longer cycle, leaves acyclic tail evaluable', () => { // cycle A->B->C->A ; plus D reads C (D is not on the cycle) const g = build({ A: ['C'], B: ['A'], C: ['B'], D: ['C'] }); const { order, circular } = g.topoOrder(g.affected(['A'])); expect([...circular].sort()).toEqual(['A', 'B', 'C']); expect(order).toEqual(['D']); }); it('rewiring precedents removes stale edges', () => { const g = build({ B: ['A'] }); expect([...g.affected(['A'])].sort()).toEqual(['A', 'B']); g.setPrecedents('B', []); // B no longer reads A expect([...g.affected(['A'])].sort()).toEqual(['A']); }); });