""" Integration tests for circular dependency detection. Tests cycle detection with real DB, pre-validation before persisting edges, and error detail reporting. """ import pytest import tempfile from pathlib import Path from markitect.prompts.models import Artifact, ArtifactType from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository from markitect.prompts.dependencies.models import ( DependencyEdge, EdgeType, CircularDependencyError, ) from markitect.prompts.dependencies.repository import SQLiteDependencyRepository from markitect.prompts.dependencies.graph import GraphBuilder 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 artifact_repo(temp_db): """Create artifact repository.""" return SQLiteArtifactRepository(temp_db) @pytest.fixture def dep_repo(temp_db): """Create dependency repository.""" return SQLiteDependencyRepository(temp_db) @pytest.fixture def builder(dep_repo): """Create GraphBuilder.""" return GraphBuilder(dep_repo) @pytest.fixture def query_service(dep_repo): """Create DependencyQueryService.""" return DependencyQueryService(dep_repo) def _create_artifact(artifact_repo, space_id, name): """Helper to create and persist an artifact.""" artifact = Artifact.create( space_id=space_id, name=name, content=f"content for {name}", artifact_type=ArtifactType.CONTENT, ) return artifact_repo.create(artifact) def _persist_edge(dep_repo, src_id, tgt_id, run_id="run-1"): """Helper to persist a dependency edge.""" edge = DependencyEdge.create( source_artifact_id=src_id, target_artifact_id=tgt_id, run_id=run_id, edge_type=EdgeType.REQUIRES, ) return dep_repo.create(edge) class TestCycleDetectionWithDB: """Tests for cycle detection using real database storage.""" def test_detect_simple_cycle(self, artifact_repo, dep_repo, query_service): """Test detecting a simple 2-node cycle in persisted graph.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_a.id, run_id="run-2") cycles = query_service.detect_circular_dependencies() assert len(cycles) > 0 def test_detect_three_node_cycle(self, artifact_repo, dep_repo, query_service): """Test detecting a 3-node cycle A -> B -> C -> A.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") art_c = _create_artifact(artifact_repo, "space-1", "c") _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_c.id) _persist_edge(dep_repo, art_c.id, art_a.id, run_id="run-2") cycles = query_service.detect_circular_dependencies() assert len(cycles) > 0 # All three artifacts should be in the cycle cycle_nodes = set(cycles[0][:-1]) assert art_a.id in cycle_nodes assert art_b.id in cycle_nodes assert art_c.id in cycle_nodes def test_no_false_positives(self, artifact_repo, dep_repo, query_service): """Test no false positive cycle detection in DAG.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") art_c = _create_artifact(artifact_repo, "space-1", "c") art_d = _create_artifact(artifact_repo, "space-1", "d") # Diamond: A -> B, A -> C, B -> D, C -> D _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_a.id, art_c.id, run_id="run-2") _persist_edge(dep_repo, art_b.id, art_d.id, run_id="run-3") _persist_edge(dep_repo, art_c.id, art_d.id, run_id="run-4") cycles = query_service.detect_circular_dependencies() assert cycles == [] class TestPreValidation: """Tests for pre-validation (would_create_cycle) before persisting edges.""" def test_validate_safe_edge(self, artifact_repo, dep_repo, query_service): """Test pre-validation accepts safe edge.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") art_c = _create_artifact(artifact_repo, "space-1", "c") _persist_edge(dep_repo, art_a.id, art_b.id) # B -> C would not create a cycle assert query_service.would_create_cycle(art_b.id, art_c.id) is False def test_validate_cycle_creating_edge( self, artifact_repo, dep_repo, query_service ): """Test pre-validation rejects edge that would create a cycle.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") art_c = _create_artifact(artifact_repo, "space-1", "c") _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_c.id, run_id="run-2") # C -> A would create a cycle assert query_service.would_create_cycle(art_c.id, art_a.id) is True def test_validate_before_persist_workflow( self, artifact_repo, dep_repo, query_service ): """Test the full validate-then-persist workflow.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") art_c = _create_artifact(artifact_repo, "space-1", "c") # Persist A -> B _persist_edge(dep_repo, art_a.id, art_b.id) # Validate and persist B -> C (safe) assert query_service.would_create_cycle(art_b.id, art_c.id) is False _persist_edge(dep_repo, art_b.id, art_c.id, run_id="run-2") # Validate C -> A (would create cycle, don't persist) assert query_service.would_create_cycle(art_c.id, art_a.id) is True # Don't persist - graph remains acyclic # Verify graph is still cycle-free assert query_service.detect_circular_dependencies() == [] class TestCycleErrorDetails: """Tests for cycle error detail reporting.""" def test_topological_sort_error_contains_cycle( self, artifact_repo, dep_repo, builder ): """Test that topological sort error includes cycle details.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") art_c = _create_artifact(artifact_repo, "space-1", "c") _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_c.id, run_id="run-2") _persist_edge(dep_repo, art_c.id, art_a.id, run_id="run-3") graph = builder.build_graph() with pytest.raises(CircularDependencyError) as exc_info: graph.topological_sort() error = exc_info.value assert error.cycle is not None assert len(error.cycle) >= 3 # The cycle should contain our artifact IDs cycle_set = set(error.cycle) assert art_a.id in cycle_set assert art_b.id in cycle_set assert art_c.id in cycle_set def test_error_message_readable(self, artifact_repo, dep_repo, builder): """Test that error message is human-readable.""" art_a = _create_artifact(artifact_repo, "space-1", "a") art_b = _create_artifact(artifact_repo, "space-1", "b") _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_a.id, run_id="run-2") graph = builder.build_graph() with pytest.raises(CircularDependencyError) as exc_info: graph.topological_sort() message = str(exc_info.value) assert "Circular dependency detected" in message assert "->" in message class TestCrossSpaceCycles: """Tests for cycle detection across information spaces.""" def test_cross_space_cycle(self, artifact_repo, dep_repo, query_service): """Test detecting cycles that span multiple spaces.""" # space-a/X -> space-b/Y -> space-a/X (cycle across spaces) art_x = _create_artifact(artifact_repo, "space-a", "x") art_y = _create_artifact(artifact_repo, "space-b", "y") _persist_edge(dep_repo, art_x.id, art_y.id) _persist_edge(dep_repo, art_y.id, art_x.id, run_id="run-2") cycles = query_service.detect_circular_dependencies() assert len(cycles) > 0 def test_cross_space_transitive_cycle( self, artifact_repo, dep_repo, query_service ): """Test detecting transitive cycles across multiple spaces.""" art_a = _create_artifact(artifact_repo, "space-a", "a") art_b = _create_artifact(artifact_repo, "space-b", "b") art_c = _create_artifact(artifact_repo, "space-c", "c") _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_c.id, run_id="run-2") _persist_edge(dep_repo, art_c.id, art_a.id, run_id="run-3") cycles = query_service.detect_circular_dependencies() assert len(cycles) > 0 # Pre-validation should also catch it # If we had A -> B -> C already, adding C -> A would be caught assert query_service.would_create_cycle(art_c.id, art_a.id) is True def test_cross_space_no_false_positive( self, artifact_repo, dep_repo, query_service ): """Test no false positives across spaces.""" art_a = _create_artifact(artifact_repo, "space-a", "a") art_b = _create_artifact(artifact_repo, "space-b", "b") art_c = _create_artifact(artifact_repo, "space-c", "c") # Linear: A -> B -> C (no cycle) _persist_edge(dep_repo, art_a.id, art_b.id) _persist_edge(dep_repo, art_b.id, art_c.id, run_id="run-2") cycles = query_service.detect_circular_dependencies() assert cycles == []