import datetime from typing import Any import pytest from fastapi.testclient import TestClient from nanoid import generate as generate_nanoid from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from src import crud, models from src.config import settings from src.models import Peer, Workspace from src.security import JWTParams, create_jwt def test_get_or_create_peer(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, _ = sample_data name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": {"peer_key": "peer_value"}}, ) assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["metadata"] == {"peer_key": "peer_value"} assert "id" in data def test_get_or_create_peer_with_configuration( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer creation with configuration parameter""" test_workspace, _ = sample_data name = str(generate_nanoid()) configuration = {"experimental": True, "beta": False} response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": name, "configuration": configuration}, ) assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["configuration"] == configuration def test_get_or_create_peer_with_all_optional_params( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer creation with all optional parameters""" test_workspace, _ = sample_data name = str(generate_nanoid()) metadata = {"key": "value", "number": 42} configuration = {"feature1": True, "feature2": False} response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": metadata, "configuration": configuration}, ) assert response.status_code in [200, 201] data = response.json() assert data["id"] == name assert data["metadata"] == metadata assert data["configuration"] == configuration def test_get_or_create_existing_peer( client: TestClient, sample_data: tuple[Workspace, Peer] ): test_workspace, _ = sample_data name = str(generate_nanoid()) # Create the peer response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": {"peer_key": "peer_value"}}, ) assert response.status_code in [200, 201] peer1 = response.json() # Try to create the same peer again - should return existing peer response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": name, "metadata": {"peer_key": "peer_value"}}, ) assert response.status_code in [200, 201] peer2 = response.json() # Both should be the same peer assert peer1["id"] == peer2["id"] assert peer1["metadata"] == peer2["metadata"] def test_get_peers(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, _ = sample_data # Create a few peers with metadata response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": str(generate_nanoid()), "metadata": {"peer_key": "peer_value"}}, ) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": str(generate_nanoid()), "metadata": {"peer_key": "peer_value"}}, ) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": str(generate_nanoid()), "metadata": {"peer_key": "peer_value2"}}, ) # Get all peers response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={}, ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) > 0 # Get peers with simple filter (backward compatibility) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={"filters": {"metadata": {"peer_key": "peer_value"}}}, ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) == 2 assert data["items"][0]["metadata"]["peer_key"] == "peer_value" # Test new filter with NOT operator response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={"filters": {"NOT": [{"metadata": {"peer_key": "peer_value2"}}]}}, ) assert response.status_code == 200 data = response.json() assert "items" in data # Should find peers that don't have peer_key = "peer_value2" # This includes the 2 peers with "peer_value" + the sample peer with empty metadata assert len(data["items"]) == 3 def test_get_peers_with_empty_filter( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer listing with empty filter object""" test_workspace, _ = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={"filters": {}} ) assert response.status_code == 200 data = response.json() assert "items" in data assert isinstance(data["items"], list) def test_get_peers_with_null_filter( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer listing with null filter""" test_workspace, _ = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={"filters": None} ) assert response.status_code == 200 data = response.json() assert "items" in data assert isinstance(data["items"], list) def test_get_peers_with_reverse( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer listing with reverse creation-time ordering.""" test_workspace, _ = sample_data reverse_group = f"reverse-peers-{generate_nanoid()}" first_name = f"reverse-peer-a-{generate_nanoid()}" second_name = f"reverse-peer-b-{generate_nanoid()}" first_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": first_name, "metadata": {"reverse_group": reverse_group}}, ) assert first_response.status_code in [200, 201] second_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": second_name, "metadata": {"reverse_group": reverse_group}}, ) assert second_response.status_code in [200, 201] normal_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert normal_response.status_code == 200 reverse_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list?reverse=true", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert reverse_response.status_code == 200 assert [item["id"] for item in normal_response.json()["items"]] == [ first_name, second_name, ] assert [item["id"] for item in reverse_response.json()["items"]] == [ second_name, first_name, ] @pytest.mark.asyncio async def test_get_peers_reverse_uses_id_tiebreaker( client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): """Peers with identical created_at fall back to ordering by id (nanoid PK).""" test_workspace, _ = sample_data reverse_group = f"tiebreaker-peers-{generate_nanoid()}" shared_created_at = datetime.datetime( 2026, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc ) low_id = "A" * 21 high_id = "z" * 21 low_name = f"tie-low-peer-{generate_nanoid()}" high_name = f"tie-high-peer-{generate_nanoid()}" db_session.add( models.Peer( id=low_id, name=low_name, workspace_name=test_workspace.name, created_at=shared_created_at, h_metadata={"reverse_group": reverse_group}, ) ) db_session.add( models.Peer( id=high_id, name=high_name, workspace_name=test_workspace.name, created_at=shared_created_at, h_metadata={"reverse_group": reverse_group}, ) ) await db_session.commit() normal_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert normal_response.status_code == 200 reverse_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list?reverse=true", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert reverse_response.status_code == 200 # When created_at ties, ordering falls back to the nanoid id: low_id < high_id # lexicographically, so low sorts first ascending and last descending. assert [item["id"] for item in normal_response.json()["items"]] == [ low_name, high_name, ] assert [item["id"] for item in reverse_response.json()["items"]] == [ high_name, low_name, ] def test_get_peers_reverse_with_pagination( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Paged reverse listing returns newest-first across consecutive pages.""" test_workspace, _ = sample_data reverse_group = f"paged-reverse-peers-{generate_nanoid()}" peer_names = [f"paged-reverse-peer-{i}-{generate_nanoid()}" for i in range(3)] for peer_name in peer_names: response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {"reverse_group": reverse_group}}, ) assert response.status_code in [200, 201] page_one = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list?reverse=true&page=1&size=1", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert page_one.status_code == 200 page_two = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list?reverse=true&page=2&size=1", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert page_two.status_code == 200 page_three = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list?reverse=true&page=3&size=1", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert page_three.status_code == 200 assert page_one.json()["total"] == 3 assert [item["id"] for item in page_one.json()["items"]] == [peer_names[2]] assert [item["id"] for item in page_two.json()["items"]] == [peer_names[1]] assert [item["id"] for item in page_three.json()["items"]] == [peer_names[0]] def test_update_peer(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"metadata": {"new_key": "new_value"}}, ) assert response.status_code == 200 data = response.json() assert data["metadata"] == {"new_key": "new_value"} def test_update_peer_with_configuration( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer update with configuration parameter""" test_workspace, test_peer = sample_data configuration = {"new_feature": True, "legacy_feature": False} response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"configuration": configuration}, ) assert response.status_code == 200 data = response.json() assert data["configuration"] == configuration def test_update_peer_with_all_optional_params( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer update with both metadata and configuration""" test_workspace, test_peer = sample_data metadata = {"updated_key": "updated_value", "count": 100} configuration = {"experimental": True, "beta": True} response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"metadata": metadata, "configuration": configuration}, ) assert response.status_code == 200 data = response.json() assert data["metadata"] == metadata assert data["configuration"] == configuration def test_update_peer_with_null_metadata( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer update with null metadata""" test_workspace, test_peer = sample_data # First set some metadata client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"metadata": {"temp": "value"}}, ) # Then clear it with null response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"metadata": None}, ) assert response.status_code == 200 data = response.json() assert "metadata" in data def test_update_peer_with_null_configuration( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer update with null configuration""" test_workspace, test_peer = sample_data response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"configuration": None}, ) assert response.status_code == 200 data = response.json() assert "configuration" in data def test_get_sessions_for_peer_no_sessions( client: TestClient, sample_data: tuple[Workspace, Peer] ): test_workspace, test_peer = sample_data # Get sessions for the peer response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions", json={}, ) assert response.status_code == 200 data = response.json() assert "items" in data def test_get_sessions_for_peer(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create session for the peer session_name = str(generate_nanoid()) create_response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_name, "peer_names": {test_peer.name: {}}}, ) assert create_response.status_code in [200, 201] created_session = create_response.json() assert created_session["id"] == session_name # Now get sessions for the peer and validate the session is returned response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions", ) assert response.status_code == 200 data = response.json() assert "items" in data # Check that the created session is in the returned items session_ids = [item["id"] for item in data["items"]] assert session_name in session_ids assert len(data["items"]) == 1 def test_get_sessions_for_peer_with_empty_filter( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test getting sessions for peer with empty filter object""" test_workspace, test_peer = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions", json={"filters": {}}, ) assert response.status_code == 200 data = response.json() assert "items" in data assert isinstance(data["items"], list) def test_get_sessions_for_peer_with_reverse( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer session listing with reverse creation-time ordering.""" test_workspace, test_peer = sample_data reverse_group = f"reverse-peer-sessions-{generate_nanoid()}" first_session = f"reverse-peer-session-a-{generate_nanoid()}" second_session = f"reverse-peer-session-b-{generate_nanoid()}" first_response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": first_session, "peer_names": {test_peer.name: {}}, "metadata": {"reverse_group": reverse_group}, }, ) assert first_response.status_code in [200, 201] second_response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": second_session, "peer_names": {test_peer.name: {}}, "metadata": {"reverse_group": reverse_group}, }, ) assert second_response.status_code in [200, 201] normal_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert normal_response.status_code == 200 reverse_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions?reverse=true", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert reverse_response.status_code == 200 assert [item["id"] for item in normal_response.json()["items"]] == [ first_session, second_session, ] assert [item["id"] for item in reverse_response.json()["items"]] == [ second_session, first_session, ] @pytest.mark.asyncio async def test_get_sessions_for_peer_reverse_uses_id_tiebreaker( client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): """Peer-scoped sessions with identical created_at fall back to ordering by id.""" test_workspace, test_peer = sample_data reverse_group = f"tiebreaker-peer-sessions-{generate_nanoid()}" shared_created_at = datetime.datetime( 2026, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc ) low_id = "A" * 21 high_id = "z" * 21 low_name = f"tie-low-peer-session-{generate_nanoid()}" high_name = f"tie-high-peer-session-{generate_nanoid()}" for session_id, session_name in ((low_id, low_name), (high_id, high_name)): db_session.add( models.Session( id=session_id, name=session_name, workspace_name=test_workspace.name, created_at=shared_created_at, h_metadata={"reverse_group": reverse_group}, ) ) db_session.add( models.SessionPeer( workspace_name=test_workspace.name, session_name=session_name, peer_name=test_peer.name, ) ) await db_session.commit() normal_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert normal_response.status_code == 200 reverse_response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions?reverse=true", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert reverse_response.status_code == 200 assert [item["id"] for item in normal_response.json()["items"]] == [ low_name, high_name, ] assert [item["id"] for item in reverse_response.json()["items"]] == [ high_name, low_name, ] def test_get_sessions_for_peer_reverse_with_pagination( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Paged reverse listing of a peer's sessions returns newest-first across pages.""" test_workspace, test_peer = sample_data reverse_group = f"paged-reverse-peer-sessions-{generate_nanoid()}" session_names = [ f"paged-reverse-peer-session-{i}-{generate_nanoid()}" for i in range(3) ] for session_name in session_names: response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_name, "peer_names": {test_peer.name: {}}, "metadata": {"reverse_group": reverse_group}, }, ) assert response.status_code in [200, 201] page_one = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions?reverse=true&page=1&size=1", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert page_one.status_code == 200 page_two = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions?reverse=true&page=2&size=1", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert page_two.status_code == 200 page_three = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions?reverse=true&page=3&size=1", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert page_three.status_code == 200 assert page_one.json()["total"] == 3 assert [item["id"] for item in page_one.json()["items"]] == [session_names[2]] assert [item["id"] for item in page_two.json()["items"]] == [session_names[1]] assert [item["id"] for item in page_three.json()["items"]] == [session_names[0]] def test_chat( client: TestClient, sample_data: tuple[Workspace, Peer], ): test_workspace, test_peer = sample_data target_peer = str(generate_nanoid()) # Test chat endpoint response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/chat", json={ "query": "Hello, how are you?", "stream": False, "target": target_peer, }, ) assert response.status_code == 200 data = response.json() assert "content" in data @pytest.mark.asyncio async def test_chat_peer_key_denied_for_non_member_session( client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], monkeypatch: pytest.MonkeyPatch, ): """A peer-scoped key cannot chat scoped to a session its peer is not a member of — the session id is in the body, so the handler checks membership. The guard fires before the dialectic runs, so no LLM call is made.""" test_workspace, alice = sample_data session_id = str(generate_nanoid()) # Session exists but alice is NOT a member of it. client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id}, ) await db_session.commit() monkeypatch.setattr(settings.AUTH, "USE_AUTH", True) monkeypatch.setattr(settings.AUTH, "JWT_SECRET", "test-secret") client.headers["Authorization"] = ( f"Bearer {create_jwt(JWTParams(w=test_workspace.name, p=alice.name))}" ) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{alice.name}/chat", json={"query": "what do you know?", "stream": False, "session_id": session_id}, ) assert response.status_code == 401 def test_chat_with_optional_params( client: TestClient, sample_data: tuple[Workspace, Peer], ): """Test chat endpoint with optional parameters""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create a session first client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) # Test chat without optional parameters response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/chat", json={ "query": "Hello, how are you?", "stream": False, "session_id": session_id, }, ) assert response.status_code == 200 data = response.json() assert "content" in data def test_get_peer_representation_with_session( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with session_id parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create a session first client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) # Test representation scoped to session response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "session_id": session_id, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data assert isinstance(data["representation"], str) def test_get_peer_representation_global( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation without session_id (global representation)""" test_workspace, test_peer = sample_data # Test global representation (no session_id) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={}, ) assert response.status_code == 200 data = response.json() assert "representation" in data assert isinstance(data["representation"], str) def test_get_peer_representation_with_target( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with target parameter""" test_workspace, test_peer = sample_data # Create a second peer to be the target target_peer_name = str(generate_nanoid()) client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name, "metadata": {}}, ) # Test representation of target from observer's perspective response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "target": target_peer_name, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data assert isinstance(data["representation"], str) def test_get_peer_representation_with_search_query( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with search_query parameter""" test_workspace, test_peer = sample_data # Test representation with semantic search query response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "What are my interests and hobbies?", }, ) assert response.status_code == 200 data = response.json() assert "representation" in data def test_get_peer_representation_with_search_top_k( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with search_top_k parameter""" test_workspace, test_peer = sample_data # Test with valid search_top_k values for top_k in [1, 10, 50, 100]: response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", "search_top_k": top_k, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data def test_get_peer_representation_with_search_max_distance( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with search_max_distance parameter""" test_workspace, test_peer = sample_data # Test with valid search_max_distance values (0.0 to 1.0) for max_distance in [0.0, 0.5, 0.8, 1.0]: response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", "search_max_distance": max_distance, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data def test_get_peer_representation_with_include_most_frequent( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with include_most_frequent parameter""" test_workspace, test_peer = sample_data # Test with include_most_frequent=True response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", "include_most_frequent": True, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data # Test with include_most_frequent=False response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", "include_most_frequent": False, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data def test_get_peer_representation_with_max_observations( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with max_observations parameter""" test_workspace, test_peer = sample_data # Test with various max_observations values for max_obs in [1, 25, 50, 100]: response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", "max_observations": max_obs, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data def test_get_peer_representation_with_all_parameters( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with all optional parameters""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create a session and target peer target_peer_name = str(generate_nanoid()) client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name, "metadata": {}}, ) client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}, target_peer_name: {}}, }, ) # Test with all parameters response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "session_id": session_id, "target": target_peer_name, "search_query": "What do I know about this peer?", "search_top_k": 15, "search_max_distance": 0.75, "include_most_frequent": True, "max_observations": 30, }, ) assert response.status_code == 200 data = response.json() assert "representation" in data assert isinstance(data["representation"], str) def test_get_peer_representation_boundary_values( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer representation with boundary values for numeric parameters""" test_workspace, test_peer = sample_data # Test minimum values response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test", "search_top_k": 1, "search_max_distance": 0.0, "max_observations": 1, }, ) assert response.status_code == 200 # Test maximum values response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test", "search_top_k": 100, "search_max_distance": 1.0, "max_observations": 100, }, ) assert response.status_code == 200 def test_get_peer_representation_default_max_observations( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test that max_observations defaults to 25 when not provided""" test_workspace, test_peer = sample_data # Test without max_observations - should use default of 25 response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation", json={ "search_query": "test query", }, ) assert response.status_code == 200 data = response.json() assert "representation" in data def test_search_peer(client: TestClient, sample_data: tuple[Workspace, Peer]): """Test the peer search functionality""" test_workspace, test_peer = sample_data # Add some messages to search through client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions/test_session/messages", json={ "messages": [ {"content": "Search this content", "peer_id": test_peer.name}, {"content": "Another searchable message", "peer_id": test_peer.name}, ] }, ) # Search with a query response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/search", json={"query": "search query", "limit": 10}, ) assert response.status_code == 200 data = response.json() # Response should be a direct list of messages assert isinstance(data, list) def test_search_peer_empty_query( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer search with empty query""" test_workspace, test_peer = sample_data # Search with empty query response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/search", json={"query": "", "limit": 10}, ) assert response.status_code == 200 data = response.json() # Response should be a direct list of messages assert isinstance(data, list) def test_search_peer_nonexistent( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test searching a peer that doesn't exist""" test_workspace, _ = sample_data nonexistent_peer_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{nonexistent_peer_id}/search", json={"query": "test query", "limit": 10}, ) assert response.status_code == 200 data: list[dict[str, Any]] = response.json() assert isinstance(data, list) assert len(data) == 0 def test_search_peer_with_messages( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer search with actual messages""" test_workspace, test_peer = sample_data # Add some messages to search through client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions/test_session/messages", json={ "messages": [ {"content": "Search this content", "peer_id": test_peer.name}, {"content": "Another searchable message", "peer_id": test_peer.name}, ] }, ) # Search for content response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/search", json={"query": "search", "limit": 10}, ) assert response.status_code == 200 data = response.json() # Response should be a direct list of messages assert isinstance(data, list) def test_search_peer_with_limit( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer search with custom limit""" test_workspace, test_peer = sample_data # Add some messages to search through client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions/test_session/messages", json={ "messages": [ {"content": "Search this content", "peer_id": test_peer.name}, {"content": "Another searchable message", "peer_id": test_peer.name}, {"content": "More searchable content", "peer_id": test_peer.name}, ] }, ) # Search with custom limit response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/search", json={"query": "search", "limit": 2}, ) assert response.status_code == 200 data: list[dict[str, Any]] = response.json() assert isinstance(data, list) # Should not exceed the limit assert len(data) <= 2 def test_get_peers_with_complex_filter( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer listing with complex filters""" test_workspace, _ = sample_data # Create peers with different metadata for i in range(3): client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={ "name": str(generate_nanoid()), "metadata": {"index": i, "type": "test"}, }, ) # Test complex filter combination response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/list", json={ "filters": { "AND": [ {"metadata": {"type": "test"}}, {"metadata": {"index": {"gte": 1}}}, ] } }, ) assert response.status_code == 200 data = response.json() # for each peer returned, assert that the metadata["index"] fits our filter for peer in data["items"]: assert ( peer["metadata"].get("index", 0) >= 1 and peer["metadata"].get("type", "") == "test" ) assert "items" in data def test_update_peer_all_fields( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test peer update with all possible fields""" test_workspace, test_peer = sample_data # Test updating both metadata and configuration metadata = {"updated": True, "version": 2} configuration = {"features": {"new_feature": True}} response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}", json={"metadata": metadata, "configuration": configuration}, ) assert response.status_code == 200 data = response.json() assert data["metadata"] == metadata assert data["configuration"] == configuration def test_get_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]): """Test the peer cards endpoint""" test_workspace, observer_peer = sample_data # Create a second peer (the target/observed peer) target_peer_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name}, ) assert response.status_code in [200, 201] # Test getting observer's own card (should return null initially) response = client.get( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card" ) assert response.status_code == 200 data = response.json() assert data["peer_card"] is None # Test getting card for target peer (should return null initially) response = client.get( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", params={"target": target_peer_name}, ) assert response.status_code == 200 data = response.json() assert data["peer_card"] is None @pytest.mark.asyncio async def test_get_peer_card_with_data( client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): """Test the peer cards endpoint with actual peer card data""" test_workspace, observer_peer = sample_data # Create a second peer (the target/observed peer) target_peer_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name}, ) assert response.status_code in [200, 201] # Set up peer cards using the database directly # Set a self-card for the observer peer self_card_content = ["I am a helpful AI assistant", "I enjoy learning about users"] await crud.set_peer_card( db_session, test_workspace.name, self_card_content, observer=observer_peer.name, observed=observer_peer.name, ) # Set a card for the observer describing the target peer target_card_content = ["This peer seems friendly", "They ask good questions"] await crud.set_peer_card( db_session, test_workspace.name, target_card_content, observer=observer_peer.name, observed=target_peer_name, ) # Test getting observer's own card response = client.get( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card" ) assert response.status_code == 200 data = response.json() assert data["peer_card"] == self_card_content # Test getting card for target peer response = client.get( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", params={"target": target_peer_name}, ) assert response.status_code == 200 data = response.json() assert data["peer_card"] == target_card_content def test_set_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]): """Test setting peer cards via the PUT endpoint.""" test_workspace, observer_peer = sample_data # Create a target peer target_peer_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": target_peer_name}, ) assert response.status_code in [200, 201] # Set the observer's own card self_card = ["I am a test peer", "I like writing tests"] response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", json={"peer_card": self_card}, ) assert response.status_code == 200 data = response.json() assert data["peer_card"] == self_card # Verify with GET response = client.get( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card" ) assert response.status_code == 200 assert response.json()["peer_card"] == self_card # Set a card for the target peer target_card = ["Target is helpful", "Target knows Python"] response = client.put( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", params={"target": target_peer_name}, json={"peer_card": target_card}, ) assert response.status_code == 200 data = response.json() assert data["peer_card"] == target_card # Verify with GET response = client.get( f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card", params={"target": target_peer_name}, ) assert response.status_code == 200 assert response.json()["peer_card"] == target_card FOOD_PREFS_SCHEMA = { "type": "object", "properties": { "preferences": { "type": "array", "items": { "type": "object", "properties": { "food": {"type": "string"}, "sentiment": {"enum": ["loves", "likes", "dislikes"]}, }, "required": ["food", "sentiment"], }, }, "summary": {"type": "string"}, }, "required": ["preferences", "summary"], } def test_chat_with_response_format( client: TestClient, sample_data: tuple[Workspace, Peer], mock_llm_call_functions: dict[str, Any], ): """A valid response_format converts to a Pydantic model and is passed to the dialectic as response_model.""" test_workspace, test_peer = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/chat", json={ "query": "What are this user's food preferences?", "stream": False, "response_format": FOOD_PREFS_SCHEMA, }, ) assert response.status_code == 200 assert "content" in response.json() kwargs = mock_llm_call_functions["agentic_chat"].await_args.kwargs response_model = kwargs["response_model"] assert isinstance(response_model, type) assert issubclass(response_model, BaseModel) # The converted model enforces the caller's schema. instance = response_model.model_validate( {"preferences": [{"food": "sushi", "sentiment": "loves"}], "summary": "s"} ) assert instance.summary == "s" # pyright: ignore def test_chat_with_response_format_streaming( client: TestClient, sample_data: tuple[Workspace, Peer], mock_llm_call_functions: dict[str, Any], ): test_workspace, test_peer = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/chat", json={ "query": "What are this user's food preferences?", "stream": True, "response_format": FOOD_PREFS_SCHEMA, }, ) assert response.status_code == 200 assert "data:" in response.text kwargs = mock_llm_call_functions["agentic_chat_stream"].call_args.kwargs response_model = kwargs["response_model"] assert isinstance(response_model, type) assert issubclass(response_model, BaseModel) @pytest.mark.parametrize( "bad_schema", [ {"type": "string"}, # non-object root {"type": "object", "properties": {"a": {"$ref": "#/x"}}}, {"type": "object", "properties": {"a": {"allOf": [{"type": "string"}]}}}, { "type": "object", "properties": { "m": {"type": "object", "additionalProperties": {"type": "string"}} }, }, ], ) def test_chat_with_invalid_response_format( client: TestClient, sample_data: tuple[Workspace, Peer], mock_llm_call_functions: dict[str, Any], bad_schema: dict[str, Any], ): """Unsupported schemas are rejected with 422 before the dialectic runs.""" test_workspace, test_peer = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/chat", json={ "query": "Hello?", "stream": False, "response_format": bad_schema, }, ) assert response.status_code == 422 assert "Invalid response_format" in response.json()["detail"] mock_llm_call_functions["agentic_chat"].assert_not_awaited()