package graph import ( "testing" ) func TestLayerByLongestPathRootsAndChildren(t *testing.T) { // Simple 2-layer tree: // work, dev (roots) // foo (under dev), bar (under work) nodes := []Node{ {ID: "w", Slug: "work"}, {ID: "d", Slug: "dev"}, {ID: "foo", Slug: "foo", ParentIDs: []string{"d"}}, {ID: "bar", Slug: "bar", ParentIDs: []string{"w"}}, } layers, err := LayerByLongestPath(nodes) if err != nil { t.Fatalf("LayerByLongestPath: %v", err) } for id, want := range map[string]int{"w": 0, "d": 0, "foo": 1, "bar": 1} { if layers[id] != want { t.Errorf("layer[%s] = %d, want %d", id, layers[id], want) } } } // Multi-parent: paliad under both work and dev. Its layer must be 1. func TestLayerByLongestPathMultiParent(t *testing.T) { nodes := []Node{ {ID: "w", Slug: "work"}, {ID: "d", Slug: "dev"}, {ID: "p", Slug: "paliad", ParentIDs: []string{"w", "d"}}, } layers, err := LayerByLongestPath(nodes) if err != nil { t.Fatalf("LayerByLongestPath: %v", err) } if layers["p"] != 1 { t.Errorf("multi-parent paliad layer = %d, want 1", layers["p"]) } } // A grandchild whose grandparent is two hops up should land at layer 2 — // LongestPath, not shortest path. func TestLayerByLongestPathRespectsLongerPath(t *testing.T) { // Graph: // r0 (root) // ├── a // │ └── x (depth-2 via a) // └── x (also direct child via r0 — would be depth-1) // LongestPath → x should be at layer 2. nodes := []Node{ {ID: "r0", Slug: "r0"}, {ID: "a", Slug: "a", ParentIDs: []string{"r0"}}, {ID: "x", Slug: "x", ParentIDs: []string{"r0", "a"}}, } layers, _ := LayerByLongestPath(nodes) if layers["x"] != 2 { t.Errorf("longest-path: x = %d, want 2", layers["x"]) } } // OrderInLayer must sort by slug for deterministic rendering. func TestOrderInLayerSortsBySlug(t *testing.T) { nodes := []Node{ {ID: "1", Slug: "bravo"}, {ID: "2", Slug: "alpha"}, {ID: "3", Slug: "charlie"}, } layers := map[string]int{"1": 0, "2": 0, "3": 0} out := OrderInLayer(nodes, layers) if len(out) != 1 || len(out[0]) != 3 { t.Fatalf("OrderInLayer output shape unexpected: %v", out) } want := []string{"2", "1", "3"} // alpha, bravo, charlie for i, id := range want { if out[0][i] != id { t.Errorf("layer[0][%d] = %s, want %s (slug order)", i, out[0][i], id) } } } func TestComputeProducesEdgesAndPositions(t *testing.T) { nodes := []Node{ {ID: "w", Slug: "work"}, {ID: "d", Slug: "dev"}, {ID: "p", Slug: "paliad", ParentIDs: []string{"w", "d"}}, } out, err := Compute(nodes, Opts{}) if err != nil { t.Fatalf("Compute: %v", err) } if len(out.Positions) != 3 { t.Errorf("expected 3 positions, got %d", len(out.Positions)) } if len(out.Edges) != 2 { t.Errorf("expected 2 edges (paliad↔work + paliad↔dev), got %d", len(out.Edges)) } // Paliad sits below both work and dev → its Y should exceed both parents'. pp := out.Positions["p"] if !(pp.Y > out.Positions["w"].Y && pp.Y > out.Positions["d"].Y) { t.Errorf("paliad Y=%.1f, expected below work=%.1f and dev=%.1f", pp.Y, out.Positions["w"].Y, out.Positions["d"].Y) } // Edges should connect parent center-bottom to child center-top. for _, e := range out.Edges { if e.SourceY >= e.TargetY { t.Errorf("edge %s→%s: source Y=%.1f should be above target Y=%.1f", e.ParentID, e.ChildID, e.SourceY, e.TargetY) } } } func TestComputeEmptyInputDoesNotPanic(t *testing.T) { out, err := Compute(nil, Opts{}) if err != nil { t.Fatalf("Compute(nil): %v", err) } if out == nil || len(out.Positions) != 0 { t.Fatalf("expected empty layout, got %+v", out) } } // Cycle guard: depth-cap returns error rather than blowing the stack. func TestLayerByLongestPathCycleErrors(t *testing.T) { // a -> b -> a (cycle). LayerByLongestPath should bail out cleanly. nodes := []Node{ {ID: "a", Slug: "a", ParentIDs: []string{"b"}}, {ID: "b", Slug: "b", ParentIDs: []string{"a"}}, } if _, err := LayerByLongestPath(nodes); err == nil { t.Fatalf("expected error on cycle, got nil") } }