package main import ( "context" "os" "path/filepath" "strings" "testing" "time" ) func TestPrepareHierarchyLayoutRequestNormalizesEdges(t *testing.T) { nodes := []Node{ {ID: 0, TermType: "uri", IRI: "http://example.com/root"}, {ID: 1, TermType: "uri", IRI: "http://example.com/child"}, {ID: 2, TermType: "uri", IRI: "http://example.com/leaf"}, } preds := NewPredicateDict([]string{"http://www.w3.org/2000/01/rdf-schema#subClassOf"}) edges := []Edge{ {Source: 1, Target: 0, PredicateID: 0}, {Source: 1, Target: 0, PredicateID: 0}, {Source: 2, Target: 2, PredicateID: 0}, {Source: 2, Target: 1, PredicateID: 0}, } prepared := prepareHierarchyLayoutRequest("http://example.com/root", nodes, edges, preds) if got, want := len(prepared.Request.Nodes), 3; got != want { t.Fatalf("len(request.nodes)=%d want %d", got, want) } if got, want := len(prepared.Request.Edges), 2; got != want { t.Fatalf("len(request.edges)=%d want %d", got, want) } if prepared.Request.Edges[0].ParentID != 0 || prepared.Request.Edges[0].ChildID != 1 { t.Fatalf("first normalized edge = %+v, want parent=0 child=1", prepared.Request.Edges[0]) } if prepared.Request.Edges[1].ParentID != 1 || prepared.Request.Edges[1].ChildID != 2 { t.Fatalf("second normalized edge = %+v, want parent=1 child=2", prepared.Request.Edges[1]) } if prepared.Request.Edges[0].PredicateIRI == nil || *prepared.Request.Edges[0].PredicateIRI == "" { t.Fatalf("expected predicate iri to be preserved") } } func TestApplyHierarchyLayoutResponsePreservesIDsAndRemapsRoutes(t *testing.T) { nodes := []Node{ {ID: 0, TermType: "uri", IRI: "http://example.com/root"}, {ID: 1, TermType: "uri", IRI: "http://example.com/child"}, {ID: 2, TermType: "uri", IRI: "http://example.com/leaf"}, } normalizedEdges := []Edge{ {Source: 1, Target: 0, PredicateID: 0}, {Source: 2, Target: 0, PredicateID: 0}, } response := hierarchyLayoutResponse{ Nodes: []hierarchyLayoutResponseNode{ {NodeID: 0, X: 10, Y: 20}, {NodeID: 2, X: 30, Y: 40}, }, RouteSegments: []hierarchyLayoutResponseRouteSegment{ { EdgeIndex: 1, Kind: "spiral", Points: []hierarchyLayoutResponseRoutePoint{ {X: 10, Y: 20}, {X: 30, Y: 40}, }, }, }, } result, err := applyHierarchyLayoutResponse(nodes, normalizedEdges, response) if err != nil { t.Fatalf("applyHierarchyLayoutResponse returned error: %v", err) } if got, want := len(result.Nodes), 2; got != want { t.Fatalf("len(nodes)=%d want %d", got, want) } if result.Nodes[0].ID != 0 || result.Nodes[1].ID != 2 { t.Fatalf("filtered node ids = [%d %d], want [0 2]", result.Nodes[0].ID, result.Nodes[1].ID) } if result.Nodes[0].X != 10 || result.Nodes[0].Y != 20 || result.Nodes[1].X != 30 || result.Nodes[1].Y != 40 { t.Fatalf("positions were not applied to filtered nodes: %+v", result.Nodes) } if got, want := len(result.Edges), 1; got != want { t.Fatalf("len(edges)=%d want %d", got, want) } if result.Edges[0] != normalizedEdges[1] { t.Fatalf("filtered edge = %+v, want %+v", result.Edges[0], normalizedEdges[1]) } if got, want := len(result.RouteSegments), 1; got != want { t.Fatalf("len(route_segments)=%d want %d", got, want) } if result.RouteSegments[0].EdgeIndex != 0 { t.Fatalf("route edge index = %d want 0", result.RouteSegments[0].EdgeIndex) } } func TestRunHierarchyLayoutBridgeUsesConfiguredWorkingDirectory(t *testing.T) { tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "pwd.txt") scriptPath := filepath.Join(tmpDir, "bridge.sh") script := "#!/bin/sh\npwd > \"" + outputPath + "\"\ncat >/dev/null\nprintf '{\"nodes\":[{\"node_id\":1,\"x\":10,\"y\":20,\"level\":0}],\"route_segments\":[]}'\n" if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil { t.Fatalf("write script: %v", err) } cfg := Config{ HierarchyLayoutBridgeBin: scriptPath, HierarchyLayoutBridgeWorkdir: tmpDir, HierarchyLayoutTimeout: 2 * time.Second, } response, err := runHierarchyLayoutBridge(context.Background(), cfg, hierarchyLayoutRequest{ RootIRI: "root", Nodes: []hierarchyLayoutRequestNode{ {NodeID: 1, IRI: "root"}, }, }) if err != nil { t.Fatalf("runHierarchyLayoutBridge returned error: %v", err) } if got, want := len(response.Nodes), 1; got != want { t.Fatalf("len(response.nodes)=%d want %d", got, want) } pwdBytes, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("read pwd output: %v", err) } if got, want := strings.TrimSpace(string(pwdBytes)), tmpDir; got != want { t.Fatalf("bridge working directory=%q want %q", got, want) } } func TestRunHierarchyLayoutBridgeReturnsSvgWriteFailure(t *testing.T) { tmpDir := t.TempDir() scriptPath := filepath.Join(tmpDir, "bridge_fail.sh") script := "#!/bin/sh\ncat >/dev/null\necho 'failed to write SVG output: permission denied' >&2\nexit 1\n" if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil { t.Fatalf("write script: %v", err) } cfg := Config{ HierarchyLayoutBridgeBin: scriptPath, HierarchyLayoutBridgeWorkdir: tmpDir, HierarchyLayoutTimeout: 2 * time.Second, } _, err := runHierarchyLayoutBridge(context.Background(), cfg, hierarchyLayoutRequest{ RootIRI: "root", }) if err == nil { t.Fatalf("expected hierarchy layout bridge error") } if !strings.Contains(err.Error(), "failed to write SVG output") { t.Fatalf("error=%q does not mention SVG write failure", err) } }