""" Integration tests for circular dependency suppression. Tests circular dependency handling with real DB, debt recording, and various cycle topologies. """ 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 from markitect.prompts.dependencies.repository import SQLiteDependencyRepository from markitect.prompts.dependencies.queries import DependencyQueryService from markitect.prompts.incremental.detector import ChangeDetector from markitect.prompts.incremental.engine import IncrementalExecutionEngine from markitect.prompts.incremental.models import RecomputeConfig, ChangeType @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 query_service(dep_repo): """Create DependencyQueryService.""" return DependencyQueryService(dep_repo) @pytest.fixture def detector(temp_db): """Create ChangeDetector.""" return ChangeDetector(temp_db) @pytest.fixture def engine(temp_db, query_service): """Create IncrementalExecutionEngine.""" return IncrementalExecutionEngine(temp_db, query_service) def _create_artifact(repo, space_id, name, content="content"): """Helper to create and persist an artifact.""" artifact = Artifact.create( space_id=space_id, name=name, content=content, artifact_type=ArtifactType.CONTENT, ) return repo.create(artifact) def _create_edge(repo, src, tgt, run_id="run-1"): """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=EdgeType.REQUIRES, ) return repo.create(edge) class TestDirectCircularSuppression: """Tests for direct circular dependency (A <-> B) suppression.""" def test_mutual_dependency_suppressed( self, artifact_repo, dep_repo, detector, engine ): """Test mutual dependency suppresses recompute and records debt.""" art_a = _create_artifact(artifact_repo, "space-1", "a", "content-a") art_b = _create_artifact(artifact_repo, "space-1", "b", "content-b") # A -> B and B -> A (circular) _create_edge(dep_repo, art_a.id, art_b.id) _create_edge(dep_repo, art_b.id, art_a.id) # Detect change in B change = detector.detect_change(art_b, "content-b-modified") assert change is not None detector.record_change(change) # Recompute: A depends on B, but A -> B creates cycle result = engine.recompute( change, config=RecomputeConfig(max_depth=1, suppress_circular=True), old_content="content-b", new_content="content-b-modified", ) assert result.total_dependents == 1 assert result.suppressed_count == 1 assert result.recomputed_count == 0 assert result.suppressed[0].suppression_reason == "circular_dependency" def test_debt_persisted_for_circular( self, artifact_repo, dep_repo, detector, engine ): """Test that circular suppression debt is persisted in DB.""" art_a = _create_artifact(artifact_repo, "space-1", "a", "a-v1") art_b = _create_artifact(artifact_repo, "space-1", "b", "b-v1") _create_edge(dep_repo, art_a.id, art_b.id) _create_edge(dep_repo, art_b.id, art_a.id) change = detector.detect_change(art_b, "b-v2") detector.record_change(change) engine.recompute( change, config=RecomputeConfig(max_depth=1, suppress_circular=True), old_content="b-v1", new_content="b-v2", ) # Verify debt persisted debt = engine.get_debt_for_artifact(art_b.id) assert len(debt) == 1 assert debt[0].suppression_reason == "circular_dependency" assert debt[0].dependent_run_id == art_a.id class TestThreeNodeCycleSuppression: """Tests for three-node circular dependency suppression.""" def test_three_node_cycle( self, artifact_repo, dep_repo, detector, engine ): """Test 3-node cycle: A -> B -> C -> A.""" art_a = _create_artifact(artifact_repo, "space-1", "a", "a") art_b = _create_artifact(artifact_repo, "space-1", "b", "b") art_c = _create_artifact(artifact_repo, "space-1", "c", "c") # A -> B -> C -> A _create_edge(dep_repo, art_a.id, art_b.id) _create_edge(dep_repo, art_b.id, art_c.id) _create_edge(dep_repo, art_c.id, art_a.id) # Change C, dependent at depth 1 is B change = detector.detect_change(art_c, "c-modified") detector.record_change(change) result = engine.recompute( change, config=RecomputeConfig(max_depth=1, suppress_circular=True), old_content="c", new_content="c-modified", ) # B depends on C. would_create_cycle(B, C) checks if C can reach B. # C -> A -> B: yes, C can reach B. So B is suppressed. assert result.total_dependents == 1 assert result.suppressed_count == 1 assert result.suppressed[0].suppression_reason == "circular_dependency" class TestMixedCircularAndNormal: """Tests with a mix of circular and normal dependencies.""" def test_some_suppressed_some_recomputed( self, artifact_repo, dep_repo, detector, engine ): """Test graph with both circular and normal dependents.""" art_a = _create_artifact(artifact_repo, "space-1", "a", "a") art_b = _create_artifact(artifact_repo, "space-1", "b", "b") art_c = _create_artifact(artifact_repo, "space-1", "c", "c") # B -> A (normal), A -> B (creates cycle with B -> A) # C -> A (normal, no cycle) _create_edge(dep_repo, art_b.id, art_a.id) _create_edge(dep_repo, art_a.id, art_b.id) _create_edge(dep_repo, art_c.id, art_a.id) # Change A: dependents are B and C change = detector.detect_change(art_a, "a-modified") detector.record_change(change) result = engine.recompute( change, config=RecomputeConfig(max_depth=1, suppress_circular=True), old_content="a", new_content="a-modified", ) assert result.total_dependents == 2 # B has circular dep with A → suppressed # C has no circular dep with A → recomputed circular_debts = [d for d in result.suppressed if d.suppression_reason == "circular_dependency"] assert len(circular_debts) == 1 assert circular_debts[0].dependent_run_id == art_b.id assert result.recomputed_count == 1 assert art_c.id in result.executed_run_ids