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>
163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
"""
|
|
Unit tests for ImpactAnalyzer and metrics functions.
|
|
|
|
Tests diff ratios, magnitude scoring, and threshold decisions.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from markitect.prompts.incremental.metrics import (
|
|
structural_diff_ratio,
|
|
line_diff_ratio,
|
|
calculate_change_magnitude,
|
|
)
|
|
from markitect.prompts.incremental.impact import ImpactAnalyzer
|
|
from markitect.prompts.incremental.models import RecomputeConfig
|
|
|
|
|
|
class TestStructuralDiffRatio:
|
|
"""Tests for structural_diff_ratio."""
|
|
|
|
def test_identical_content(self):
|
|
"""Test identical content returns 0.0."""
|
|
assert structural_diff_ratio("hello", "hello") == 0.0
|
|
|
|
def test_completely_different(self):
|
|
"""Test completely different content returns high ratio."""
|
|
ratio = structural_diff_ratio("aaa", "zzz")
|
|
assert ratio > 0.5
|
|
|
|
def test_empty_strings(self):
|
|
"""Test both empty returns 0.0."""
|
|
assert structural_diff_ratio("", "") == 0.0
|
|
|
|
def test_one_empty(self):
|
|
"""Test one empty returns 1.0."""
|
|
assert structural_diff_ratio("", "content") == 1.0
|
|
assert structural_diff_ratio("content", "") == 1.0
|
|
|
|
def test_small_change(self):
|
|
"""Test small change returns low ratio."""
|
|
old = "The quick brown fox jumps over the lazy dog"
|
|
new = "The quick brown fox leaps over the lazy dog"
|
|
ratio = structural_diff_ratio(old, new)
|
|
assert 0.0 < ratio < 0.5
|
|
|
|
def test_returns_float(self):
|
|
"""Test return value is float between 0 and 1."""
|
|
ratio = structural_diff_ratio("abc", "abd")
|
|
assert isinstance(ratio, float)
|
|
assert 0.0 <= ratio <= 1.0
|
|
|
|
|
|
class TestLineDiffRatio:
|
|
"""Tests for line_diff_ratio."""
|
|
|
|
def test_identical_lines(self):
|
|
"""Test identical multi-line content returns 0.0."""
|
|
content = "line1\nline2\nline3"
|
|
assert line_diff_ratio(content, content) == 0.0
|
|
|
|
def test_one_line_changed(self):
|
|
"""Test changing one line of several."""
|
|
old = "line1\nline2\nline3"
|
|
new = "line1\nmodified\nline3"
|
|
ratio = line_diff_ratio(old, new)
|
|
assert 0.0 < ratio < 1.0
|
|
|
|
def test_all_lines_changed(self):
|
|
"""Test all lines changed returns high ratio."""
|
|
old = "aaa\nbbb\nccc"
|
|
new = "xxx\nyyy\nzzz"
|
|
ratio = line_diff_ratio(old, new)
|
|
assert ratio > 0.5
|
|
|
|
def test_empty_strings(self):
|
|
"""Test both empty returns 0.0."""
|
|
assert line_diff_ratio("", "") == 0.0
|
|
|
|
def test_one_empty(self):
|
|
"""Test one empty returns 1.0."""
|
|
assert line_diff_ratio("", "content") == 1.0
|
|
assert line_diff_ratio("content", "") == 1.0
|
|
|
|
|
|
class TestCalculateChangeMagnitude:
|
|
"""Tests for calculate_change_magnitude."""
|
|
|
|
def test_none_old_content(self):
|
|
"""Test None old_content (creation) returns 1.0."""
|
|
assert calculate_change_magnitude(None, "new content") == 1.0
|
|
|
|
def test_none_new_content(self):
|
|
"""Test None new_content (deletion) returns 1.0."""
|
|
assert calculate_change_magnitude("old content", None) == 1.0
|
|
|
|
def test_both_none(self):
|
|
"""Test both None returns 0.0."""
|
|
assert calculate_change_magnitude(None, None) == 0.0
|
|
|
|
def test_structural_method(self):
|
|
"""Test structural method (default)."""
|
|
result = calculate_change_magnitude("abc", "abd", method="structural")
|
|
assert 0.0 < result < 1.0
|
|
|
|
def test_line_method(self):
|
|
"""Test line method."""
|
|
result = calculate_change_magnitude("abc\ndef", "abc\nxyz", method="line")
|
|
assert 0.0 < result < 1.0
|
|
|
|
def test_identical_content(self):
|
|
"""Test identical content returns 0.0."""
|
|
assert calculate_change_magnitude("same", "same") == 0.0
|
|
|
|
|
|
class TestImpactAnalyzer:
|
|
"""Tests for ImpactAnalyzer class."""
|
|
|
|
@pytest.fixture
|
|
def analyzer(self):
|
|
"""Create ImpactAnalyzer instance."""
|
|
return ImpactAnalyzer()
|
|
|
|
def test_calculate_magnitude(self, analyzer):
|
|
"""Test magnitude calculation delegates to metrics."""
|
|
result = analyzer.calculate_magnitude("old", "new")
|
|
assert isinstance(result, float)
|
|
assert 0.0 <= result <= 1.0
|
|
|
|
def test_calculate_magnitude_creation(self, analyzer):
|
|
"""Test magnitude for creation."""
|
|
assert analyzer.calculate_magnitude(None, "new") == 1.0
|
|
|
|
def test_calculate_magnitude_identical(self, analyzer):
|
|
"""Test magnitude for identical content."""
|
|
assert analyzer.calculate_magnitude("same", "same") == 0.0
|
|
|
|
def test_should_recompute_above_threshold(self, analyzer):
|
|
"""Test recompute when magnitude exceeds threshold."""
|
|
config = RecomputeConfig(impact_threshold=0.3)
|
|
assert analyzer.should_recompute(0.5, config) is True
|
|
|
|
def test_should_recompute_at_threshold(self, analyzer):
|
|
"""Test recompute when magnitude equals threshold."""
|
|
config = RecomputeConfig(impact_threshold=0.5)
|
|
assert analyzer.should_recompute(0.5, config) is True
|
|
|
|
def test_should_not_recompute_below_threshold(self, analyzer):
|
|
"""Test no recompute when magnitude below threshold."""
|
|
config = RecomputeConfig(impact_threshold=0.5)
|
|
assert analyzer.should_recompute(0.3, config) is False
|
|
|
|
def test_zero_threshold_always_recomputes(self, analyzer):
|
|
"""Test zero threshold means any change triggers recompute."""
|
|
config = RecomputeConfig(impact_threshold=0.0)
|
|
assert analyzer.should_recompute(0.0, config) is True
|
|
assert analyzer.should_recompute(0.01, config) is True
|
|
|
|
def test_high_threshold_only_major_changes(self, analyzer):
|
|
"""Test high threshold only triggers on major changes."""
|
|
config = RecomputeConfig(impact_threshold=0.9)
|
|
assert analyzer.should_recompute(0.5, config) is False
|
|
assert analyzer.should_recompute(0.95, config) is True
|