""" Integration tests for the Representation class and related workflows. This test suite covers the full representation workflow including: - Document creation with embedding and duplicate detection - Representation building from documents - Representation merging and diffing operations - Working representation retrieval with different strategies """ from datetime import datetime, timezone from unittest.mock import patch import pytest from nanoid import generate as generate_nanoid from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src import crud, models from src.utils.representation import ( DeductiveObservation, # DeductiveObservationBase, ExplicitObservation, ExplicitObservationBase, PromptRepresentation, Representation, ) @pytest.fixture def fixed_embedding_vector() -> list[float]: """ Fixture providing a deterministic embedding vector for hermetic tests. Returns a 1536-dimensional vector with predictable values to avoid network calls to external embedding services during testing. """ # Create a deterministic 1536-dimensional embedding vector # Using a simple pattern that's easy to verify in tests return [0.1 * (i % 10) for i in range(1536)] @pytest.mark.asyncio class TestRepresentationWorkflow: """Test suite for the complete representation workflow""" async def create_test_workspace_and_peer( self, db_session: AsyncSession, workspace_name: str | None = None, peer_name: str | None = None, ) -> tuple[models.Workspace, models.Peer]: """Helper to create test workspace and peer""" workspace_name = workspace_name or generate_nanoid() peer_name = peer_name or generate_nanoid() workspace = models.Workspace(name=workspace_name) db_session.add(workspace) await db_session.flush() peer = models.Peer(name=peer_name, workspace_name=workspace_name) db_session.add(peer) await db_session.flush() return workspace, peer async def create_test_session( self, db_session: AsyncSession, workspace: models.Workspace, session_name: str | None = None, ) -> models.Session: """Helper to create test session""" session_name = session_name or generate_nanoid() session = models.Session( name=session_name, workspace_name=workspace.name, ) db_session.add(session) await db_session.flush() return session async def test_representation_class_creation_and_operations(self): """Test basic Representation class operations""" # Create explicit observations explicit_obs1 = ExplicitObservation( content="User likes dogs", created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="test_session", ) explicit_obs2 = ExplicitObservation( content="User has a pet named Rover", created_at=datetime(2025, 1, 1, 12, 1, 0, tzinfo=timezone.utc), message_ids=[2], session_name="test_session", ) # Create deductive observations deductive_obs1 = DeductiveObservation( conclusion="User probably has a dog named Rover", premises=["User likes dogs", "User has a pet named Rover"], created_at=datetime(2025, 1, 1, 12, 2, 0, tzinfo=timezone.utc), message_ids=[3], session_name="test_session", ) # Create representation representation = Representation( explicit=[explicit_obs1, explicit_obs2], deductive=[deductive_obs1] ) # Test basic properties assert not representation.is_empty() assert len(representation.explicit) == 2 assert len(representation.deductive) == 1 # Test string formatting str_output = str(representation) assert "EXPLICIT:" in str_output assert "DEDUCTIVE:" in str_output assert "User likes dogs" in str_output assert "User probably has a dog named Rover" in str_output # Test no-timestamp formatting no_timestamp_output = representation.str_no_timestamps() assert "User likes dogs" in no_timestamp_output assert "[" not in no_timestamp_output # Test markdown formatting markdown_output = representation.format_as_markdown() assert "## Explicit Observations" in markdown_output assert "## Deductive Observations" in markdown_output assert "User probably has a dog named Rover" in markdown_output assert "Premises:" in markdown_output async def test_representation_merging_and_diffing(self): """Test representation merge and diff operations""" # Create first representation rep1 = Representation( explicit=[ ExplicitObservation( content="User likes cats", created_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) ] ) # Create second representation with some overlap rep2 = Representation( explicit=[ ExplicitObservation( content="User likes cats", # Duplicate created_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ), ExplicitObservation( content="User likes dogs", # New created_at=datetime(2025, 1, 1, 11, 0, 0, tzinfo=timezone.utc), message_ids=[2], session_name="session1", ), ] ) # Test diff - should only return new observations diff = rep1.diff_representation(rep2) assert len(diff.explicit) == 1 assert diff.explicit[0].content == "User likes dogs" # Test merge - should deduplicate and preserve order rep1.merge_representation(rep2) assert len(rep1.explicit) == 2 # Should be sorted by created_at assert rep1.explicit[0].content == "User likes cats" assert rep1.explicit[1].content == "User likes dogs" # Test merge with max_observations limit rep3 = Representation( explicit=[ ExplicitObservation( content="User likes birds", created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[3], session_name="session1", ) ] ) rep1.merge_representation(rep3, max_observations=2) assert len(rep1.explicit) == 2 # Should keep the most recent observations (FIFO) assert rep1.explicit[0].content == "User likes dogs" assert rep1.explicit[1].content == "User likes birds" @pytest.mark.asyncio class TestDocumentCreationWorkflow: """Test document creation with embedding and duplicate detection""" async def test_get_working_representation_with_semantic_query( self, db_session: AsyncSession ): """Test working representation retrieval with semantic query""" workspace, observer_peer = await self.create_test_workspace_and_peer(db_session) _, observed_peer = await self.create_test_workspace_and_peer( db_session, workspace.name ) # Mock semantic search to return specific documents from src.models import Document mock_document = Document( id="test_doc_id", workspace_name=workspace.name, observer=observer_peer.name, observed=observed_peer.name, content="User likes dogs", session_name="test_session", level="explicit", internal_metadata={ "message_ids": [1], "session_name": "test_session", }, created_at=datetime.now(timezone.utc), ) with patch( "src.crud.representation.RepresentationManager._query_documents_semantic" ) as mock_semantic: mock_semantic.return_value = [mock_document] representation = await crud.get_working_representation( workspace.name, include_semantic_query="pets dogs animals", observer=observer_peer.name, observed=observed_peer.name, ) # Should have called semantic search mock_semantic.assert_called_once() assert len(representation.explicit) == 1 assert representation.explicit[0].content == "User likes dogs" async def test_get_working_representation_with_include_most_frequent( self, db_session: AsyncSession ): """Test working representation retrieval prioritizing most frequent observations""" workspace, observer_peer = await self.create_test_workspace_and_peer(db_session) _, observed_peer = await self.create_test_workspace_and_peer( db_session, workspace.name ) session = await self.create_test_session(db_session, workspace) # Create collection first - need to do it directly since mock doesn't persist to DB collection = models.Collection( observer=observer_peer.name, observed=observed_peer.name, workspace_name=workspace.name, ) db_session.add(collection) await db_session.flush() # Create document with high times_derived count highly_derived_doc = models.Document( workspace_name=workspace.name, observer=observer_peer.name, observed=observed_peer.name, content="Highly derived observation", session_name=session.name, level="explicit", times_derived=5, internal_metadata={}, ) db_session.add(highly_derived_doc) # Create document with lower times_derived count less_derived_doc = models.Document( workspace_name=workspace.name, observer=observer_peer.name, observed=observed_peer.name, content="Less derived observation", session_name=session.name, level="explicit", times_derived=2, internal_metadata={}, ) db_session.add(less_derived_doc) await db_session.commit() # Retrieve with include_most_frequent=True representation = await crud.get_working_representation( workspace.name, include_most_derived=True, observer=observer_peer.name, observed=observed_peer.name, ) # Should prioritize highly derived observation assert len(representation.explicit) >= 1 # The highly derived observation should be included contents = [obs.content for obs in representation.explicit] assert "Highly derived observation" in contents async def test_representation_from_documents(self): """Test converting documents to representation""" # Create test documents with IDs (simulating database-assigned IDs) explicit_doc = models.Document( id="test_explicit_doc_id", workspace_name="test_workspace", observer="test_peer", observed="test_peer", content="User said they like programming", level="explicit", internal_metadata={ "message_ids": [1], }, session_name="test_session", created_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc), ) deductive_doc = models.Document( id="test_deductive_doc_id", workspace_name="test_workspace", observer="test_peer", observed="test_peer", content="User is likely a software developer", level="deductive", internal_metadata={ "message_ids": [1], "premises": ["User said they like programming"], }, session_name="test_session", created_at=datetime(2025, 1, 1, 10, 1, 0, tzinfo=timezone.utc), ) # Convert to representation representation = Representation.from_documents([explicit_doc, deductive_doc]) assert len(representation.explicit) == 1 assert len(representation.deductive) == 1 explicit_obs = representation.explicit[0] assert explicit_obs.content == "User said they like programming" assert explicit_obs.message_ids == [1] assert explicit_obs.session_name == "test_session" deductive_obs = representation.deductive[0] assert deductive_obs.conclusion == "User is likely a software developer" assert deductive_obs.premises == ["User said they like programming"] assert deductive_obs.message_ids == [1] assert deductive_obs.session_name == "test_session" async def create_test_workspace_and_peer( self, db_session: AsyncSession, workspace_name: str | None = None ) -> tuple[models.Workspace, models.Peer]: """Helper to create test workspace and peer""" workspace_name = workspace_name or generate_nanoid() peer_name = generate_nanoid() # Check if workspace already exists to avoid uniqueness constraint workspace = ( await db_session.execute( select(models.Workspace).where(models.Workspace.name == workspace_name) ) ).scalar_one_or_none() if workspace is None: workspace = models.Workspace(name=workspace_name) db_session.add(workspace) await db_session.flush() peer = models.Peer(name=peer_name, workspace_name=workspace_name) db_session.add(peer) await db_session.flush() return workspace, peer async def create_test_session( self, db_session: AsyncSession, workspace: models.Workspace ) -> models.Session: """Helper to create test session""" session_name = generate_nanoid() session = models.Session( name=session_name, workspace_name=workspace.name, ) db_session.add(session) await db_session.flush() return session @pytest.mark.asyncio class TestPromptRepresentationConversion: """Test conversion between PromptRepresentation and Representation""" async def test_prompt_representation_to_representation(self): """Test converting PromptRepresentation to Representation. Note: In the current architecture, the Deriver only creates explicit observations. Deductive and inductive observations are created by the Dreamer agent. Therefore, from_prompt_representation only converts explicit observations. """ prompt_rep = PromptRepresentation( explicit=[ ExplicitObservationBase(content="User likes coffee"), ExplicitObservationBase(content="User works remotely"), ], ) timestamp = datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc) representation = Representation.from_prompt_representation( prompt_rep, message_ids=[123], session_name="test_session", created_at=timestamp, ) assert len(representation.explicit) == 2 # Deductive observations from PromptRepresentation are not converted # (they would be created directly by the Dreamer via the create_observations tool) assert len(representation.deductive) == 0 # Check explicit observations assert representation.explicit[0].content == "User likes coffee" assert representation.explicit[0].message_ids == [123] assert representation.explicit[0].session_name == "test_session" assert representation.explicit[1].content == "User works remotely" assert representation.explicit[0].created_at == timestamp async def test_empty_prompt_representation_conversion(self): """Test converting empty PromptRepresentation""" empty_prompt_rep = PromptRepresentation() representation = Representation.from_prompt_representation( empty_prompt_rep, message_ids=[1], session_name="test", created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), ) assert representation.is_empty() assert len(representation.explicit) == 0 assert len(representation.deductive) == 0 @pytest.mark.asyncio class TestRepresentationHashingAndEquality: """Test hashing and equality behavior for observations""" async def test_explicit_observation_equality(self): """Test ExplicitObservation equality and hashing""" obs1 = ExplicitObservation( content="Test content", created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) obs2 = ExplicitObservation( content="Test content", created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) obs3 = ExplicitObservation( content="Different content", created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) # Test equality assert obs1 == obs2 assert obs1 != obs3 assert obs1 != "not an observation" # Test hashing (should be able to use in sets) obs_set = {obs1, obs2, obs3} assert len(obs_set) == 2 # obs1 and obs2 are duplicates async def test_deductive_observation_equality(self): """Test DeductiveObservation equality and hashing""" obs1 = DeductiveObservation( conclusion="Test conclusion", premises=["premise1", "premise2"], created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) obs2 = DeductiveObservation( conclusion="Test conclusion", premises=["premise1", "premise2"], created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) obs3 = DeductiveObservation( conclusion="Different conclusion", premises=["premise1", "premise2"], created_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), message_ids=[1], session_name="session1", ) # Test equality assert obs1 == obs2 assert obs1 != obs3 assert obs1 != "not an observation" # Test hashing obs_set = {obs1, obs2, obs3} assert len(obs_set) == 2