133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
from types import SimpleNamespace
|
|
from typing import cast
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from ai_platform.bootstrap.app import create_app
|
|
from ai_platform.infrastructure.config.settings import Settings
|
|
from ai_platform.infrastructure.persistence.models import ScoringRun
|
|
from ai_platform.presentation.http.routes import to_detail_json
|
|
from ai_platform.presentation.http.run_responses import RunDetail, RunMetrics
|
|
|
|
|
|
def test_run_detail_and_summary_responses_are_documented():
|
|
openapi = create_app(Settings()).openapi()
|
|
paths = openapi["paths"]
|
|
schemas = openapi["components"]["schemas"]
|
|
|
|
assert paths["/v1/lead-scoring/runs"]["post"]["responses"]["202"]["content"][
|
|
"application/json"
|
|
]["schema"] == {"$ref": "#/components/schemas/RunSummary"}
|
|
assert paths["/v1/lead-scoring/runs"]["post"]["responses"]["200"]["content"][
|
|
"application/json"
|
|
]["schema"] == {"$ref": "#/components/schemas/RunSummary"}
|
|
assert paths["/v1/lead-scoring/runs/{run_id}"]["get"]["responses"]["200"]["content"][
|
|
"application/json"
|
|
]["schema"] == {"$ref": "#/components/schemas/RunDetail"}
|
|
assert paths["/v1/admin/runs"]["get"]["responses"]["200"]["content"]["application/json"][
|
|
"schema"
|
|
] == {"$ref": "#/components/schemas/RunList"}
|
|
assert paths["/v1/admin/runs/{run_id}"]["get"]["responses"]["200"]["content"][
|
|
"application/json"
|
|
]["schema"] == {"$ref": "#/components/schemas/RunDetail"}
|
|
|
|
assert "snapshot" not in schemas["RunSummary"]["properties"]
|
|
assert "metrics" not in schemas["RunSummary"]["properties"]
|
|
assert "snapshot" in schemas["RunDetail"]["properties"]
|
|
assert "metrics" in schemas["RunDetail"]["properties"]
|
|
assert "ai_evidence" in schemas["RunDetail"]["properties"]
|
|
assert "profile_field_count" in schemas["RunMetrics"]["properties"]
|
|
assert "conversation_turn_count" in schemas["RunMetrics"]["properties"]
|
|
assert "contact_started_conversation_count" in schemas["RunMetrics"]["properties"]
|
|
assert "contact_message_recency_hours" in schemas["RunMetrics"]["properties"]
|
|
assert "contact_message_recency_hours" in schemas["RunMetrics"]["required"]
|
|
assert schemas["RunMetrics"]["properties"]["customer_initiated_return"]["type"] == "boolean"
|
|
assert (
|
|
"original snapshot position"
|
|
in schemas["RunMetrics"]["properties"]["customer_initiated_return"]["description"]
|
|
)
|
|
assert schemas["RunDetail"]["properties"]["ai_evidence"]["anyOf"][0]["maxItems"] == 20
|
|
assert schemas["RunList"]["properties"]["runs"]["items"] == {
|
|
"$ref": "#/components/schemas/RunSummary"
|
|
}
|
|
|
|
|
|
def test_run_detail_serializes_new_metrics_and_requires_boolean_values():
|
|
snapshot = {
|
|
"captured_at": "2025-09-25T10:00:00Z",
|
|
"lead_profile": {"ems_lead_id": "lead-1"},
|
|
"conversations": [
|
|
{"conversation_id": "empty", "messages": []},
|
|
{
|
|
"conversation_id": "first",
|
|
"messages": [
|
|
{
|
|
"message_id": "m1",
|
|
"sender_type": "agent",
|
|
"content": "Hello",
|
|
"sent_at": "2025-09-25T08:00:00Z",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"conversation_id": "second",
|
|
"messages": [
|
|
{
|
|
"message_id": "m2",
|
|
"sender_type": "contact",
|
|
"content": "Thanks",
|
|
"sent_at": "2025-09-25T09:00:00Z",
|
|
}
|
|
],
|
|
},
|
|
],
|
|
}
|
|
row = SimpleNamespace(
|
|
id=str(uuid4()),
|
|
request_id=str(uuid4()),
|
|
tenant_id="tenant-1",
|
|
status="completed",
|
|
profile_version="profile-v1",
|
|
rule_score=80,
|
|
ai_score=None,
|
|
final_score=80,
|
|
degraded=False,
|
|
result_mode="rules_only",
|
|
error_code=None,
|
|
breakdown=[],
|
|
created_at=None,
|
|
completed_at=None,
|
|
request={"snapshot": snapshot},
|
|
ai_confidence=None,
|
|
ai_reasoning=None,
|
|
ai_evidence=None,
|
|
)
|
|
|
|
detail = RunDetail.model_validate(to_detail_json(cast(ScoringRun, row)))
|
|
|
|
assert detail.metrics.conversation_count == 3
|
|
assert detail.metrics.contact_message_recency_days == 0.04
|
|
assert detail.metrics.contact_message_recency_hours == 1.0
|
|
assert detail.metrics.contact_started_conversation_count == 1
|
|
assert detail.metrics.customer_initiated_return is True
|
|
with pytest.raises(ValidationError):
|
|
RunMetrics.model_validate({**detail.metrics.model_dump(), "customer_initiated_return": 1})
|
|
|
|
|
|
def test_required_routes_are_registered():
|
|
app = create_app(Settings())
|
|
paths = set(app.openapi()["paths"])
|
|
assert {
|
|
"/v1/admin/login",
|
|
"/v1/admin/logout",
|
|
"/v1/admin/me",
|
|
"/v1/admin/ai-profiles",
|
|
"/v1/admin/runs",
|
|
"/v1/admin/runs/{run_id}",
|
|
"/v1/lead-scoring/runs",
|
|
"/v1/lead-scoring/runs/{run_id}",
|
|
"/v1/platform/tenant-keys",
|
|
} <= paths
|