174 lines
5.8 KiB
Python
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",
|
|
)
|
|
)
|