1385 lines
46 KiB
Python
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()
|