from myfasthtml.core.network_utils import from_nested_dict, from_tree_with_metadata, from_parent_child_list class TestFromNestedDict: def test_i_can_convert_single_root_node(self): """Test conversion of a single root node without children.""" trees = [{"root": {}}] nodes, edges = from_nested_dict(trees) assert len(nodes) == 1 assert nodes[0] == {"id": 1, "label": "root"} assert len(edges) == 0 def test_i_can_convert_tree_with_one_level_children(self): """Test conversion with direct children, verifying edge creation.""" trees = [{"root": {"child1": {}, "child2": {}}}] nodes, edges = from_nested_dict(trees) assert len(nodes) == 3 assert nodes[0] == {"id": 1, "label": "root"} assert nodes[1] == {"id": 2, "label": "child1"} assert nodes[2] == {"id": 3, "label": "child2"} assert len(edges) == 2 assert {"from": 1, "to": 2} in edges assert {"from": 1, "to": 3} in edges def test_i_can_convert_tree_with_multiple_levels(self): """Test recursive conversion with multiple levels of nesting.""" trees = [ { "root": { "child1": { "grandchild1": {}, "grandchild2": {} }, "child2": {} } } ] nodes, edges = from_nested_dict(trees) assert len(nodes) == 5 assert len(edges) == 4 # Verify hierarchy assert {"from": 1, "to": 2} in edges # root -> child1 assert {"from": 1, "to": 5} in edges # root -> child2 assert {"from": 2, "to": 3} in edges # child1 -> grandchild1 assert {"from": 2, "to": 4} in edges # child1 -> grandchild2 def test_i_can_generate_auto_incremented_ids(self): """Test that IDs start at 1 and increment correctly.""" trees = [{"a": {"b": {"c": {}}}}] nodes, edges = from_nested_dict(trees) ids = [node["id"] for node in nodes] assert ids == [1, 2, 3] def test_i_can_use_dict_keys_as_labels(self): """Test that dictionary keys become node labels.""" trees = [{"RootNode": {"ChildNode": {}}}] nodes, edges = from_nested_dict(trees) assert nodes[0]["label"] == "RootNode" assert nodes[1]["label"] == "ChildNode" def test_i_can_convert_empty_list(self): """Test that empty list returns empty nodes and edges.""" trees = [] nodes, edges = from_nested_dict(trees) assert nodes == [] assert edges == [] def test_i_can_convert_multiple_root_nodes(self): """Test conversion with multiple independent trees.""" trees = [ {"root1": {"child1": {}}}, {"root2": {"child2": {}}} ] nodes, edges = from_nested_dict(trees) assert len(nodes) == 4 assert nodes[0] == {"id": 1, "label": "root1"} assert nodes[1] == {"id": 2, "label": "child1"} assert nodes[2] == {"id": 3, "label": "root2"} assert nodes[3] == {"id": 4, "label": "child2"} # Verify edges connect within trees, not across assert len(edges) == 2 assert {"from": 1, "to": 2} in edges assert {"from": 3, "to": 4} in edges def test_i_can_maintain_id_sequence_across_multiple_trees(self): """Test that ID counter continues across multiple trees.""" trees = [ {"tree1": {}}, {"tree2": {}}, {"tree3": {}} ] nodes, edges = from_nested_dict(trees) ids = [node["id"] for node in nodes] assert ids == [1, 2, 3] class TestFromTreeWithMetadata: def test_i_can_convert_single_node_with_metadata(self): """Test basic conversion with explicit id and label.""" trees = [{"id": "root", "label": "Root Node"}] nodes, edges = from_tree_with_metadata(trees) assert len(nodes) == 1 assert nodes[0] == {"id": "root", "label": "Root Node"} assert len(edges) == 0 def test_i_can_preserve_string_ids_from_metadata(self): """Test that string IDs from the tree are preserved.""" trees = [ { "id": "root_id", "label": "Root", "children": [ {"id": "child_id", "label": "Child"} ] } ] nodes, edges = from_tree_with_metadata(trees) assert nodes[0]["id"] == "root_id" assert nodes[1]["id"] == "child_id" assert edges[0] == {"from": "root_id", "to": "child_id"} def test_i_can_auto_increment_when_id_is_missing(self): """Test fallback to auto-increment when ID is not provided.""" trees = [ { "label": "Root", "children": [ {"label": "Child1"}, {"id": "child2_id", "label": "Child2"} ] } ] nodes, edges = from_tree_with_metadata(trees) assert nodes[0]["id"] == 1 # auto-incremented assert nodes[1]["id"] == 2 # auto-incremented assert nodes[2]["id"] == "child2_id" # preserved def test_i_can_convert_tree_with_children(self): """Test handling of children list.""" trees = [ { "id": "root", "label": "Root", "children": [ { "id": "child1", "label": "Child 1", "children": [ {"id": "grandchild", "label": "Grandchild"} ] }, {"id": "child2", "label": "Child 2"} ] } ] nodes, edges = from_tree_with_metadata(trees) assert len(nodes) == 4 assert len(edges) == 3 assert {"from": "root", "to": "child1"} in edges assert {"from": "root", "to": "child2"} in edges assert {"from": "child1", "to": "grandchild"} in edges def test_i_can_use_custom_id_getter(self): """Test custom callback for extracting node ID.""" trees = [ { "node_id": "custom_root", "label": "Root" } ] def custom_id_getter(node): return node.get("node_id") nodes, edges = from_tree_with_metadata( trees, id_getter=custom_id_getter ) assert nodes[0]["id"] == "custom_root" def test_i_can_use_custom_label_getter(self): """Test custom callback for extracting node label.""" trees = [ { "id": "root", "name": "Custom Label" } ] def custom_label_getter(node): return node.get("name", "") nodes, edges = from_tree_with_metadata( trees, label_getter=custom_label_getter ) assert nodes[0]["label"] == "Custom Label" def test_i_can_use_custom_children_getter(self): """Test custom callback for extracting children.""" trees = [ { "id": "root", "label": "Root", "kids": [ {"id": "child", "label": "Child"} ] } ] def custom_children_getter(node): return node.get("kids", []) nodes, edges = from_tree_with_metadata( trees, children_getter=custom_children_getter ) assert len(nodes) == 2 assert nodes[1]["id"] == "child" def test_i_can_handle_missing_label_with_default(self): """Test that missing label returns empty string.""" trees = [{"id": "root"}] nodes, edges = from_tree_with_metadata(trees) assert nodes[0]["label"] == "" def test_i_can_handle_missing_children_with_default(self): """Test that missing children returns empty list (no children processed).""" trees = [{"id": "root", "label": "Root"}] nodes, edges = from_tree_with_metadata(trees) assert len(nodes) == 1 assert len(edges) == 0 def test_i_can_convert_multiple_root_trees(self): """Test conversion with multiple independent trees with metadata.""" trees = [ { "id": "root1", "label": "Root 1", "children": [ {"id": "child1", "label": "Child 1"} ] }, { "id": "root2", "label": "Root 2", "children": [ {"id": "child2", "label": "Child 2"} ] } ] nodes, edges = from_tree_with_metadata(trees) assert len(nodes) == 4 assert nodes[0]["id"] == "root1" assert nodes[1]["id"] == "child1" assert nodes[2]["id"] == "root2" assert nodes[3]["id"] == "child2" # Verify edges connect within trees, not across assert len(edges) == 2 assert {"from": "root1", "to": "child1"} in edges assert {"from": "root2", "to": "child2"} in edges def test_i_can_maintain_id_counter_across_multiple_trees_when_missing_ids(self): """Test that auto-increment counter continues across multiple trees.""" trees = [ {"label": "Tree1"}, {"label": "Tree2"}, {"label": "Tree3"} ] nodes, edges = from_tree_with_metadata(trees) assert nodes[0]["id"] == 1 assert nodes[1]["id"] == 2 assert nodes[2]["id"] == 3 def test_i_can_convert_empty_list(self): """Test that empty list returns empty nodes and edges.""" trees = [] nodes, edges = from_tree_with_metadata(trees) assert nodes == [] assert edges == [] class TestFromParentChildList: def test_i_can_convert_single_root_node_without_parent(self): """Test conversion of a single root node without parent.""" items = [{"id": "root", "label": "Root"}] nodes, edges = from_parent_child_list(items) assert len(nodes) == 1 assert nodes[0] == {"id": "root", "label": "Root"} assert len(edges) == 0 def test_i_can_convert_simple_parent_child_relationship(self): """Test conversion with basic parent-child relationship.""" items = [ {"id": "root", "label": "Root"}, {"id": "child", "parent": "root", "label": "Child"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 2 assert {"id": "root", "label": "Root"} in nodes assert {"id": "child", "label": "Child"} in nodes assert len(edges) == 1 assert edges[0] == {"from": "root", "to": "child"} def test_i_can_convert_multiple_children_with_same_parent(self): """Test that one parent can have multiple children.""" items = [ {"id": "root", "label": "Root"}, {"id": "child1", "parent": "root", "label": "Child 1"}, {"id": "child2", "parent": "root", "label": "Child 2"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 3 assert len(edges) == 2 assert {"from": "root", "to": "child1"} in edges assert {"from": "root", "to": "child2"} in edges def test_i_can_convert_multi_level_hierarchy(self): """Test conversion with multiple levels (root -> child -> grandchild).""" items = [ {"id": "root", "label": "Root"}, {"id": "child", "parent": "root", "label": "Child"}, {"id": "grandchild", "parent": "child", "label": "Grandchild"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 3 assert len(edges) == 2 assert {"from": "root", "to": "child"} in edges assert {"from": "child", "to": "grandchild"} in edges def test_i_can_handle_parent_none_as_root(self): """Test that parent=None identifies a root node.""" items = [ {"id": "root", "parent": None, "label": "Root"}, {"id": "child", "parent": "root", "label": "Child"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 2 assert len(edges) == 1 assert edges[0] == {"from": "root", "to": "child"} def test_i_can_handle_parent_empty_string_as_root(self): """Test that parent='' identifies a root node.""" items = [ {"id": "root", "parent": "", "label": "Root"}, {"id": "child", "parent": "root", "label": "Child"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 2 assert len(edges) == 1 assert edges[0] == {"from": "root", "to": "child"} def test_i_can_create_ghost_node_for_missing_parent(self): """Test automatic creation of ghost node when parent doesn't exist.""" items = [ {"id": "child", "parent": "missing_parent", "label": "Child"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 2 # Find the ghost node ghost_node = [n for n in nodes if n["id"] == "missing_parent"][0] assert ghost_node is not None assert len(edges) == 1 assert edges[0] == {"from": "missing_parent", "to": "child"} def test_i_can_apply_ghost_color_to_missing_parent(self): """Test that ghost nodes have the default ghost color.""" items = [ {"id": "child", "parent": "ghost", "label": "Child"} ] nodes, edges = from_parent_child_list(items) ghost_node = [n for n in nodes if n["id"] == "ghost"][0] assert "color" in ghost_node assert ghost_node["color"] == "#ff9999" def test_i_can_use_custom_ghost_color(self): """Test that custom ghost_color parameter is applied.""" items = [ {"id": "child", "parent": "ghost", "label": "Child"} ] nodes, edges = from_parent_child_list(items, ghost_color="#0000ff") ghost_node = [n for n in nodes if n["id"] == "ghost"][0] assert ghost_node["color"] == "#0000ff" def test_i_can_create_multiple_ghost_nodes(self): """Test handling of multiple missing parents.""" items = [ {"id": "child1", "parent": "ghost1", "label": "Child 1"}, {"id": "child2", "parent": "ghost2", "label": "Child 2"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 4 # 2 real + 2 ghost ghost_ids = [n["id"] for n in nodes if "color" in n] assert "ghost1" in ghost_ids assert "ghost2" in ghost_ids def test_i_can_avoid_duplicate_ghost_nodes(self): """Test that same missing parent creates only one ghost node.""" items = [ {"id": "child1", "parent": "ghost", "label": "Child 1"}, {"id": "child2", "parent": "ghost", "label": "Child 2"} ] nodes, edges = from_parent_child_list(items) assert len(nodes) == 3 # 2 real + 1 ghost ghost_nodes = [n for n in nodes if n["id"] == "ghost"] assert len(ghost_nodes) == 1 assert len(edges) == 2 assert {"from": "ghost", "to": "child1"} in edges assert {"from": "ghost", "to": "child2"} in edges def test_i_can_use_custom_id_getter(self): """Test custom callback for extracting node ID.""" items = [ {"node_id": "root", "label": "Root"} ] def custom_id_getter(item): return item.get("node_id") nodes, edges = from_parent_child_list( items, id_getter=custom_id_getter ) assert nodes[0]["id"] == "root" def test_i_can_use_custom_label_getter(self): """Test custom callback for extracting node label.""" items = [ {"id": "root", "name": "Custom Label"} ] def custom_label_getter(item): return item.get("name", "") nodes, edges = from_parent_child_list( items, label_getter=custom_label_getter ) assert nodes[0]["label"] == "Custom Label" def test_i_can_use_custom_parent_getter(self): """Test custom callback for extracting parent ID.""" items = [ {"id": "root", "label": "Root"}, {"id": "child", "parent_id": "root", "label": "Child"} ] def custom_parent_getter(item): return item.get("parent_id") nodes, edges = from_parent_child_list( items, parent_getter=custom_parent_getter ) assert len(edges) == 1 assert edges[0] == {"from": "root", "to": "child"} def test_i_can_handle_empty_list(self): """Test that empty list returns empty nodes and edges.""" items = [] nodes, edges = from_parent_child_list(items) assert nodes == [] assert edges == [] def test_i_can_use_id_as_label_for_ghost_nodes(self): """Test that ghost nodes use their ID as label by default.""" items = [ {"id": "child", "parent": "ghost_parent", "label": "Child"} ] nodes, edges = from_parent_child_list(items) ghost_node = [n for n in nodes if n["id"] == "ghost_parent"][0] assert ghost_node["label"] == "ghost_parent"