import datetime from typing import Any import pytest from fastapi.testclient import TestClient from nanoid import generate as generate_nanoid from sqlalchemy.ext.asyncio import AsyncSession from src import models from src.models import Peer, Workspace def test_get_or_create_session(client: TestClient, sample_data: tuple[Workspace, Peer]): # Test get or create session test_workspace, test_peer = sample_data # Test creating a new session with no parameters response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": str(generate_nanoid())}, ) assert response.status_code in [200, 201] data = response.json() assert isinstance(data["id"], str) assert data["metadata"] == {} assert data["workspace_id"] == test_workspace.name # Test creating a session with a specific id and peer_names (should get or create) session_id = str(generate_nanoid()) response2 = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) assert response2.status_code in [200, 201] data2 = response2.json() assert data2["id"] == session_id assert data2["metadata"] == {} assert data2["workspace_id"] == test_workspace.name # Test getting the same session again (should return the same session) response3 = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) assert response3.status_code in [200, 201] data3 = response3.json() assert data3["id"] == session_id assert data3["metadata"] == {} assert data3["workspace_id"] == test_workspace.name def test_create_session_with_metadata( client: TestClient, sample_data: tuple[Workspace, Peer] ): test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "metadata": {"session_key": "session_value"}, }, ) assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == {"session_key": "session_value"} assert "id" in data assert data["id"] == session_id assert data["workspace_id"] == test_workspace.name def test_create_session_with_configuration( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session creation with configuration parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) configuration = {"experimental_feature": True, "beta_mode": False} response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "configuration": configuration, }, ) assert response.status_code in [200, 201] data = response.json() assert data["configuration"] == configuration assert data["id"] == session_id assert data["workspace_id"] == test_workspace.name def test_create_session_with_all_optional_params( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session creation with all optional parameters""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) metadata = {"key": "value", "number": 42} configuration = {"feature1": True, "feature2": False} response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "metadata": metadata, "configuration": configuration, }, ) assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == metadata assert data["configuration"] == configuration assert data["id"] == session_id assert data["workspace_id"] == test_workspace.name def test_create_session_with_too_many_peers( client: TestClient, sample_data: tuple[Workspace, Peer], ): """Test that creating a session with too many observers fails""" test_workspace, test_peer = sample_data # create 10 peers peer_names = [test_peer.name] for _ in range(10): peer_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {}}, ) assert response.status_code in [200, 201] peer_names.append(peer_name) # Test 1: Create session with 11 non-observers should succeed response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": str(generate_nanoid()), "peer_names": {peer_name: {} for peer_name in peer_names}, }, ) assert response.status_code in [200, 201] # Should succeed since no observers # Test 2: Try to create session with 11 observers (exceeds limit) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": str(generate_nanoid()), "peer_names": { peer_name: {"observe_others": True} for peer_name in peer_names }, }, ) assert response.status_code == 400 assert "11 observers" in response.json()["detail"] assert "Maximum allowed is 10 observers" in response.json()["detail"] session_response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/list", json={"filters": {"id": "test_session"}}, ) assert session_response.status_code == 200 assert len(session_response.json()["items"]) == 0 # Remove one peer from our list peer_names.pop() # Attempt to create session with same name response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": "test_session", "peer_names": {peer_name: {} for peer_name in peer_names}, }, ) assert response.status_code in [200, 201] data = response.json() assert data["id"] == "test_session" assert data["workspace_id"] == test_workspace.name def test_get_sessions(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "metadata": {"test_key": "test_value"}, }, ) assert response.status_code in [200, 201] data = response.json() assert data["metadata"] == {"test_key": "test_value"} assert "id" in data assert data["workspace_id"] == test_workspace.name response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/list", json={"filters": {"metadata": {"test_key": "test_value"}}}, ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) == 1 assert data["items"][0]["metadata"] == {"test_key": "test_value"} assert data["items"][0]["workspace_id"] == test_workspace.name def test_get_sessions_with_empty_filter( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session listing with empty filter object""" test_workspace, _ = sample_data response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/list", json={"filters": {}} ) assert response.status_code == 200 data = response.json() assert "items" in data assert isinstance(data["items"], list) def test_get_sessions_with_reverse( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session listing with reverse creation-time ordering.""" test_workspace, test_peer = sample_data reverse_group = f"reverse-sessions-{generate_nanoid()}" first_session = f"reverse-session-a-{generate_nanoid()}" second_session = f"reverse-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}/sessions/list", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert normal_response.status_code == 200 reverse_response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/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_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_reverse_uses_id_tiebreaker( client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer], ): """Sessions with identical created_at fall back to ordering by id (nanoid PK).""" test_workspace, _ = sample_data reverse_group = f"tiebreaker-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-{generate_nanoid()}" high_name = f"tie-high-{generate_nanoid()}" db_session.add( models.Session( 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.Session( 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}/sessions/list", json={"filters": {"metadata": {"reverse_group": reverse_group}}}, ) assert normal_response.status_code == 200 reverse_response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/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_sessions_reverse_with_pagination( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Paged reverse listing returns newest-first across consecutive pages.""" test_workspace, test_peer = sample_data reverse_group = f"paged-reverse-sessions-{generate_nanoid()}" session_names = [f"paged-reverse-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}/sessions/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}/sessions/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}/sessions/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"]] == [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_update_delete_metadata( client: TestClient, sample_data: tuple[Workspace, Peer] ): test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "metadata": {"default": "value"}, }, ) assert response.status_code in [200, 201] response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}", json={"metadata": {}}, ) assert response.status_code == 200 data = response.json() assert data["metadata"] == {} def test_update_session(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, }, ) assert response.status_code in [200, 201] response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}", json={"metadata": {"new_key": "new_value"}}, ) assert response.status_code == 200 data = response.json() assert data["metadata"] == {"new_key": "new_value"} def test_delete_session(client: TestClient, sample_data: tuple[Workspace, Peer]): """Test deleting a session""" test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "metadata": {"test_key": "test_value"}, }, ) assert response.status_code in [200, 201] # Delete the session response = client.delete( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}", ) assert response.status_code == 202 data = response.json() assert data["message"] == "Session deleted successfully" # Verify the session is deleted by trying to list it response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/list", json={"filters": {"id": session_id}}, ) assert response.status_code == 200 data = response.json() assert len(data["items"]) == 0 def test_update_session_with_configuration( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session update with configuration parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) # Update with configuration configuration = {"new_feature": True, "legacy_feature": False} response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}", json={"metadata": {}, "configuration": configuration}, ) assert response.status_code == 200 data = response.json() assert data["configuration"] == configuration def test_update_session_with_all_optional_params( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session update with both metadata and configuration""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) # Update with all params metadata = {"updated_key": "updated_value"} configuration = {"experimental": True, "beta": True} response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}", json={"metadata": metadata, "configuration": configuration}, ) assert response.status_code == 200 data = response.json() assert data["metadata"] == metadata assert data["configuration"] == configuration def test_update_session_with_null_configuration( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session update with null configuration""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session with configuration client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "configuration": {"temp": "value"}, }, ) # Update with null configuration response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}", json={"metadata": {}, "configuration": None}, ) assert response.status_code == 200 data = response.json() assert "configuration" in data def test_clone_session(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, "metadata": {"test": "key"}, }, ) assert response.status_code in [200, 201] # Create some messages in the session response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ { "content": "Test message", "peer_id": test_peer.name, "metadata": {"key": "value"}, }, { "content": "Test message 2", "peer_id": test_peer.name, "metadata": {"key": "value2"}, }, ] }, ) assert response.status_code == 201 response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/clone", ) assert response.status_code == 201 data = response.json() assert data["metadata"] == {"test": "key"} # Check messages were cloned response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{data['id']}/messages/list", json={}, ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) >= 2 assert data["items"][0]["content"] == "Test message" assert data["items"][0]["metadata"] == {"key": "value"} assert data["items"][1]["content"] == "Test message 2" assert data["items"][1]["metadata"] == {"key": "value2"} def test_clone_session_with_cutoff( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session cloning with message cutoff parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) # Create messages response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Message 1", "peer_id": test_peer.name}, {"content": "Message 2", "peer_id": test_peer.name}, ] }, ) assert response.status_code == 201 messages_data = response.json() # The response is a list of messages, not a paginated response first_message_id = messages_data[0]["id"] # Clone with cutoff at first message response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/clone?message_id={first_message_id}", ) assert response.status_code == 201 data = response.json() assert "id" in data def test_add_peers_to_session(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create another peer peer2_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) assert response.status_code in [200, 201] # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, }, ) assert response.status_code in [200, 201] # Add another peer to the session response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json={peer2_name: {}}, ) assert response.status_code == 200 def test_get_session_peers(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create another peer peer2_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) assert response.status_code in [200, 201] # Create a test session with multiple peers session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}, peer2_name: {}}, }, ) assert response.status_code in [200, 201] # Get peers from the session response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) == 2 peer_names = [peer["id"] for peer in data["items"]] assert test_peer.name in peer_names assert peer2_name in peer_names def test_set_session_peers(client: TestClient, sample_data: tuple[Workspace, Peer]): test_workspace, test_peer = sample_data # Create another peer peer2_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) assert response.status_code in [200, 201] # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}}, }, ) assert response.status_code in [200, 201] # Set peers for the session (should replace existing peers) response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json={peer2_name: {}}, ) assert response.status_code == 200 # Check that only the new peer is in the session response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) == 1 assert data["items"][0]["id"] == peer2_name def test_set_session_peers_with_observer_limit( client: TestClient, sample_data: tuple[Workspace, Peer], ): """Test that session observer limit is enforced based on observe_others setting""" test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, }, ) assert response.status_code in [200, 201] # Create 15 peers (more than the limit of 10 observers) peer_names = [test_peer.name] for _ in range(14): peer_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {}}, ) assert response.status_code in [200, 201] peer_names.append(peer_name) # Test 1: Adding 15 peers with observe_others=False should succeed peers_dict_no_observers: dict[str, dict[str, Any]] = { peer_name: {"observe_others": False} for peer_name in peer_names } response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json=peers_dict_no_observers, ) assert response.status_code == 200 # Should succeed since no observers # Test 2: Try to set 11 peers with observe_others=True (exceeds limit of 10) peers_dict_all_observers: dict[str, dict[str, Any]] = { peer_name: {"observe_others": True} for peer_name in peer_names[:11] } response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json=peers_dict_all_observers, ) assert response.status_code == 400 # ObserverException assert "11 observers" in response.json()["detail"] assert "Maximum allowed is 10 observers" in response.json()["detail"] # Test 3: Set exactly 10 observers should succeed peers_dict_ten_observers: dict[str, dict[str, Any]] = { peer_name: {"observe_others": True} for peer_name in peer_names[:10] } response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json=peers_dict_ten_observers, ) assert response.status_code == 200 # Should succeed with exactly 10 observers def test_update_peer_config_observer_limit( client: TestClient, sample_data: tuple[Workspace, Peer], ): """Test that updating peer config respects observer limits""" test_workspace, test_peer = sample_data # Create a test session session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, }, ) assert response.status_code in [200, 201] # Create exactly 10 peers and add them as observers peer_names: list[str] = [] for i in range(10): peer_name = f"observer_{i}" response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer_name, "metadata": {}}, ) assert response.status_code in [200, 201] peer_names.append(peer_name) # Add all 10 peers as observers peers_dict_observers: dict[str, dict[str, Any]] = { peer_name: {"observe_others": True} for peer_name in peer_names } # Also add the test_peer as non-observer peers_dict_observers[test_peer.name] = {"observe_others": False} response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json=peers_dict_observers, ) assert response.status_code == 200 # Should succeed with exactly 10 observers # Now try to update test_peer to become an observer (would exceed limit) response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{test_peer.name}/config", json={"observe_others": True}, ) assert response.status_code == 400 # ObserverException assert "11 observers" in response.json()["detail"] assert "Maximum allowed is 10 observers" in response.json()["detail"] # Verify that updating a peer that's already an observer still works response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{peer_names[0]}/config", json={"observe_others": True, "observe_me": False}, # Still an observer ) assert response.status_code == 204 # Should succeed since count doesn't change # Change one observer to non-observer response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{peer_names[0]}/config", json={"observe_others": False}, ) assert response.status_code == 204 # Now test_peer can become an observer (9 + 1 = 10) response = client.put( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers/{test_peer.name}/config", json={"observe_others": True}, ) assert response.status_code == 204 # Should succeed now def test_remove_peers_from_session( client: TestClient, sample_data: tuple[Workspace, Peer] ): test_workspace, test_peer = sample_data # Create another peer peer2_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) assert response.status_code in [200, 201] # Create a test session with multiple peers session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={ "id": session_id, "peer_names": {test_peer.name: {}, peer2_name: {}}, }, ) assert response.status_code in [200, 201] # Remove one peer from the session response = client.request( "DELETE", f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", json=[test_peer.name], ) assert response.status_code == 200 # Check that only the remaining peer is in the session response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/peers", ) assert response.status_code == 200 data = response.json() assert "items" in data assert len(data["items"]) == 1 assert data["items"][0]["id"] == peer2_name def test_get_session_context(client: TestClient, sample_data: tuple[Workspace, Peer]): """Test the session context endpoint""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Add some messages to have context client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Test message 1", "peer_id": test_peer.name}, {"content": "Test message 2", "peer_id": test_peer.name}, ] }, ) # Get context response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", ) assert response.status_code == 200 data = response.json() # SessionContext schema serializes "name" as "id" assert "id" in data assert "messages" in data assert "summary" in data assert data["id"] == session_id assert isinstance(data["messages"], list) assert data["summary"] is None # No summary available def test_get_session_context_with_summary( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with summary parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session with messages client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with summary response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?summary=true", ) assert response.status_code == 200 data = response.json() assert "summary" in data def test_get_session_context_with_tokens( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with token limit parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with token limit response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?tokens=100", ) assert response.status_code == 200 data = response.json() assert "messages" in data assert isinstance(data["messages"], list) def test_get_session_context_with_all_params( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with both summary and tokens parameters""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with all parameters response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?tokens=100&summary=true", ) assert response.status_code == 200 data = response.json() assert "messages" in data assert "summary" in data def test_get_session_summaries( client: TestClient, sample_data: tuple[Workspace, Peer] ) -> None: """Test getting summaries for a valid session""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get summaries response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/summaries", ) assert response.status_code == 200 data = response.json() # Validate response structure assert "id" in data assert data["id"] == session_id assert "short_summary" in data assert "long_summary" in data # Summaries will be None since they're created asynchronously assert data["short_summary"] is None assert data["long_summary"] is None def test_get_session_summaries_nonexistent_session( client: TestClient, sample_data: tuple[Workspace, Peer] ) -> None: """Test getting summaries for a non-existent session""" test_workspace, _ = sample_data nonexistent_session_id = str(generate_nanoid()) # Try to get summaries for non-existent session # Should still return 200 with null summaries response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{nonexistent_session_id}/summaries", ) assert response.status_code == 200 data = response.json() assert data["id"] == nonexistent_session_id assert data["short_summary"] is None assert data["long_summary"] is None def test_search_session(client: TestClient, sample_data: tuple[Workspace, Peer]): """Test the session search functionality""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Add messages to search through client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Search this content", "peer_id": test_peer.name}, {"content": "Another message", "peer_id": test_peer.name}, ] }, ) # Search with a query response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/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_session_empty_query( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session search with empty query""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peer_names": {test_peer.name: {}}}, ) # Search with empty query response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/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_session_nonexistent( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test searching a session that doesn't exist""" test_workspace, _ = sample_data nonexistent_session_id = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{nonexistent_session_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_session_with_messages( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session search with actual messages""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Add messages to search through client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Search this content", "peer_id": test_peer.name}, {"content": "Another message to find", "peer_id": test_peer.name}, ] }, ) # Search for content response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/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_session_with_limit( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session search with custom limit""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Add messages to search through client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Search this content", "peer_id": test_peer.name}, {"content": "Another message to find", "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}/sessions/{session_id}/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_session_context_with_peer_target( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with peer_target parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Add some messages client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Test message 1", "peer_id": test_peer.name}, ] }, ) # Get context with peer_target response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?peer_target={test_peer.name}", ) assert response.status_code == 200 data = response.json() assert "id" in data assert "messages" in data assert "peer_representation" in data assert "peer_card" in data # Representation should be present assert data["peer_representation"] is not None assert isinstance(data["peer_representation"], str) def test_get_session_context_with_peer_perspective( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with both peer_target and peer_perspective""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create another peer peer2_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) assert response.status_code in [200, 201] # Create session with both peers client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}, peer2_name: {}}}, ) # Get context with peer_perspective response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?peer_target={test_peer.name}&peer_perspective={peer2_name}", ) assert response.status_code == 200 data = response.json() assert "peer_representation" in data assert "peer_card" in data def test_get_session_context_peer_perspective_without_target_fails( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test that peer_perspective without peer_target raises validation error""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Try to get context with peer_perspective but no peer_target (should fail) response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?peer_perspective={test_peer.name}", ) # FastAPI returns 422 for validation errors, or 400 if it's a custom ValidationException assert response.status_code in [400, 422] error_detail = response.json()["detail"] assert "peer_target" in error_detail.lower() def test_get_session_context_with_search_query( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with search_query parameter for semantic search""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with search_query and peer_target response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "peer_target": test_peer.name, "search_query": "What is my favorite color?", }, ) assert response.status_code == 200 data = response.json() assert "peer_representation" in data def test_get_session_context_with_limit_to_session( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with limit_to_session parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with limit_to_session=true response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "peer_target": test_peer.name, "search_query": "Test query", "limit_to_session": True, }, ) assert response.status_code == 200 data = response.json() assert "peer_representation" in data def test_get_session_context_with_search_parameters( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with search_top_k and search_max_distance parameters""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with search parameters response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "peer_target": test_peer.name, "search_query": "Test query", "search_top_k": 5, "search_max_distance": 0.8, # float value (semantic distance 0.0-1.0) }, ) assert response.status_code == 200 data = response.json() assert "peer_representation" in data def test_get_session_context_with_include_most_frequent( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with include_most_frequent parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with include_most_frequent response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "peer_target": test_peer.name, "search_query": "Test query", "include_most_frequent": True, }, ) assert response.status_code == 200 data = response.json() assert "peer_representation" in data def test_get_session_context_with_max_observations( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with max_observations parameter""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Get context with max_observations response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "peer_target": test_peer.name, "search_query": "Test query", "max_observations": 10, }, ) assert response.status_code == 200 data = response.json() assert "peer_representation" in data def test_get_session_context_with_all_representation_params( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test session context with all representation-related parameters""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create another peer peer2_name = str(generate_nanoid()) response = client.post( f"/v3/workspaces/{test_workspace.name}/peers", json={"name": peer2_name, "metadata": {}}, ) assert response.status_code in [200, 201] # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}, peer2_name: {}}}, ) # Get context with all representation parameters response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", params={ "tokens": 500, "peer_target": test_peer.name, "peer_perspective": peer2_name, "search_query": "What do you know about me?", "limit_to_session": True, "search_top_k": 10, "search_max_distance": 0.9, # float value (semantic distance 0.0-1.0) "include_most_frequent": True, "max_observations": 15, "summary": True, }, ) assert response.status_code == 200 data = response.json() assert "id" in data assert data["id"] == session_id assert "messages" in data assert isinstance(data["messages"], list) assert "summary" in data assert "peer_representation" in data assert "peer_card" in data # Validate representation structure assert isinstance(data["peer_representation"], str) def test_get_session_context_response_structure( client: TestClient, sample_data: tuple[Workspace, Peer] ): """Test that session context response has correct structure""" test_workspace, test_peer = sample_data session_id = str(generate_nanoid()) # Create session client.post( f"/v3/workspaces/{test_workspace.name}/sessions", json={"id": session_id, "peers": {test_peer.name: {}}}, ) # Add messages response = client.post( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages", json={ "messages": [ {"content": "Message 1", "peer_id": test_peer.name}, {"content": "Message 2", "peer_id": test_peer.name}, ] }, ) assert response.status_code == 201 # Get context and validate response structure response = client.get( f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context", ) assert response.status_code == 200 data = response.json() # Validate SessionContext schema assert "id" in data assert data["id"] == session_id assert "messages" in data assert isinstance(data["messages"], list) assert len(data["messages"]) >= 2 # Validate Message schema for message in data["messages"]: assert "id" in message assert "content" in message assert "peer_id" in message assert "session_id" in message assert "workspace_id" in message assert "created_at" in message assert "token_count" in message # When no peer_target, these should not be present or be None assert data.get("peer_representation") is None assert data.get("peer_card") is None