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.lead_scoring.application.repository import ScoringRunRecord from ai_platform.lead_scoring.application.run_use_cases import to_run_detail 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" } admin_key_paths = { "/v1/admin/tenant-keys": paths["/v1/admin/tenant-keys"], "/v1/admin/tenant-keys/{tenant_id}/rotate": paths[ "/v1/admin/tenant-keys/{tenant_id}/rotate" ], "/v1/admin/tenant-keys/{tenant_id}": paths["/v1/admin/tenant-keys/{tenant_id}"], } assert admin_key_paths["/v1/admin/tenant-keys"]["get"]["responses"]["200"]["content"][ "application/json" ]["schema"] == {"$ref": "#/components/schemas/TenantKeyList"} assert admin_key_paths["/v1/admin/tenant-keys"]["post"]["requestBody"]["content"][ "application/json" ]["schema"] == {"$ref": "#/components/schemas/TenantKeyCreateRequest"} assert admin_key_paths["/v1/admin/tenant-keys"]["post"]["responses"]["201"]["content"][ "application/json" ]["schema"] == {"$ref": "#/components/schemas/TenantKeySecret"} assert admin_key_paths["/v1/admin/tenant-keys/{tenant_id}/rotate"]["post"]["responses"]["200"][ "content" ]["application/json"]["schema"] == {"$ref": "#/components/schemas/TenantKeySecret"} assert admin_key_paths["/v1/admin/tenant-keys/{tenant_id}"]["delete"]["responses"]["200"][ "content" ]["application/json"]["schema"] == {"$ref": "#/components/schemas/TenantKeySummary"} provisioner = paths["/v1/platform/tenant-keys"]["post"] assert provisioner["parameters"][0]["name"] == "authorization" assert provisioner["requestBody"]["content"]["application/json"]["schema"] == { "$ref": "#/components/schemas/TenantKeyCreateRequest" } assert provisioner["responses"]["201"]["content"]["application/json"]["schema"] == { "$ref": "#/components/schemas/TenantKeySecret" } assert "verifier" not in schemas["TenantKeySummary"]["properties"] assert "tenant_key" not in schemas["TenantKeySummary"]["properties"] assert "tenant_key" in schemas["TenantKeySecret"]["properties"] 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(to_run_detail(cast(ScoringRunRecord, 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/admin/tenant-keys", "/v1/admin/tenant-keys/{tenant_id}/rotate", "/v1/admin/tenant-keys/{tenant_id}", "/v1/lead-scoring/runs", "/v1/lead-scoring/runs/{run_id}", "/v1/platform/tenant-keys", } <= paths