""" Unit tests for DependencyQueryService. Tests direct/transitive dependents and dependencies, dependency chains, cycle validation, would_create_cycle, and build order. """ import pytest import tempfile from pathlib import Path from markitect.prompts.dependencies.models import ( DependencyEdge, DependencyGraph, EdgeType, CircularDependencyError, ) from markitect.prompts.dependencies.repository import SQLiteDependencyRepository from markitect.prompts.dependencies.queries import DependencyQueryService @pytest.fixture def temp_db(): """Create temporary database for testing.""" with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: db_path = f.name yield db_path Path(db_path).unlink(missing_ok=True) @pytest.fixture def repository(temp_db): """Create repository instance with temp database.""" return SQLiteDependencyRepository(temp_db) @pytest.fixture def query_service(repository): """Create DependencyQueryService instance.""" return DependencyQueryService(repository) def _create_edge(repository, src, tgt, run_id="run-1", edge_type=EdgeType.REQUIRES): """Helper to create and persist a dependency edge.""" edge = DependencyEdge.create( source_artifact_id=src, target_artifact_id=tgt, run_id=run_id, edge_type=edge_type, ) return repository.create(edge) class TestFindDependents: """Tests for find_dependents (who depends on X).""" def test_find_direct_dependents(self, repository, query_service): """Test finding direct dependents of an artifact.""" _create_edge(repository, "A", "B") _create_edge(repository, "C", "B") dependents = query_service.find_dependents("B") assert dependents == {"A", "C"} def test_find_dependents_none(self, repository, query_service): """Test finding dependents when none exist.""" _create_edge(repository, "A", "B") dependents = query_service.find_dependents("A") assert dependents == set() def test_find_dependents_nonexistent(self, query_service): """Test finding dependents of non-existent artifact.""" dependents = query_service.find_dependents("nonexistent") assert dependents == set() class TestFindDependencies: """Tests for find_dependencies (what X depends on).""" def test_find_direct_dependencies(self, repository, query_service): """Test finding direct dependencies of an artifact.""" _create_edge(repository, "A", "B") _create_edge(repository, "A", "C") dependencies = query_service.find_dependencies("A") assert dependencies == {"B", "C"} def test_find_dependencies_none(self, repository, query_service): """Test finding dependencies when none exist.""" _create_edge(repository, "A", "B") dependencies = query_service.find_dependencies("B") assert dependencies == set() class TestTransitiveDependents: """Tests for find_transitive_dependents.""" def test_transitive_dependents(self, repository, query_service): """Test finding transitive dependents (upstream impact).""" # A -> B -> C _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") # Who is transitively affected by C? transitive = query_service.find_transitive_dependents("C") assert transitive == {"A", "B"} def test_transitive_dependents_diamond(self, repository, query_service): """Test transitive dependents in diamond graph.""" # A -> B -> D, A -> C -> D _create_edge(repository, "A", "B") _create_edge(repository, "A", "C") _create_edge(repository, "B", "D") _create_edge(repository, "C", "D") transitive = query_service.find_transitive_dependents("D") assert transitive == {"A", "B", "C"} def test_transitive_dependents_none(self, repository, query_service): """Test transitive dependents when none exist.""" _create_edge(repository, "A", "B") transitive = query_service.find_transitive_dependents("A") assert transitive == set() class TestTransitiveDependencies: """Tests for find_transitive_dependencies.""" def test_transitive_dependencies(self, repository, query_service): """Test finding transitive dependencies (full dep tree).""" # A -> B -> C _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") # What does A transitively depend on? transitive = query_service.find_transitive_dependencies("A") assert transitive == {"B", "C"} def test_transitive_dependencies_complex(self, repository, query_service): """Test transitive dependencies in complex graph.""" # A -> B -> D, A -> C -> D, D -> E _create_edge(repository, "A", "B") _create_edge(repository, "A", "C") _create_edge(repository, "B", "D") _create_edge(repository, "C", "D") _create_edge(repository, "D", "E") transitive = query_service.find_transitive_dependencies("A") assert transitive == {"B", "C", "D", "E"} def test_transitive_dependencies_leaf(self, repository, query_service): """Test transitive dependencies of a leaf node.""" _create_edge(repository, "A", "B") transitive = query_service.find_transitive_dependencies("B") assert transitive == set() class TestDependencyChain: """Tests for get_dependency_chain (BFS path finding).""" def test_direct_chain(self, repository, query_service): """Test finding a direct dependency chain.""" _create_edge(repository, "A", "B") chain = query_service.get_dependency_chain("A", "B") assert chain == ["A", "B"] def test_multi_hop_chain(self, repository, query_service): """Test finding a multi-hop dependency chain.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") _create_edge(repository, "C", "D") chain = query_service.get_dependency_chain("A", "D") assert chain is not None assert chain[0] == "A" assert chain[-1] == "D" assert len(chain) == 4 def test_no_chain_exists(self, repository, query_service): """Test when no chain exists between artifacts.""" _create_edge(repository, "A", "B") _create_edge(repository, "C", "D") chain = query_service.get_dependency_chain("A", "D") assert chain is None def test_same_source_target(self, query_service): """Test chain from node to itself.""" chain = query_service.get_dependency_chain("A", "A") assert chain == ["A"] def test_shortest_chain(self, repository, query_service): """Test BFS finds shortest chain.""" # A -> B -> C (length 3) # A -> C (length 2, shorter) _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") _create_edge(repository, "A", "C") chain = query_service.get_dependency_chain("A", "C") assert chain == ["A", "C"] class TestCircularDependencyDetection: """Tests for detect_circular_dependencies.""" def test_no_cycles(self, repository, query_service): """Test detection when no cycles exist.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") cycles = query_service.detect_circular_dependencies() assert cycles == [] def test_detect_simple_cycle(self, repository, query_service): """Test detecting a simple cycle.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "A") cycles = query_service.detect_circular_dependencies() assert len(cycles) > 0 def test_detect_three_node_cycle(self, repository, query_service): """Test detecting a 3-node cycle.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") _create_edge(repository, "C", "A") cycles = query_service.detect_circular_dependencies() assert len(cycles) > 0 class TestWouldCreateCycle: """Tests for would_create_cycle pre-validation.""" def test_would_not_create_cycle(self, repository, query_service): """Test edge that would not create a cycle.""" _create_edge(repository, "A", "B") assert query_service.would_create_cycle("B", "C") is False def test_would_create_cycle_direct(self, repository, query_service): """Test edge that would create a direct cycle.""" _create_edge(repository, "A", "B") assert query_service.would_create_cycle("B", "A") is True def test_would_create_cycle_transitive(self, repository, query_service): """Test edge that would create a transitive cycle.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") assert query_service.would_create_cycle("C", "A") is True def test_self_reference_creates_cycle(self, query_service): """Test self-reference always creates a cycle.""" assert query_service.would_create_cycle("A", "A") is True def test_safe_parallel_edge(self, repository, query_service): """Test parallel forward edge is safe.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") # Adding A -> C doesn't create a cycle assert query_service.would_create_cycle("A", "C") is False class TestBuildOrder: """Tests for get_build_order (topological sort).""" def test_simple_build_order(self, repository, query_service): """Test simple build order (dependencies before dependents).""" # A requires B, B requires C → build C first, then B, then A _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") order = query_service.get_build_order() assert order.index("C") < order.index("B") assert order.index("B") < order.index("A") def test_build_order_diamond(self, repository, query_service): """Test build order with diamond dependencies.""" # A requires B and C, B and C require D → build D first, then B/C, then A _create_edge(repository, "A", "B") _create_edge(repository, "A", "C") _create_edge(repository, "B", "D") _create_edge(repository, "C", "D") order = query_service.get_build_order() assert order.index("D") < order.index("B") assert order.index("D") < order.index("C") assert order.index("B") < order.index("A") assert order.index("C") < order.index("A") def test_build_order_scoped(self, repository, query_service): """Test build order with scoped artifact set.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") _create_edge(repository, "C", "D") order = query_service.get_build_order(artifact_ids={"A", "B", "C"}) assert len(order) == 3 assert "D" not in order assert order.index("C") < order.index("B") assert order.index("B") < order.index("A") def test_build_order_with_cycle_raises_error(self, repository, query_service): """Test build order raises error on cycle.""" _create_edge(repository, "A", "B") _create_edge(repository, "B", "C") _create_edge(repository, "C", "A") with pytest.raises(CircularDependencyError): query_service.get_build_order() def test_build_order_empty(self, query_service): """Test build order with no edges.""" order = query_service.get_build_order() assert order == []