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") 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", ) )