""" Unit tests for dependency tracking models. Tests EdgeType, DependencyEdge, DependencyGraph operations, cycle detection, and topological sort. """ import pytest from datetime import datetime from markitect.prompts.dependencies.models import ( EdgeType, DependencyEdge, DependencyGraph, CircularDependencyError, ) class TestEdgeType: """Tests for EdgeType enum.""" def test_edge_type_values(self): """Test edge type enum values.""" assert EdgeType.REQUIRES.value == "requires" assert EdgeType.GENERATES.value == "generates" assert EdgeType.INCLUDES.value == "includes" def test_edge_type_from_value(self): """Test creating edge type from string value.""" assert EdgeType("requires") == EdgeType.REQUIRES assert EdgeType("generates") == EdgeType.GENERATES assert EdgeType("includes") == EdgeType.INCLUDES def test_edge_type_invalid_value(self): """Test invalid edge type raises ValueError.""" with pytest.raises(ValueError): EdgeType("invalid") class TestDependencyEdge: """Tests for persistent DependencyEdge.""" def test_create_edge(self): """Test creating a dependency edge via factory method.""" edge = DependencyEdge.create( source_artifact_id="artifact-1", target_artifact_id="artifact-2", run_id="run-1", edge_type=EdgeType.REQUIRES, ) assert edge.id # UUID generated assert edge.source_artifact_id == "artifact-1" assert edge.target_artifact_id == "artifact-2" assert edge.run_id == "run-1" assert edge.edge_type == EdgeType.REQUIRES assert isinstance(edge.created_at, datetime) def test_create_edge_unique_ids(self): """Test each created edge gets a unique ID.""" edge1 = DependencyEdge.create( source_artifact_id="a", target_artifact_id="b", run_id="r1", edge_type=EdgeType.REQUIRES, ) edge2 = DependencyEdge.create( source_artifact_id="a", target_artifact_id="b", run_id="r1", edge_type=EdgeType.REQUIRES, ) assert edge1.id != edge2.id def test_edge_to_dict(self): """Test edge serialization to dict.""" edge = DependencyEdge.create( source_artifact_id="artifact-1", target_artifact_id="artifact-2", run_id="run-1", edge_type=EdgeType.GENERATES, ) data = edge.to_dict() assert data["id"] == edge.id assert data["source_artifact_id"] == "artifact-1" assert data["target_artifact_id"] == "artifact-2" assert data["run_id"] == "run-1" assert data["edge_type"] == "generates" assert "created_at" in data def test_edge_from_dict(self): """Test edge deserialization from dict.""" data = { "id": "edge-123", "source_artifact_id": "artifact-1", "target_artifact_id": "artifact-2", "run_id": "run-1", "edge_type": "includes", "created_at": "2026-01-01T00:00:00", } edge = DependencyEdge.from_dict(data) assert edge.id == "edge-123" assert edge.source_artifact_id == "artifact-1" assert edge.target_artifact_id == "artifact-2" assert edge.run_id == "run-1" assert edge.edge_type == EdgeType.INCLUDES assert edge.created_at == datetime(2026, 1, 1) def test_edge_roundtrip(self): """Test serialization roundtrip preserves data.""" original = DependencyEdge.create( source_artifact_id="src", target_artifact_id="tgt", run_id="run", edge_type=EdgeType.REQUIRES, ) restored = DependencyEdge.from_dict(original.to_dict()) assert restored.id == original.id assert restored.source_artifact_id == original.source_artifact_id assert restored.target_artifact_id == original.target_artifact_id assert restored.run_id == original.run_id assert restored.edge_type == original.edge_type class TestDependencyGraph: """Tests for DependencyGraph.""" def test_empty_graph(self): """Test empty graph properties.""" graph = DependencyGraph() assert len(graph.nodes) == 0 assert graph.edge_count == 0 assert not graph.has_cycle() def test_add_edge(self): """Test adding an edge to the graph.""" graph = DependencyGraph() graph.add_edge("A", "B", EdgeType.REQUIRES) assert "A" in graph.nodes assert "B" in graph.nodes assert graph.edge_count == 1 assert graph.has_edge("A", "B") assert not graph.has_edge("B", "A") def test_add_multiple_edges(self): """Test adding multiple edges.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("B", "C") graph.add_edge("A", "C") assert len(graph.nodes) == 3 assert graph.edge_count == 3 def test_get_successors(self): """Test getting direct successors.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("A", "C") graph.add_edge("B", "C") assert graph.get_successors("A") == {"B", "C"} assert graph.get_successors("B") == {"C"} assert graph.get_successors("C") == set() def test_get_predecessors(self): """Test getting direct predecessors.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("A", "C") graph.add_edge("B", "C") assert graph.get_predecessors("A") == set() assert graph.get_predecessors("B") == {"A"} assert graph.get_predecessors("C") == {"A", "B"} def test_get_successors_nonexistent_node(self): """Test getting successors of non-existent node.""" graph = DependencyGraph() assert graph.get_successors("X") == set() def test_get_predecessors_nonexistent_node(self): """Test getting predecessors of non-existent node.""" graph = DependencyGraph() assert graph.get_predecessors("X") == set() def test_get_edge_type(self): """Test getting edge type between nodes.""" graph = DependencyGraph() graph.add_edge("A", "B", EdgeType.REQUIRES) graph.add_edge("B", "C", EdgeType.GENERATES) assert graph.get_edge_type("A", "B") == EdgeType.REQUIRES assert graph.get_edge_type("B", "C") == EdgeType.GENERATES assert graph.get_edge_type("A", "C") is None def test_has_edge(self): """Test edge existence check.""" graph = DependencyGraph() graph.add_edge("A", "B") assert graph.has_edge("A", "B") assert not graph.has_edge("B", "A") assert not graph.has_edge("A", "C") class TestDependencyGraphCycleDetection: """Tests for DependencyGraph cycle detection.""" def test_no_cycle_linear(self): """Test no cycle in linear graph A -> B -> C.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("B", "C") assert not graph.has_cycle() assert graph.detect_cycles() == [] def test_no_cycle_diamond(self): """Test no cycle in diamond graph A -> B,C -> D.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("A", "C") graph.add_edge("B", "D") graph.add_edge("C", "D") assert not graph.has_cycle() def test_simple_cycle(self): """Test detecting simple cycle A -> B -> A.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("B", "A") assert graph.has_cycle() cycles = graph.detect_cycles() assert len(cycles) > 0 def test_three_node_cycle(self): """Test detecting 3-node cycle A -> B -> C -> A.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("B", "C") graph.add_edge("C", "A") assert graph.has_cycle() cycles = graph.detect_cycles() assert len(cycles) > 0 # The cycle should contain A, B, C cycle_nodes = set(cycles[0][:-1]) # Last element repeats the start assert cycle_nodes == {"A", "B", "C"} def test_self_loop(self): """Test detecting self-loop A -> A.""" graph = DependencyGraph() graph.add_edge("A", "A") assert graph.has_cycle() def test_cycle_with_acyclic_parts(self): """Test cycle detection in graph with both cyclic and acyclic parts.""" graph = DependencyGraph() # Acyclic part graph.add_edge("X", "Y") # Cyclic part graph.add_edge("A", "B") graph.add_edge("B", "C") graph.add_edge("C", "A") assert graph.has_cycle() class TestDependencyGraphTopologicalSort: """Tests for DependencyGraph topological sort.""" def test_topological_sort_linear(self): """Test topological sort of linear graph.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("B", "C") order = graph.topological_sort() assert order.index("A") < order.index("B") assert order.index("B") < order.index("C") def test_topological_sort_diamond(self): """Test topological sort of diamond graph.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("A", "C") graph.add_edge("B", "D") graph.add_edge("C", "D") order = graph.topological_sort() assert order.index("A") < order.index("B") assert order.index("A") < order.index("C") assert order.index("B") < order.index("D") assert order.index("C") < order.index("D") def test_topological_sort_single_node(self): """Test topological sort with single node.""" graph = DependencyGraph() graph.add_edge("A", "B") order = graph.topological_sort() assert len(order) == 2 assert order[0] == "A" assert order[1] == "B" def test_topological_sort_disconnected(self): """Test topological sort with disconnected components.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("C", "D") order = graph.topological_sort() assert len(order) == 4 assert order.index("A") < order.index("B") assert order.index("C") < order.index("D") def test_topological_sort_with_cycle_raises_error(self): """Test topological sort raises error on cycle.""" graph = DependencyGraph() graph.add_edge("A", "B") graph.add_edge("B", "C") graph.add_edge("C", "A") with pytest.raises(CircularDependencyError) as exc_info: graph.topological_sort() assert exc_info.value.cycle is not None assert len(exc_info.value.cycle) > 0 def test_topological_sort_empty_graph(self): """Test topological sort of empty graph.""" graph = DependencyGraph() assert graph.topological_sort() == [] class TestDependencyGraphSubgraph: """Tests for DependencyGraph subgraph extraction.""" def test_subgraph_extraction(self): """Test extracting a subgraph.""" graph = DependencyGraph() graph.add_edge("A", "B", EdgeType.REQUIRES) graph.add_edge("B", "C", EdgeType.GENERATES) graph.add_edge("C", "D", EdgeType.INCLUDES) subgraph = graph.get_subgraph({"A", "B", "C"}) assert subgraph.has_edge("A", "B") assert subgraph.has_edge("B", "C") assert not subgraph.has_edge("C", "D") assert "D" not in subgraph.nodes def test_subgraph_preserves_edge_types(self): """Test subgraph preserves edge types.""" graph = DependencyGraph() graph.add_edge("A", "B", EdgeType.REQUIRES) graph.add_edge("B", "C", EdgeType.GENERATES) subgraph = graph.get_subgraph({"A", "B", "C"}) assert subgraph.get_edge_type("A", "B") == EdgeType.REQUIRES assert subgraph.get_edge_type("B", "C") == EdgeType.GENERATES def test_subgraph_isolated_nodes(self): """Test subgraph includes isolated nodes.""" graph = DependencyGraph() graph.add_edge("A", "B") subgraph = graph.get_subgraph({"A", "B", "C"}) assert "C" in subgraph.nodes assert subgraph.get_successors("C") == set() def test_subgraph_empty(self): """Test extracting empty subgraph.""" graph = DependencyGraph() graph.add_edge("A", "B") subgraph = graph.get_subgraph(set()) assert len(subgraph.nodes) == 0 class TestCircularDependencyError: """Tests for CircularDependencyError.""" def test_error_with_cycle(self): """Test error with cycle information.""" error = CircularDependencyError( "Cycle detected", cycle=["A", "B", "C", "A"], ) assert str(error) == "Cycle detected" assert error.cycle == ["A", "B", "C", "A"] def test_error_without_cycle(self): """Test error without cycle information.""" error = CircularDependencyError("Cycle detected") assert error.cycle == [] def test_error_is_exception(self): """Test that CircularDependencyError is an Exception.""" with pytest.raises(CircularDependencyError): raise CircularDependencyError("test")