|
1 | 1 | import asyncio |
| 2 | +import hashlib |
2 | 3 | import json |
3 | 4 | import tempfile |
| 5 | +import uuid as uuid_module |
4 | 6 |
|
5 | 7 | from pathlib import Path |
6 | 8 | from unittest.mock import AsyncMock, patch |
|
10 | 12 | from deepfabric.graph import ( |
11 | 13 | Graph, |
12 | 14 | GraphConfig, |
| 15 | + GraphMetadata, |
13 | 16 | GraphModel, |
14 | 17 | Node, |
15 | 18 | NodeModel, |
@@ -72,6 +75,51 @@ def test_node_to_pydantic(self): |
72 | 75 | assert child_model.children == [] |
73 | 76 | assert child_model.parents == [1] |
74 | 77 |
|
| 78 | + def test_node_uuid_generation(self): |
| 79 | + """Test that nodes automatically get a UUID4 in metadata.""" |
| 80 | + node = Node("Test topic", 42) |
| 81 | + assert "uuid" in node.metadata |
| 82 | + # Verify it's a valid UUID4 format |
| 83 | + parsed_uuid = uuid_module.UUID(node.metadata["uuid"]) |
| 84 | + assert parsed_uuid.version == 4 # noqa: PLR2004 |
| 85 | + |
| 86 | + def test_node_topic_hash_generation(self): |
| 87 | + """Test that nodes automatically get a SHA256 topic_hash in metadata.""" |
| 88 | + topic = "Test topic for hashing" |
| 89 | + node = Node(topic, 42) |
| 90 | + assert "topic_hash" in node.metadata |
| 91 | + # Verify it matches the expected SHA256 hash |
| 92 | + expected_hash = hashlib.sha256(topic.encode("utf-8")).hexdigest() |
| 93 | + assert node.metadata["topic_hash"] == expected_hash |
| 94 | + # SHA256 hex digest is 64 characters |
| 95 | + assert len(node.metadata["topic_hash"]) == 64 # noqa: PLR2004 |
| 96 | + |
| 97 | + def test_node_preserves_existing_metadata(self): |
| 98 | + """Test that existing metadata is not overwritten during node creation.""" |
| 99 | + existing_uuid = "custom-uuid-value" |
| 100 | + existing_hash = "custom-hash-value" |
| 101 | + existing_metadata = { |
| 102 | + "uuid": existing_uuid, |
| 103 | + "topic_hash": existing_hash, |
| 104 | + "custom_field": "custom_value", |
| 105 | + } |
| 106 | + node = Node("Test topic", 42, metadata=existing_metadata) |
| 107 | + |
| 108 | + # Existing values should be preserved |
| 109 | + assert node.metadata["uuid"] == existing_uuid |
| 110 | + assert node.metadata["topic_hash"] == existing_hash |
| 111 | + assert node.metadata["custom_field"] == "custom_value" |
| 112 | + |
| 113 | + def test_node_metadata_in_pydantic(self): |
| 114 | + """Test that uuid and topic_hash are included in Pydantic conversion.""" |
| 115 | + node = Node("Test topic", 42) |
| 116 | + pydantic_model = node.to_pydantic() |
| 117 | + |
| 118 | + assert "uuid" in pydantic_model.metadata |
| 119 | + assert "topic_hash" in pydantic_model.metadata |
| 120 | + assert pydantic_model.metadata["uuid"] == node.metadata["uuid"] |
| 121 | + assert pydantic_model.metadata["topic_hash"] == node.metadata["topic_hash"] |
| 122 | + |
75 | 123 |
|
76 | 124 | class TestGraphConfig: |
77 | 125 | """Tests for GraphConfig model.""" |
@@ -194,6 +242,49 @@ def test_to_pydantic(self, topic_graph): |
194 | 242 | assert pydantic_model.nodes[0].topic == "Test root topic" |
195 | 243 | assert pydantic_model.nodes[1].topic == "Child" |
196 | 244 |
|
| 245 | + def test_graph_metadata_serialization(self, topic_graph): |
| 246 | + """Test that graph-level metadata is included in Pydantic conversion.""" |
| 247 | + pydantic_model = topic_graph.to_pydantic() |
| 248 | + |
| 249 | + # Verify metadata is present |
| 250 | + assert pydantic_model.metadata is not None |
| 251 | + assert isinstance(pydantic_model.metadata, GraphMetadata) |
| 252 | + |
| 253 | + # Verify all required fields are present |
| 254 | + assert pydantic_model.metadata.provider == "openai" |
| 255 | + assert pydantic_model.metadata.model == "test-model" |
| 256 | + assert pydantic_model.metadata.temperature == 0.7 # noqa: PLR2004 |
| 257 | + assert pydantic_model.metadata.created_at is not None |
| 258 | + |
| 259 | + # Verify created_at is ISO 8601 format (contains 'T' separator) |
| 260 | + assert "T" in pydantic_model.metadata.created_at |
| 261 | + |
| 262 | + def test_graph_metadata_in_json(self, topic_graph): |
| 263 | + """Test that graph-level metadata appears in JSON output.""" |
| 264 | + json_str = topic_graph.to_json() |
| 265 | + data = json.loads(json_str) |
| 266 | + |
| 267 | + assert "metadata" in data |
| 268 | + assert data["metadata"]["provider"] == "openai" |
| 269 | + assert data["metadata"]["model"] == "test-model" |
| 270 | + assert data["metadata"]["temperature"] == 0.7 # noqa: PLR2004 |
| 271 | + assert "created_at" in data["metadata"] |
| 272 | + |
| 273 | + def test_node_metadata_in_json(self, topic_graph): |
| 274 | + """Test that node-level metadata (uuid, topic_hash) appears in JSON output.""" |
| 275 | + json_str = topic_graph.to_json() |
| 276 | + data = json.loads(json_str) |
| 277 | + |
| 278 | + # Check root node metadata |
| 279 | + root_node = data["nodes"]["0"] |
| 280 | + assert "metadata" in root_node |
| 281 | + assert "uuid" in root_node["metadata"] |
| 282 | + assert "topic_hash" in root_node["metadata"] |
| 283 | + |
| 284 | + # Verify topic_hash is correct SHA256 |
| 285 | + expected_hash = hashlib.sha256(b"Test root topic").hexdigest() |
| 286 | + assert root_node["metadata"]["topic_hash"] == expected_hash |
| 287 | + |
197 | 288 | def test_to_json(self, topic_graph): |
198 | 289 | """Test JSON serialization.""" |
199 | 290 | node1 = topic_graph.add_node("Child") |
@@ -488,3 +579,58 @@ def test_graph_persistence_roundtrip(self): |
488 | 579 | assert any(child.topic == "Biology" for child in loaded_chemistry.children) |
489 | 580 | finally: |
490 | 581 | Path(temp_path).unlink() |
| 582 | + |
| 583 | + def test_backward_compatibility_no_metadata(self): |
| 584 | + """Test loading old graph JSON files that don't have metadata fields.""" |
| 585 | + # Simulate an old graph JSON without metadata |
| 586 | + old_graph_json = { |
| 587 | + "nodes": { |
| 588 | + "0": { |
| 589 | + "id": 0, |
| 590 | + "topic": "Root Topic", |
| 591 | + "children": [1], |
| 592 | + "parents": [], |
| 593 | + "metadata": {}, # Old format: empty metadata |
| 594 | + }, |
| 595 | + "1": { |
| 596 | + "id": 1, |
| 597 | + "topic": "Child Topic", |
| 598 | + "children": [], |
| 599 | + "parents": [0], |
| 600 | + "metadata": {}, |
| 601 | + }, |
| 602 | + }, |
| 603 | + "root_id": 0, |
| 604 | + # No "metadata" field at graph level |
| 605 | + } |
| 606 | + |
| 607 | + graph_params = { |
| 608 | + "topic_prompt": "Root Topic", |
| 609 | + "model_name": "test-model", |
| 610 | + "temperature": 0.5, |
| 611 | + "degree": 2, |
| 612 | + "depth": 2, |
| 613 | + } |
| 614 | + |
| 615 | + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: |
| 616 | + json.dump(old_graph_json, f) |
| 617 | + temp_path = f.name |
| 618 | + |
| 619 | + try: |
| 620 | + # Load should succeed without errors |
| 621 | + loaded = Graph.from_json(temp_path, graph_params) |
| 622 | + |
| 623 | + # Verify structure was loaded correctly |
| 624 | + assert len(loaded.nodes) == 2 # noqa: PLR2004 |
| 625 | + assert loaded.root.topic == "Root Topic" |
| 626 | + assert len(loaded.root.children) == 1 |
| 627 | + |
| 628 | + # Verify nodes get uuid and topic_hash auto-generated on load |
| 629 | + assert "uuid" in loaded.root.metadata |
| 630 | + assert "topic_hash" in loaded.root.metadata |
| 631 | + |
| 632 | + # Verify topic_hash is correct for the loaded topic |
| 633 | + expected_hash = hashlib.sha256(b"Root Topic").hexdigest() |
| 634 | + assert loaded.root.metadata["topic_hash"] == expected_hash |
| 635 | + finally: |
| 636 | + Path(temp_path).unlink() |
0 commit comments