Files
markitect-main/tests/unit/prompts/test_dependency_models.py
tegwick 9ce157400e feat(prompts): implement Phase 5 - Dependency Tracking (FR-6)
Add directed dependency graph with cycle detection, topological sort,
and query service for finding dependents/dependencies transitively.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:18:18 +01:00

411 lines
14 KiB
Python

"""
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")