Add change detection, structural diff-based impact analysis, configurable-depth incremental recomputation with circular suppression, and impact debt tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
7.5 KiB
Python
230 lines
7.5 KiB
Python
"""
|
|
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
|