feat(ui): implement premium beige design system and ux refinements
This commit is contained in:
214
backend/tests/e2e/test_replay_analytics.py
Normal file
214
backend/tests/e2e/test_replay_analytics.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""E2E tests for replay and analytics flows (flow 6).
|
||||
|
||||
Flow 6: list conversations -> select one -> step-by-step replay.
|
||||
Also tests the analytics dashboard endpoint.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from tests.e2e.conftest import FakePool, create_e2e_app
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom pool that returns specific data per query
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ReplayPool(FakePool):
|
||||
"""Pool that returns different data depending on the SQL query."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conversations: list[dict] | None = None,
|
||||
checkpoints: list[dict] | None = None,
|
||||
analytics_rows: list[dict] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._conversations = conversations or []
|
||||
self._checkpoints = checkpoints or []
|
||||
self._analytics = analytics_rows or []
|
||||
|
||||
class _Conn:
|
||||
def __init__(self, convos, checkpoints, analytics):
|
||||
self._convos = convos
|
||||
self._checkpoints = checkpoints
|
||||
self._analytics = analytics
|
||||
|
||||
async def execute(self, query: str, params=None):
|
||||
from tests.e2e.conftest import FakeCursor
|
||||
|
||||
if "conversations" in query and "SELECT" in query:
|
||||
return FakeCursor(self._convos)
|
||||
if "checkpoints" in query:
|
||||
return FakeCursor(self._checkpoints)
|
||||
# Analytics queries
|
||||
return FakeCursor(self._analytics)
|
||||
|
||||
def connection(self):
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
conn = self._Conn(self._conversations, self._checkpoints, self._analytics)
|
||||
|
||||
@asynccontextmanager
|
||||
async def _ctx():
|
||||
yield conn
|
||||
|
||||
return _ctx()
|
||||
|
||||
|
||||
class TestFlow6ReplayConversation:
|
||||
"""Flow 6: list conversations -> select one -> step replay."""
|
||||
|
||||
def test_list_conversations(self) -> None:
|
||||
now = datetime.now(tz=timezone.utc).isoformat()
|
||||
conversations = [
|
||||
{
|
||||
"thread_id": "conv-001",
|
||||
"created_at": now,
|
||||
"last_activity": now,
|
||||
"status": "active",
|
||||
"total_tokens": 150,
|
||||
"total_cost_usd": 0.003,
|
||||
},
|
||||
{
|
||||
"thread_id": "conv-002",
|
||||
"created_at": now,
|
||||
"last_activity": now,
|
||||
"status": "completed",
|
||||
"total_tokens": 300,
|
||||
"total_cost_usd": 0.006,
|
||||
},
|
||||
]
|
||||
pool = ReplayPool(conversations=conversations)
|
||||
app = create_e2e_app(pool=pool)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
assert len(body["data"]) == 2
|
||||
assert body["data"][0]["thread_id"] == "conv-001"
|
||||
assert body["data"][1]["thread_id"] == "conv-002"
|
||||
|
||||
def test_list_conversations_pagination(self) -> None:
|
||||
conversations = [
|
||||
{
|
||||
"thread_id": f"conv-{i:03d}",
|
||||
"created_at": "2026-04-01T00:00:00Z",
|
||||
"last_activity": "2026-04-01T00:00:00Z",
|
||||
"status": "active",
|
||||
"total_tokens": 100,
|
||||
"total_cost_usd": 0.001,
|
||||
}
|
||||
for i in range(5)
|
||||
]
|
||||
pool = ReplayPool(conversations=conversations)
|
||||
app = create_e2e_app(pool=pool)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations", params={"page": 1, "per_page": 2})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
|
||||
def test_replay_thread_not_found(self) -> None:
|
||||
pool = ReplayPool(checkpoints=[])
|
||||
app = create_e2e_app(pool=pool)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/replay/nonexistent-thread")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_replay_invalid_thread_id_format(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
# Thread ID with special chars fails regex validation
|
||||
resp = client.get("/api/replay/invalid%20thread%21%40")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestAnalyticsDashboard:
|
||||
"""Analytics endpoint tests."""
|
||||
|
||||
def test_analytics_invalid_range_format(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/analytics", params={"range": "invalid"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_analytics_range_too_large(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/analytics", params={"range": "999d"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_analytics_range_zero_rejected(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/analytics", params={"range": "0d"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestFullUserJourney:
|
||||
"""End-to-end journey: chat -> then check replay list shows the conversation."""
|
||||
|
||||
def test_chat_then_check_conversations_endpoint(self) -> None:
|
||||
"""After chatting via WebSocket, the conversations endpoint is reachable."""
|
||||
from tests.e2e.conftest import make_chunk, make_graph
|
||||
|
||||
graph = make_graph(chunks=[make_chunk("Your order is shipped.")])
|
||||
now = datetime.now(tz=timezone.utc).isoformat()
|
||||
pool = ReplayPool(
|
||||
conversations=[
|
||||
{
|
||||
"thread_id": "e2e-journey-1",
|
||||
"created_at": now,
|
||||
"last_activity": now,
|
||||
"status": "active",
|
||||
"total_tokens": 50,
|
||||
"total_cost_usd": 0.001,
|
||||
},
|
||||
],
|
||||
)
|
||||
app = create_e2e_app(graph=graph, pool=pool)
|
||||
|
||||
with TestClient(app) as client:
|
||||
# Step 1: Chat via WebSocket
|
||||
with client.websocket_connect("/ws") as ws:
|
||||
ws.send_json({
|
||||
"type": "message",
|
||||
"thread_id": "e2e-journey-1",
|
||||
"content": "Where is my order?",
|
||||
})
|
||||
messages = []
|
||||
for _ in range(20):
|
||||
msg = ws.receive_json()
|
||||
messages.append(msg)
|
||||
if msg["type"] in ("message_complete", "error"):
|
||||
break
|
||||
assert any(m["type"] == "message_complete" for m in messages)
|
||||
|
||||
# Step 2: Check conversations endpoint
|
||||
resp = client.get("/api/conversations")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
assert any(
|
||||
c["thread_id"] == "e2e-journey-1" for c in body["data"]
|
||||
)
|
||||
|
||||
# Step 3: Health check still works
|
||||
resp = client.get("/api/health")
|
||||
assert resp.status_code == 200
|
||||
Reference in New Issue
Block a user