# tests/unit/services/memory/semantic/test_verification.py """Unit tests for fact verification.""" from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock from uuid import uuid4 import pytest from app.services.memory.semantic.verification import ( FactConflict, FactVerifier, VerificationResult, ) def create_mock_fact_model( subject="FastAPI", predicate="uses", obj="Starlette", confidence=0.8, project_id=None, ): """Create a mock fact model for testing.""" mock = MagicMock() mock.id = uuid4() mock.project_id = project_id mock.subject = subject mock.predicate = predicate mock.object = obj mock.confidence = confidence mock.source_episode_ids = [] mock.first_learned = datetime.now(UTC) mock.last_reinforced = datetime.now(UTC) mock.reinforcement_count = 1 mock.embedding = None mock.created_at = datetime.now(UTC) mock.updated_at = datetime.now(UTC) return mock class TestFactConflict: """Tests for FactConflict dataclass.""" def test_to_dict(self) -> None: """Test converting conflict to dictionary.""" conflict = FactConflict( fact_a_id=uuid4(), fact_b_id=uuid4(), conflict_type="contradiction", description="Test conflict", suggested_resolution="Keep higher confidence", ) result = conflict.to_dict() assert "fact_a_id" in result assert "fact_b_id" in result assert result["conflict_type"] == "contradiction" assert result["description"] == "Test conflict" class TestVerificationResult: """Tests for VerificationResult dataclass.""" def test_default_values(self) -> None: """Test default values.""" result = VerificationResult(is_valid=True) assert result.is_valid is True assert result.confidence_adjustment == 0.0 assert result.conflicts == [] assert result.supporting_facts == [] assert result.messages == [] class TestFactVerifier: """Tests for FactVerifier class.""" @pytest.fixture def mock_session(self) -> AsyncMock: """Create a mock database session.""" session = AsyncMock() mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [] session.execute.return_value = mock_result return session @pytest.fixture def verifier(self, mock_session: AsyncMock) -> FactVerifier: """Create a fact verifier.""" return FactVerifier(session=mock_session) @pytest.mark.asyncio async def test_verify_fact_valid( self, verifier: FactVerifier, ) -> None: """Test verifying a valid fact with no conflicts.""" result = await verifier.verify_fact( subject="Python", predicate="is_a", obj="programming language", ) assert result.is_valid is True assert len(result.conflicts) == 0 @pytest.mark.asyncio async def test_verify_fact_with_support( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test verifying a fact with supporting facts.""" # Mock finding supporting facts supporting = [create_mock_fact_model()] # First query: contradictions (empty) contradiction_result = MagicMock() contradiction_result.scalars.return_value.all.return_value = [] # Second query: supporting facts support_result = MagicMock() support_result.scalars.return_value.all.return_value = supporting mock_session.execute.side_effect = [contradiction_result, support_result] result = await verifier.verify_fact( subject="Python", predicate="uses", obj="dynamic typing", ) assert result.is_valid is True assert len(result.supporting_facts) >= 1 assert result.confidence_adjustment > 0 @pytest.mark.asyncio async def test_verify_fact_with_contradiction( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test verifying a fact with contradictions.""" # Mock finding contradicting fact contradicting = create_mock_fact_model( subject="Python", predicate="does_not_use", obj="static typing", ) contradiction_result = MagicMock() contradiction_result.scalars.return_value.all.return_value = [contradicting] support_result = MagicMock() support_result.scalars.return_value.all.return_value = [] mock_session.execute.side_effect = [contradiction_result, support_result] result = await verifier.verify_fact( subject="Python", predicate="uses", obj="static typing", ) assert result.is_valid is False assert len(result.conflicts) >= 1 assert result.confidence_adjustment < 0 def test_get_opposite_predicates( self, verifier: FactVerifier, ) -> None: """Test getting opposite predicates.""" opposites = verifier._get_opposite_predicates("uses") assert "does_not_use" in opposites def test_get_opposite_predicates_unknown( self, verifier: FactVerifier, ) -> None: """Test getting opposites for unknown predicate.""" opposites = verifier._get_opposite_predicates("unknown_predicate") assert opposites == [] @pytest.mark.asyncio async def test_find_all_conflicts_empty( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test finding all conflicts in empty fact base.""" mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [] mock_session.execute.return_value = mock_result conflicts = await verifier.find_all_conflicts() assert conflicts == [] @pytest.mark.asyncio async def test_find_all_conflicts_no_conflicts( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test finding conflicts when there are none.""" # Two facts with different subjects fact1 = create_mock_fact_model(subject="Python", predicate="uses") fact2 = create_mock_fact_model(subject="JavaScript", predicate="uses") mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [fact1, fact2] mock_session.execute.return_value = mock_result conflicts = await verifier.find_all_conflicts() assert conflicts == [] @pytest.mark.asyncio async def test_find_all_conflicts_with_contradiction( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test finding contradicting facts.""" # Two contradicting facts fact1 = create_mock_fact_model( subject="Python", predicate="best_practice", obj="Use type hints", ) fact2 = create_mock_fact_model( subject="Python", predicate="anti_pattern", obj="Use type hints", ) mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [fact1, fact2] mock_session.execute.return_value = mock_result conflicts = await verifier.find_all_conflicts() assert len(conflicts) == 1 assert conflicts[0].conflict_type == "contradiction" @pytest.mark.asyncio async def test_get_fact_reliability_score_not_found( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test reliability score for non-existent fact.""" mock_result = MagicMock() mock_result.scalar_one_or_none.return_value = None mock_session.execute.return_value = mock_result score = await verifier.get_fact_reliability_score(uuid4()) assert score == 0.0 @pytest.mark.asyncio async def test_get_fact_reliability_score( self, verifier: FactVerifier, mock_session: AsyncMock, ) -> None: """Test calculating reliability score.""" fact = create_mock_fact_model(confidence=0.8) fact.reinforcement_count = 5 # Query 1: Get fact fact_result = MagicMock() fact_result.scalar_one_or_none.return_value = fact # Query 2: Supporting facts support_result = MagicMock() support_result.scalars.return_value.all.return_value = [] # Query 3: Contradictions conflict_result = MagicMock() conflict_result.scalars.return_value.all.return_value = [] mock_session.execute.side_effect = [ fact_result, support_result, conflict_result, ] score = await verifier.get_fact_reliability_score(fact.id) # Score should be >= confidence (0.8) due to reinforcement bonus assert score >= 0.8 assert score <= 1.0