dotb_ai-platform/tests/test_seed_demo.py

174 lines
5.8 KiB
Python

from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from typing import Any
import pytest
from ai_platform.infrastructure.config.settings import Settings
from ai_platform.presentation.http.schemas import RunRequest
SEED_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "seed_demo.py"
SEED_SPEC = spec_from_file_location("seed_demo_script", SEED_SCRIPT)
assert SEED_SPEC is not None and SEED_SPEC.loader is not None
seed_demo = module_from_spec(SEED_SPEC)
SEED_SPEC.loader.exec_module(seed_demo)
class FakeResponse:
def __init__(self, status_code: int, body: dict[str, Any]):
self.status_code = status_code
self._body = body
def json(self) -> dict[str, Any]:
return self._body
class FakeClient:
def __init__(self, responses: list[FakeResponse]):
self.responses = responses
self.calls: list[tuple[str, str, dict[str, Any]]] = []
def post(self, path: str, **kwargs: Any) -> FakeResponse:
self.calls.append(("POST", path, kwargs))
return self.responses.pop(0)
def get(self, path: str, **kwargs: Any) -> FakeResponse:
self.calls.append(("GET", path, kwargs))
return self.responses.pop(0)
def settings() -> Settings:
return Settings(
environment="development",
database_url="postgresql+asyncpg://local@127.0.0.1:55432/ai_platform",
)
def complete_run_response(run_id: str, payload: dict[str, Any]) -> FakeResponse:
request = RunRequest.model_validate(payload).model_dump(mode="json")
return FakeResponse(
200,
{
"run_id": run_id,
"status": "completed",
"rule_score": 80,
"ai_score": None,
"final_score": 80,
"degraded": False,
"request": {"snapshot": request["snapshot"]},
},
)
def test_fixture_has_realistic_conversations_and_metrics_rules():
payload = seed_demo.load_payload()
assert len(payload["snapshot"]["conversations"]) == 2
assert sum(len(c["messages"]) for c in payload["snapshot"]["conversations"]) == 5
rules = payload["scoring_config"]["rules"]
assert {rule["field"] for rule in rules} >= {
"metrics.conversation_count",
"metrics.contact_message_count",
}
assert sum(rule["points"] for rule in rules) == 80
assert payload["scoring_config"]["ai_weight"] == 0
def test_seed_creates_one_run_and_reads_persisted_conversation_snapshot():
payload = seed_demo.load_payload()
run_id = "123e4567-e89b-12d3-a456-426614174000"
client = FakeClient(
[
FakeResponse(202, {"run_id": run_id}),
complete_run_response(run_id, payload),
]
)
actual_id, result = seed_demo.seed_demo(client, settings(), "test-app-key")
assert actual_id == run_id
assert result["status"] == "completed"
assert result["ai_score"] is None
assert [call[1] for call in client.calls] == [
"/v1/lead-scoring/runs",
f"/v1/lead-scoring/runs/{run_id}",
]
assert client.calls[0][2]["json"] == payload
assert client.calls[0][2]["headers"]["Idempotency-Key"] == payload["request_id"]
assert client.calls[1][2]["headers"]["Authorization"] == "Bearer test-app-key"
def test_missing_app_key_stops_without_submitting_a_run():
client = FakeClient([])
with pytest.raises(SystemExit, match="operator-issued app key"):
seed_demo.seed_demo(client, settings())
assert client.calls == []
def test_existing_app_key_accepts_idempotent_replay():
payload = seed_demo.load_payload()
run_id = "123e4567-e89b-12d3-a456-426614174000"
client = FakeClient(
[
FakeResponse(200, {"run_id": run_id}),
complete_run_response(run_id, payload),
]
)
actual_id, _result = seed_demo.seed_demo(client, settings(), "existing-app-secret")
assert actual_id == run_id
assert [call[1] for call in client.calls] == [
"/v1/lead-scoring/runs",
f"/v1/lead-scoring/runs/{run_id}",
]
assert client.calls[0][2]["headers"]["Authorization"] == "Bearer existing-app-secret"
def test_seed_rejects_readback_without_persisted_snapshot():
run_id = "123e4567-e89b-12d3-a456-426614174000"
client = FakeClient(
[
FakeResponse(202, {"run_id": run_id}),
FakeResponse(
200,
{
"run_id": run_id,
"status": "completed",
"ai_score": None,
"degraded": False,
},
),
]
)
with pytest.raises(SystemExit, match="did not return the persisted conversation snapshot"):
seed_demo.seed_demo(client, settings(), "existing-app-secret")
@pytest.mark.usefixtures("isolated_settings")
def test_seed_refuses_non_development_or_nonlocal_database_names():
with pytest.raises(SystemExit, match="must be development"):
seed_demo.validate_local_target(Settings(environment="production"))
with pytest.raises(SystemExit, match="API target must use the local"):
seed_demo.validate_local_target(
Settings(
environment="development",
database_url="postgresql+asyncpg://local@127.0.0.1:55432/ai_platform_test",
)
)
with pytest.raises(SystemExit, match="API target must use the local"):
seed_demo.validate_local_target(
Settings(
environment="development",
database_url="postgresql+asyncpg://local@remote.example:5432/ai_platform",
)
)
with pytest.raises(SystemExit, match="API target must use the local"):
seed_demo.validate_local_target(
Settings(
environment="development",
database_url="postgresql+asyncpg://local@db:5432/customer_data",
)
)