""" Integration test for full incremental recompute workflow. Tests: change artifact → detect → find dependents → recompute with a real SQLite database. """ import pytest import tempfile from pathlib import Path from markitect.prompts.models import Artifact, ArtifactType, calculate_content_digest 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 @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 (shares same DB).""" 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): """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 TestFullRecomputeWorkflow: """Full end-to-end incremental recompute workflow.""" def test_change_detect_and_recompute( self, artifact_repo, dep_repo, query_service, detector, engine ): """Test complete flow: create artifacts, detect change, recompute dependents.""" # Step 1: Create artifacts lib = _create_artifact(artifact_repo, "space-1", "lib", "library v1") app = _create_artifact(artifact_repo, "space-1", "app", "app using lib") # Step 2: Establish dependency (app depends on lib) _create_edge(dep_repo, app.id, lib.id) # Step 3: Detect a change in lib change = detector.detect_change(lib, "library v2") assert change is not None detector.record_change(change) # Step 4: Recompute dependents executed_ids = [] def callback(dep_id): from markitect.prompts.execution.models import PromptRun run = PromptRun.create( template_id=dep_id, input_bundle_hash="recompute-hash", ) run.mark_complete() executed_ids.append(dep_id) return run result = engine.recompute( change, config=RecomputeConfig(max_depth=1), execution_callback=callback, old_content="library v1", new_content="library v2", ) # Verify assert result.total_dependents == 1 assert result.recomputed_count == 1 assert result.suppressed_count == 0 assert app.id in executed_ids def test_multi_level_recompute( self, artifact_repo, dep_repo, query_service, detector, engine ): """Test recompute propagates through multiple dependency levels.""" # core -> utils -> app core = _create_artifact(artifact_repo, "space-1", "core", "core v1") utils = _create_artifact(artifact_repo, "space-1", "utils", "utils v1") app = _create_artifact(artifact_repo, "space-1", "app", "app v1") _create_edge(dep_repo, utils.id, core.id) _create_edge(dep_repo, app.id, utils.id) # Change core change = detector.detect_change(core, "core v2") assert change is not None detector.record_change(change) # Recompute with depth 2 result = engine.recompute( change, config=RecomputeConfig(max_depth=2), old_content="core v1", new_content="core v2", ) assert result.total_dependents == 2 assert result.recomputed_count == 2 assert set(result.executed_run_ids) == {utils.id, app.id} def test_no_change_no_recompute( self, artifact_repo, dep_repo, detector, engine ): """Test that no change means no recompute.""" lib = _create_artifact(artifact_repo, "space-1", "lib", "unchanged") app = _create_artifact(artifact_repo, "space-1", "app", "app") _create_edge(dep_repo, app.id, lib.id) # Same content → no change change = detector.detect_change(lib, "unchanged") assert change is None def test_change_record_persisted( self, artifact_repo, detector ): """Test change records are persisted across detector instances.""" lib = _create_artifact(artifact_repo, "space-1", "lib", "v1") change = detector.detect_change(lib, "v2") assert change is not None detector.record_change(change) # Verify persisted changes = detector.get_changes_for_artifact(lib.id) assert len(changes) == 1 assert changes[0].id == change.id class TestMultipleArtifactChanges: """Tests for handling changes to multiple artifacts.""" def test_independent_changes( self, artifact_repo, dep_repo, detector, engine ): """Test independent artifact changes trigger separate recomputes.""" lib_a = _create_artifact(artifact_repo, "space-1", "lib-a", "lib-a v1") lib_b = _create_artifact(artifact_repo, "space-1", "lib-b", "lib-b v1") app = _create_artifact(artifact_repo, "space-1", "app", "app v1") _create_edge(dep_repo, app.id, lib_a.id) _create_edge(dep_repo, app.id, lib_b.id) # Change lib_a change_a = detector.detect_change(lib_a, "lib-a v2") assert change_a is not None detector.record_change(change_a) result_a = engine.recompute( change_a, config=RecomputeConfig(max_depth=1), old_content="lib-a v1", new_content="lib-a v2", ) assert result_a.total_dependents == 1 assert result_a.recomputed_count == 1 # Change lib_b change_b = detector.detect_change(lib_b, "lib-b v2") assert change_b is not None detector.record_change(change_b) result_b = engine.recompute( change_b, config=RecomputeConfig(max_depth=1), old_content="lib-b v1", new_content="lib-b v2", ) assert result_b.total_dependents == 1 assert result_b.recomputed_count == 1