import json from datetime import UTC, datetime import httpx import pytest from ai_platform.infrastructure.chat_completions import make_provider_factory from ai_platform.lead_scoring.application.ai_provider import AIProviderFailure from ai_platform.lead_scoring.application.repository import AIProfileRecord GOOD = { "signals": [{"polarity": "negative", "signal": "s" * 200, "detail": "d", "source": "m1"}], "score": 30, "summary": "x" * 400, "suggested_action": None, } def _profile(provider="aihub", base_url="https://ai.example/v1", **overrides) -> AIProfileRecord: fields = dict( provider=provider, base_url=base_url, model="m", timeout_seconds=5, reasoning_effort=None, instructions="i", include_reference_date=False, timezone="UTC", suggested_action_enabled=True, revision=1, active=True, created_by="a", created_at=datetime(2026, 1, 1, tzinfo=UTC), ) return AIProfileRecord(**{**fields, **overrides}) async def _assess(handler, profile, ai_key="caller-key", **factory_args): seen: list[httpx.Request] = [] def record(request: httpx.Request) -> httpx.Response: seen.append(request) return handler(request) async with httpx.AsyncClient(transport=httpx.MockTransport(record)) as client: factory = make_provider_factory( client, environment=factory_args.get("environment", "production"), server_base_url=factory_args.get("server_base_url"), server_key=factory_args.get("server_key"), ) provider = factory(profile, ai_key) result = await provider.assess( model=profile.model, system_prompt="s", user_prompt="u", reasoning_effort=profile.reasoning_effort, timeout=5, ) return result, seen def _ok(content=GOOD): return lambda request: httpx.Response( 200, json={"choices": [{"message": {"content": json.dumps(content)}}]} ) @pytest.mark.asyncio async def test_request_shape_and_trimmed_response(): result, seen = await _assess(_ok(), _profile()) body = json.loads(seen[0].content) assert str(seen[0].url) == "https://ai.example/v1/chat/completions" assert seen[0].headers["authorization"] == "Bearer caller-key" assert ("temperature" in body, "reasoning_effort" in body) == (False, False) assert body["response_format"] == {"type": "json_object"} assert (len(result.summary), len(result.signals[0].signal), result.score) == (300, 80, 30) _, seen = await _assess(_ok(), _profile(reasoning_effort="low")) body = json.loads(seen[0].content) assert body["reasoning_effort"] == "low" @pytest.mark.asyncio async def test_openai_compatible_sends_the_server_key_only_to_its_own_url(): own = _profile("openai_compatible", "https://proxy.example/v1") other = _profile("openai_compatible", "https://other.example/v1") args = {"server_base_url": "https://proxy.example/v1/", "server_key": "server-key"} _, seen_own = await _assess(_ok(), own, **args) _, seen_other = await _assess(_ok(), other, **args) assert seen_own[0].headers["authorization"] == "Bearer server-key" assert "authorization" not in seen_other[0].headers @pytest.mark.asyncio @pytest.mark.parametrize( ("handler", "error_code"), [ (lambda r: httpx.Response(500), "ai_http_error"), (lambda r: httpx.Response(200, json={"choices": []}), "ai_invalid_response"), (_ok({**GOOD, "score": 101}), "ai_invalid_assessment"), (_ok({**GOOD, "extra": 1}), "ai_invalid_assessment"), ], ) async def test_bad_responses_become_stable_error_codes(handler, error_code): with pytest.raises(AIProviderFailure) as raised: await _assess(handler, _profile()) assert raised.value.error_code == error_code @pytest.mark.asyncio async def test_unsafe_profile_url_is_refused_before_any_request(): with pytest.raises(AIProviderFailure, match="ai_url_not_allowed"): await _assess(_ok(), _profile(base_url="http://remote.example/v1"))