dotb_ai-platform/tests/test_chat_completions.py
hungtran24 5c61227e6f fix: drop temperature from AI requests and make the trial scenario editable
- gpt-6-luna on AIHub rejects temperature=0 (HTTP 400), which made every
  call without reasoning_effort fall back; no longer send temperature.
- Fixtures page edits the scenario as JSON instead of a fixed preview;
  backend validation errors are shown.
- AI profile form keeps submitted values after a trial run.
- Record the live AIHub check in the spec and README.
2026-10-09 08:32:19 +07:00

115 lines
4.0 KiB
Python

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