mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #902 from Devansh070/test-conflicts-865
test(conflicts): add coverage for 4 resolution strategies and 3 conflict types
This commit is contained in:
@@ -238,6 +238,297 @@ class TestConflictsModule(unittest.TestCase):
|
||||
checklist = generator.export_investigation_checklist(guide, format="text")
|
||||
self.assertIn("INVESTIGATION GUIDE: c1", checklist)
|
||||
|
||||
def test_conflict_resolver_credibility_weighted(self):
|
||||
"""Test credibility-weighted resolution strategy (#865)."""
|
||||
resolver = ConflictResolver()
|
||||
|
||||
# Boost the credibility of "doc_trusted" so its value should win
|
||||
# even though it only has one vote, vs. two lower-credibility votes.
|
||||
# Use the validated setter rather than mutating the internal dict
|
||||
# directly, so the test stays coupled to the public API surface.
|
||||
resolver.source_tracker.set_source_credibility("doc_trusted", 1.0)
|
||||
resolver.source_tracker.set_source_credibility("doc_flaky", 0.1)
|
||||
|
||||
conflict = Conflict(
|
||||
conflict_id="c_cred",
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 30, 40],
|
||||
sources=[
|
||||
{"document": "doc_flaky", "confidence": 0.9},
|
||||
{"document": "doc_flaky", "confidence": 0.9},
|
||||
{"document": "doc_trusted", "confidence": 0.9},
|
||||
],
|
||||
)
|
||||
|
||||
result = resolver.resolve_conflict(conflict, strategy="credibility_weighted")
|
||||
self.assertTrue(result.resolved)
|
||||
self.assertEqual(result.resolved_value, 40)
|
||||
self.assertEqual(result.resolution_strategy, "credibility_weighted")
|
||||
self.assertGreater(result.confidence, 0.0)
|
||||
|
||||
def test_conflict_resolver_first_seen(self):
|
||||
"""Test first-seen resolution strategy (#865)."""
|
||||
resolver = ConflictResolver()
|
||||
|
||||
conflict = Conflict(
|
||||
conflict_id="c_first",
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 32],
|
||||
sources=[
|
||||
{"document": "doc1", "confidence": 0.9},
|
||||
{"document": "doc2", "confidence": 0.9},
|
||||
],
|
||||
)
|
||||
|
||||
result = resolver.resolve_conflict(conflict, strategy="first_seen")
|
||||
self.assertTrue(result.resolved)
|
||||
self.assertEqual(result.resolved_value, 30) # first value in the list
|
||||
self.assertEqual(result.resolution_strategy, "first_seen")
|
||||
self.assertEqual(result.sources_used, ["doc1"])
|
||||
|
||||
def test_conflict_resolver_manual_review(self):
|
||||
"""Test manual-review resolution strategy flags without resolving (#865)."""
|
||||
resolver = ConflictResolver()
|
||||
|
||||
conflict = Conflict(
|
||||
conflict_id="c_manual",
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 32],
|
||||
sources=[{"document": "doc1"}, {"document": "doc2"}],
|
||||
severity="high",
|
||||
)
|
||||
|
||||
result = resolver.resolve_conflict(conflict, strategy="manual_review")
|
||||
self.assertFalse(result.resolved)
|
||||
self.assertEqual(result.resolution_strategy, "manual_review")
|
||||
self.assertTrue(result.metadata.get("requires_manual_review"))
|
||||
self.assertEqual(result.metadata.get("severity"), "high")
|
||||
|
||||
def test_conflict_resolver_expert_review(self):
|
||||
"""Test expert-review resolution strategy flags without resolving (#865)."""
|
||||
resolver = ConflictResolver()
|
||||
|
||||
conflict = Conflict(
|
||||
conflict_id="c_expert",
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 32],
|
||||
sources=[{"document": "doc1"}, {"document": "doc2"}],
|
||||
severity="critical",
|
||||
)
|
||||
|
||||
result = resolver.resolve_conflict(conflict, strategy="expert_review")
|
||||
self.assertFalse(result.resolved)
|
||||
self.assertEqual(result.resolution_strategy, "expert_review")
|
||||
self.assertTrue(result.metadata.get("requires_expert_review"))
|
||||
self.assertEqual(result.metadata.get("severity"), "critical")
|
||||
|
||||
def test_conflict_detector_relationship_conflicts(self):
|
||||
"""Test relationship conflict detection (#865)."""
|
||||
detector = ConflictDetector()
|
||||
|
||||
relationships = [
|
||||
{
|
||||
"id": "r1",
|
||||
"source_id": "e1",
|
||||
"target_id": "e2",
|
||||
"type": "works_at",
|
||||
"source": "doc1",
|
||||
},
|
||||
{
|
||||
"id": "r1",
|
||||
"source_id": "e1",
|
||||
"target_id": "e2",
|
||||
"type": "founded",
|
||||
"source": "doc2",
|
||||
},
|
||||
]
|
||||
|
||||
conflicts = detector.detect_relationship_conflicts(relationships)
|
||||
self.assertEqual(len(conflicts), 1)
|
||||
conflict = conflicts[0]
|
||||
self.assertEqual(conflict.conflict_type, ConflictType.RELATIONSHIP_CONFLICT)
|
||||
self.assertEqual(conflict.relationship_id, "r1")
|
||||
self.assertEqual(conflict.property_name, "type")
|
||||
self.assertIn("works_at", conflict.conflicting_values)
|
||||
self.assertIn("founded", conflict.conflicting_values)
|
||||
|
||||
def test_conflict_detector_relationship_conflicts_no_conflict(self):
|
||||
"""Relationships with a single occurrence should not raise conflicts (#865)."""
|
||||
detector = ConflictDetector()
|
||||
|
||||
relationships = [
|
||||
{"id": "r1", "source_id": "e1", "target_id": "e2", "type": "works_at"},
|
||||
]
|
||||
|
||||
conflicts = detector.detect_relationship_conflicts(relationships)
|
||||
self.assertEqual(len(conflicts), 0)
|
||||
|
||||
def test_conflict_detector_temporal_conflicts(self):
|
||||
"""Test temporal conflict detection (#865)."""
|
||||
detector = ConflictDetector()
|
||||
|
||||
entities = [
|
||||
{"id": "e1", "founded": "1998", "source": "doc1", "confidence": 0.9},
|
||||
{"id": "e1", "founded": "2004", "source": "doc2", "confidence": 0.8},
|
||||
]
|
||||
|
||||
conflicts = detector.detect_temporal_conflicts(entities)
|
||||
self.assertEqual(len(conflicts), 1)
|
||||
conflict = conflicts[0]
|
||||
self.assertEqual(conflict.conflict_type, ConflictType.TEMPORAL_CONFLICT)
|
||||
self.assertEqual(conflict.entity_id, "e1")
|
||||
self.assertEqual(conflict.property_name, "founded")
|
||||
self.assertIn("1998", conflict.conflicting_values)
|
||||
self.assertIn("2004", conflict.conflicting_values)
|
||||
|
||||
def test_conflict_detector_temporal_conflicts_no_conflict(self):
|
||||
"""Matching temporal values across sources should not raise conflicts (#865)."""
|
||||
detector = ConflictDetector()
|
||||
|
||||
entities = [
|
||||
{"id": "e1", "founded": "1998", "source": "doc1"},
|
||||
{"id": "e1", "founded": "1998", "source": "doc2"},
|
||||
]
|
||||
|
||||
conflicts = detector.detect_temporal_conflicts(entities)
|
||||
self.assertEqual(len(conflicts), 0)
|
||||
|
||||
def test_conflict_detector_logical_conflicts(self):
|
||||
"""Test logical conflict detection for incompatible entity types (#865)."""
|
||||
detector = ConflictDetector()
|
||||
|
||||
entities = [
|
||||
{"id": "e1", "type": "Person", "source": "doc1"},
|
||||
{"id": "e1", "type": "Organization", "source": "doc2"},
|
||||
]
|
||||
|
||||
conflicts = detector.detect_logical_conflicts(entities)
|
||||
self.assertEqual(len(conflicts), 1)
|
||||
conflict = conflicts[0]
|
||||
self.assertEqual(conflict.conflict_type, ConflictType.LOGICAL_CONFLICT)
|
||||
self.assertEqual(conflict.entity_id, "e1")
|
||||
self.assertEqual(conflict.severity, "critical")
|
||||
self.assertIn("Person", conflict.conflicting_values)
|
||||
self.assertIn("Organization", conflict.conflicting_values)
|
||||
|
||||
def test_conflict_detector_logical_conflicts_compatible_types(self):
|
||||
"""Compatible/unrelated types should not raise logical conflicts (#865)."""
|
||||
detector = ConflictDetector()
|
||||
|
||||
entities = [
|
||||
{"id": "e1", "type": "Person", "source": "doc1"},
|
||||
{"id": "e1", "type": "Employee", "source": "doc2"},
|
||||
]
|
||||
|
||||
conflicts = detector.detect_logical_conflicts(entities)
|
||||
self.assertEqual(len(conflicts), 0)
|
||||
|
||||
def test_conflict_analyzer_by_source_breakdown(self):
|
||||
"""Test the by_source breakdown of analyze_conflicts (#902)."""
|
||||
analyzer = ConflictAnalyzer()
|
||||
|
||||
conflicts = [
|
||||
Conflict(
|
||||
conflict_id="c1",
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 32],
|
||||
sources=[{"document": "doc1"}, {"document": "doc2"}],
|
||||
severity="medium",
|
||||
),
|
||||
Conflict(
|
||||
conflict_id="c2",
|
||||
conflict_type=ConflictType.TYPE_CONFLICT,
|
||||
entity_id="e2",
|
||||
property_name="type",
|
||||
conflicting_values=["Person", "Org"],
|
||||
sources=[{"document": "doc1"}, {"document": "doc3"}],
|
||||
severity="critical",
|
||||
),
|
||||
]
|
||||
|
||||
analysis = analyzer.analyze_conflicts(conflicts)
|
||||
|
||||
self.assertIn("by_source", analysis)
|
||||
by_source = analysis["by_source"]
|
||||
|
||||
# doc1 appears in both conflicts, doc2 and doc3 in one each.
|
||||
self.assertEqual(by_source["counts"]["doc1"], 2)
|
||||
self.assertEqual(by_source["counts"]["doc2"], 1)
|
||||
self.assertEqual(by_source["counts"]["doc3"], 1)
|
||||
|
||||
top_sources = {
|
||||
s["source"]: s["conflict_count"] for s in by_source["top_sources"]
|
||||
}
|
||||
self.assertEqual(top_sources["doc1"], 2)
|
||||
|
||||
self.assertIn("doc1", by_source["details"])
|
||||
doc1_entries = by_source["details"]["doc1"]
|
||||
self.assertEqual(len(doc1_entries), 2)
|
||||
self.assertEqual({e["conflict_id"] for e in doc1_entries}, {"c1", "c2"})
|
||||
|
||||
def test_conflict_analyzer_analyze_trends(self):
|
||||
"""Test analyze_trends over deterministic, time-ordered data (#902)."""
|
||||
analyzer = ConflictAnalyzer()
|
||||
|
||||
def make_conflict(conflict_id, timestamp):
|
||||
return Conflict(
|
||||
conflict_id=conflict_id,
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 32],
|
||||
sources=[{"document": "doc1", "metadata": {"timestamp": timestamp}}],
|
||||
)
|
||||
|
||||
# January: 1 conflict. February: 3 conflicts (>10% increase -> "increasing").
|
||||
conflicts = [
|
||||
make_conflict("c1", "2023-01-05T00:00:00"),
|
||||
make_conflict("c2", "2023-02-01T00:00:00"),
|
||||
make_conflict("c3", "2023-02-10T00:00:00"),
|
||||
make_conflict("c4", "2023-02-20T00:00:00"),
|
||||
]
|
||||
|
||||
trends = analyzer.analyze_trends(conflicts)
|
||||
|
||||
self.assertEqual(len(trends), 2)
|
||||
self.assertEqual(trends[0]["period"], "2023-01")
|
||||
self.assertEqual(trends[0]["conflict_count"], 1)
|
||||
self.assertEqual(trends[1]["period"], "2023-02")
|
||||
self.assertEqual(trends[1]["conflict_count"], 3)
|
||||
self.assertEqual(trends[1]["trend"], "increasing")
|
||||
self.assertEqual(trends[1]["trend_direction"], "up")
|
||||
|
||||
def test_conflict_analyzer_analyze_trends_insufficient_data(self):
|
||||
"""Single-period data should report insufficient_data, not crash (#902)."""
|
||||
analyzer = ConflictAnalyzer()
|
||||
|
||||
conflict = Conflict(
|
||||
conflict_id="c1",
|
||||
conflict_type=ConflictType.VALUE_CONFLICT,
|
||||
entity_id="e1",
|
||||
property_name="age",
|
||||
conflicting_values=[30, 32],
|
||||
sources=[
|
||||
{"document": "doc1", "metadata": {"timestamp": "2023-01-05T00:00:00"}}
|
||||
],
|
||||
)
|
||||
|
||||
trends = analyzer.analyze_trends([conflict])
|
||||
|
||||
self.assertEqual(len(trends), 1)
|
||||
self.assertEqual(trends[0]["trend"], "insufficient_data")
|
||||
self.assertEqual(trends[0]["conflict_count"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user