- Replay models: StepType enum, ReplayStep, ReplayPage frozen dataclasses
- Checkpoint transformer: PostgresSaver JSONB -> structured timeline steps
- Replay API: GET /api/conversations (paginated), GET /api/replay/{thread_id}
- Analytics models: AgentUsage, InterruptStats, AnalyticsResult
- Analytics event recorder: Protocol + PostgresAnalyticsRecorder + NoOp
- Analytics queries: resolution_rate, agent_usage, escalation_rate, cost, interrupts
- Analytics API: GET /api/analytics?range=Xd with envelope response
- DB migration: analytics_events table + conversations column additions
- 74 new tests, 399 total passing, 92.87% coverage
156 lines
5.4 KiB
Python
156 lines
5.4 KiB
Python
"""Unit tests for app.replay.transformer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def _make_row(messages: list[dict], metadata: dict | None = None) -> dict:
|
|
"""Helper to build a checkpoint row with the given messages."""
|
|
return {
|
|
"thread_id": "thread-abc",
|
|
"checkpoint_id": "cp-001",
|
|
"checkpoint": {"channel_values": {"messages": messages}},
|
|
"metadata": metadata or {},
|
|
}
|
|
|
|
|
|
class TestTransformCheckpoints:
|
|
def test_empty_rows_returns_empty_list(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
result = transform_checkpoints([])
|
|
assert result == []
|
|
|
|
def test_human_message_produces_user_message_step(self) -> None:
|
|
from app.replay.models import StepType
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [_make_row([{"type": "human", "content": "Hello, I need help"}])]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].type == StepType.user_message
|
|
assert steps[0].content == "Hello, I need help"
|
|
assert steps[0].step == 1
|
|
|
|
def test_ai_message_with_content_produces_agent_response(self) -> None:
|
|
from app.replay.models import StepType
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[{"type": "ai", "content": "I can help you with that.", "tool_calls": []}],
|
|
metadata={"writes": {"some_agent": "response"}},
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].type == StepType.agent_response
|
|
assert steps[0].content == "I can help you with that."
|
|
|
|
def test_ai_message_with_tool_calls_produces_tool_call_step(self) -> None:
|
|
from app.replay.models import StepType
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{
|
|
"type": "ai",
|
|
"content": "",
|
|
"tool_calls": [
|
|
{
|
|
"name": "get_order_status",
|
|
"args": {"order_id": "ORD-123"},
|
|
"id": "call_abc",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].type == StepType.tool_call
|
|
assert steps[0].tool == "get_order_status"
|
|
assert steps[0].params == {"order_id": "ORD-123"}
|
|
|
|
def test_tool_message_produces_tool_result_step(self) -> None:
|
|
from app.replay.models import StepType
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{
|
|
"type": "tool",
|
|
"content": '{"status": "shipped"}',
|
|
"name": "get_order_status",
|
|
}
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].type == StepType.tool_result
|
|
assert steps[0].tool == "get_order_status"
|
|
|
|
def test_multiple_messages_sequential_steps(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{"type": "human", "content": "Help"},
|
|
{"type": "ai", "content": "Sure!", "tool_calls": []},
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 2
|
|
assert steps[0].step == 1
|
|
assert steps[1].step == 2
|
|
|
|
def test_unknown_message_type_skipped(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [_make_row([{"type": "unknown_type", "content": "test"}])]
|
|
steps = transform_checkpoints(rows)
|
|
# Should not crash; unknown types may be skipped
|
|
assert isinstance(steps, list)
|
|
|
|
def test_row_missing_checkpoint_skipped(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [{"thread_id": "t1", "checkpoint_id": "cp1", "checkpoint": None, "metadata": {}}]
|
|
steps = transform_checkpoints(rows)
|
|
assert isinstance(steps, list)
|
|
|
|
def test_row_missing_messages_key_skipped(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [{"thread_id": "t1", "checkpoint_id": "cp1", "checkpoint": {}, "metadata": {}}]
|
|
steps = transform_checkpoints(rows)
|
|
assert isinstance(steps, list)
|
|
|
|
def test_multiple_rows_steps_are_continuous(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row([{"type": "human", "content": "Q1"}]),
|
|
_make_row([{"type": "ai", "content": "A1", "tool_calls": []}]),
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 2
|
|
assert steps[0].step == 1
|
|
assert steps[1].step == 2
|
|
|
|
def test_timestamps_are_strings(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [_make_row([{"type": "human", "content": "Hi"}])]
|
|
steps = transform_checkpoints(rows)
|
|
assert isinstance(steps[0].timestamp, str)
|