dotb_ai-platform/tests/test_api_surface.py

170 lines
7.2 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.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