honcho/tests/routes/test_peers.py

1385 lines
46 KiB
Python

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()