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>
167 lines
5.8 KiB
Python
167 lines
5.8 KiB
Python
"""
|
|
Unit tests for ChangeDetector.
|
|
|
|
Tests change detection, recording, change types, and no-change cases.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from markitect.prompts.models import Artifact, ArtifactType, calculate_content_digest
|
|
from markitect.prompts.incremental.detector import ChangeDetector
|
|
from markitect.prompts.incremental.models import 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 detector(temp_db):
|
|
"""Create ChangeDetector instance."""
|
|
return ChangeDetector(temp_db)
|
|
|
|
|
|
def _make_artifact(content="original content"):
|
|
"""Helper to create an in-memory artifact."""
|
|
return Artifact.create(
|
|
space_id="space-1",
|
|
name="test-artifact",
|
|
content=content,
|
|
artifact_type=ArtifactType.CONTENT,
|
|
)
|
|
|
|
|
|
class TestDetectChange:
|
|
"""Tests for detecting content changes."""
|
|
|
|
def test_detect_modification(self, detector):
|
|
"""Test detecting a content modification."""
|
|
artifact = _make_artifact("original content")
|
|
change = detector.detect_change(artifact, "modified content")
|
|
|
|
assert change is not None
|
|
assert change.artifact_id == artifact.id
|
|
assert change.old_digest == artifact.content_digest
|
|
assert change.new_digest == calculate_content_digest("modified content")
|
|
assert change.change_type == ChangeType.MODIFIED
|
|
|
|
def test_no_change_returns_none(self, detector):
|
|
"""Test that identical content returns None."""
|
|
artifact = _make_artifact("same content")
|
|
change = detector.detect_change(artifact, "same content")
|
|
|
|
assert change is None
|
|
|
|
def test_detect_whitespace_change(self, detector):
|
|
"""Test detecting whitespace-only changes."""
|
|
artifact = _make_artifact("content")
|
|
change = detector.detect_change(artifact, "content ")
|
|
|
|
assert change is not None
|
|
assert change.change_type == ChangeType.MODIFIED
|
|
|
|
def test_detect_empty_to_content(self, detector):
|
|
"""Test detecting change from empty to content."""
|
|
artifact = _make_artifact("")
|
|
change = detector.detect_change(artifact, "new content")
|
|
|
|
assert change is not None
|
|
assert change.change_type == ChangeType.MODIFIED
|
|
|
|
|
|
class TestDetectCreation:
|
|
"""Tests for recording artifact creation."""
|
|
|
|
def test_detect_creation(self, detector):
|
|
"""Test creation change record."""
|
|
change = detector.detect_creation("artifact-123", "new content")
|
|
|
|
assert change.artifact_id == "artifact-123"
|
|
assert change.old_digest is None
|
|
assert change.new_digest == calculate_content_digest("new content")
|
|
assert change.change_type == ChangeType.CREATED
|
|
|
|
def test_creation_has_unique_id(self, detector):
|
|
"""Test that each creation gets a unique ID."""
|
|
change1 = detector.detect_creation("art-1", "content")
|
|
change2 = detector.detect_creation("art-2", "content")
|
|
|
|
assert change1.id != change2.id
|
|
|
|
|
|
class TestDetectDeletion:
|
|
"""Tests for recording artifact deletion."""
|
|
|
|
def test_detect_deletion(self, detector):
|
|
"""Test deletion change record."""
|
|
artifact = _make_artifact("content to delete")
|
|
change = detector.detect_deletion(artifact)
|
|
|
|
assert change.artifact_id == artifact.id
|
|
assert change.old_digest == artifact.content_digest
|
|
assert change.change_type == ChangeType.DELETED
|
|
|
|
|
|
class TestRecordChange:
|
|
"""Tests for persisting change records."""
|
|
|
|
def test_record_and_retrieve(self, detector):
|
|
"""Test recording a change and retrieving it."""
|
|
artifact = _make_artifact("original")
|
|
change = detector.detect_change(artifact, "modified")
|
|
assert change is not None
|
|
|
|
detector.record_change(change)
|
|
changes = detector.get_changes_for_artifact(artifact.id)
|
|
|
|
assert len(changes) == 1
|
|
assert changes[0].id == change.id
|
|
assert changes[0].artifact_id == artifact.id
|
|
assert changes[0].change_type == ChangeType.MODIFIED
|
|
|
|
def test_record_multiple_changes(self, detector):
|
|
"""Test recording multiple changes for same artifact."""
|
|
artifact = _make_artifact("v1")
|
|
|
|
change1 = detector.detect_change(artifact, "v2")
|
|
detector.record_change(change1)
|
|
|
|
# Simulate artifact update
|
|
artifact.update_content("v2")
|
|
change2 = detector.detect_change(artifact, "v3")
|
|
detector.record_change(change2)
|
|
|
|
changes = detector.get_changes_for_artifact(artifact.id)
|
|
assert len(changes) == 2
|
|
|
|
def test_get_changes_by_type(self, detector):
|
|
"""Test filtering changes by type."""
|
|
# Record a creation
|
|
creation = detector.detect_creation("art-new", "content")
|
|
detector.record_change(creation)
|
|
|
|
# Record a modification
|
|
artifact = _make_artifact("old")
|
|
modification = detector.detect_change(artifact, "new")
|
|
detector.record_change(modification)
|
|
|
|
created_changes = detector.get_changes_by_type(ChangeType.CREATED)
|
|
assert len(created_changes) == 1
|
|
assert created_changes[0].change_type == ChangeType.CREATED
|
|
|
|
modified_changes = detector.get_changes_by_type(ChangeType.MODIFIED)
|
|
assert len(modified_changes) == 1
|
|
assert modified_changes[0].change_type == ChangeType.MODIFIED
|
|
|
|
def test_no_changes_returns_empty(self, detector):
|
|
"""Test querying changes for artifact with none recorded."""
|
|
changes = detector.get_changes_for_artifact("nonexistent")
|
|
assert changes == []
|