import asyncio import json import time from pathlib import Path from urllib.parse import urlsplit from uuid import uuid4 import pytest from fastapi.testclient import TestClient from sqlalchemy import delete, select from ai_platform.bootstrap.app import create_app from ai_platform.infrastructure.config.settings import Settings from ai_platform.infrastructure.persistence.database import make_session_factory from ai_platform.infrastructure.persistence.models import ScoringRun, TenantKey from ai_platform.presentation.http.schemas import RunRequest FIXTURE = Path(__file__).parent / "fixtures" / "mock_omni_run.json" def isolated_test_settings() -> Settings: settings = Settings.load() parsed = urlsplit(settings.database_url) database_name = parsed.path.rstrip("/").rsplit("/", 1)[-1] if database_name != "ai_platform_test" or parsed.hostname not in { "127.0.0.1", "localhost", "::1", "postgres", }: pytest.skip("requires the dedicated local ai_platform_test database") if settings.tenant_provisioning_secret is None: pytest.skip("requires the local tenant provisioning secret") return settings def terminal_run(client: TestClient, run_id: str, headers: dict[str, str]) -> dict: for _ in range(100): response = client.get(f"/v1/lead-scoring/runs/{run_id}", headers=headers) assert response.status_code == 200 result = response.json() if result["status"] in {"completed", "failed"}: return result time.sleep(0.02) pytest.fail("Scoring run did not reach a terminal state") def test_api_seed_persists_and_reads_back_conversation_snapshot(): settings = isolated_test_settings() provisioning_secret = settings.tenant_provisioning_secret assert provisioning_secret is not None payload = json.loads(FIXTURE.read_text()) expected_payload = RunRequest.model_validate(payload).model_dump(mode="json") tenant_id = f"seed-persistence-{uuid4().hex}" app = create_app(settings) session_factory = make_session_factory(settings.database_url) run_id: str | None = None try: with TestClient(app) as client: provisioned = client.post( "/v1/platform/tenant-keys", headers={"Authorization": ("Bearer " + provisioning_secret.get_secret_value())}, json={"tenant_id": tenant_id}, ) assert provisioned.status_code == 201 tenant_headers = { "Authorization": f"Bearer {provisioned.json()['tenant_key']}", "Idempotency-Key": payload["request_id"], } admitted = client.post("/v1/lead-scoring/runs", headers=tenant_headers, json=payload) assert admitted.status_code == 202 run_id = admitted.json()["run_id"] assert isinstance(run_id, str) completed = terminal_run(client, run_id, tenant_headers) assert completed["status"] == "completed" assert completed["ai_score"] is None assert completed["degraded"] is False assert completed["rule_score"] == 80 assert completed["final_score"] == 80 readback = client.get(f"/v1/lead-scoring/runs/{run_id}", headers=tenant_headers) assert readback.status_code == 200 assert readback.json() == completed api_snapshot = readback.json().get("snapshot") if api_snapshot is None: api_snapshot = readback.json().get("request", {}).get("snapshot") assert api_snapshot == expected_payload["snapshot"] finally: async def verify_and_cleanup_created_rows() -> None: try: async with session_factory() as session: if run_id is not None: row = await session.scalar( select(ScoringRun).where( ScoringRun.id == run_id, ScoringRun.tenant_id == tenant_id, ) ) assert row is not None assert row.request["snapshot"] == expected_payload["snapshot"] await session.execute(delete(ScoringRun).where(ScoringRun.id == run_id)) await session.execute(delete(TenantKey).where(TenantKey.tenant_id == tenant_id)) await session.commit() finally: await session_factory.kw["bind"].dispose() asyncio.run(verify_and_cleanup_created_rows())