dotb_ai-platform/tests/test_seed_demo_persistence.py
2026-09-25 09:55:25 +07:00

108 lines
4.4 KiB
Python

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']}"}
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())