Backend (516 tests, 94% coverage): - Add azure_openai endpoint/deployment validation tests (config.py -> 100%) - Add _total_conversations and _avg_turns direct tests (queries.py -> 100%) - Add transformer edge cases: list content, string checkpoint, invalid JSON, malformed message graceful skip (transformer.py -> 93%) - Add safety combined status_code+error_message interaction tests - Fix ambiguous 200/422 assertion to strict 422 - Add E2E pagination shape assertions (total, page, per_page, row count) - Fix ReplayPool mock to respect LIMIT/OFFSET params Frontend (23 tests, vitest + happy-dom + @testing-library/react): - Add vitest infrastructure with happy-dom environment - Add api.ts tests: success, HTTP error, success=false, URL encoding - Add DashboardPage tests: loading, data, error, empty states - Add ReplayListPage tests: loading, empty, data, error, status badge classes - Add ReplayPage tests: loading, steps, empty, error states
258 lines
8.6 KiB
Python
258 lines
8.6 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)
|
|
|
|
def test_list_content_joined_to_string(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{
|
|
"type": "human",
|
|
"content": [
|
|
{"text": "Hello"},
|
|
{"text": " world"},
|
|
],
|
|
}
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].content == "Hello world"
|
|
|
|
def test_checkpoint_as_string_skipped(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
{
|
|
"thread_id": "t1",
|
|
"checkpoint_id": "cp1",
|
|
"checkpoint": "not-a-dict",
|
|
"metadata": {},
|
|
}
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert steps == []
|
|
|
|
def test_channel_values_not_dict_skipped(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
{
|
|
"thread_id": "t1",
|
|
"checkpoint_id": "cp1",
|
|
"checkpoint": {"channel_values": "bad"},
|
|
"metadata": {},
|
|
}
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert steps == []
|
|
|
|
def test_tool_result_valid_json_parsed(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{
|
|
"type": "tool",
|
|
"content": '{"order_id": "123", "status": "shipped"}',
|
|
"name": "get_order_status",
|
|
}
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].result == {"order_id": "123", "status": "shipped"}
|
|
|
|
def test_tool_result_invalid_json_wrapped(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{
|
|
"type": "tool",
|
|
"content": "not valid json",
|
|
"name": "some_tool",
|
|
}
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
assert len(steps) == 1
|
|
assert steps[0].result == {"raw": "not valid json"}
|
|
|
|
def test_malformed_message_skipped_gracefully(self) -> None:
|
|
from app.replay.transformer import transform_checkpoints
|
|
|
|
rows = [
|
|
_make_row(
|
|
[
|
|
{"type": "human", "content": "Good message"},
|
|
42, # not a dict -- will raise in _step_from_message
|
|
{"type": "ai", "content": "Response", "tool_calls": []},
|
|
]
|
|
)
|
|
]
|
|
steps = transform_checkpoints(rows)
|
|
# The malformed message is skipped; the other two produce steps.
|
|
assert len(steps) == 2
|
|
assert steps[0].step == 1
|
|
assert steps[1].step == 2
|