import asyncio from datetime import UTC, datetime from typing import cast import pytest from ai_platform.lead_scoring.application.ai_provider import ( AIProviderFailure, Assessment, ScoringAIProvider, Signal, ) from ai_platform.lead_scoring.application.repository import AIProfileRecord from ai_platform.lead_scoring.application.scoring_execution import evaluate def _profile(**overrides) -> AIProfileRecord: fields = dict( provider="aihub", base_url="http://localhost/v1", model="m", timeout_seconds=5, reasoning_effort="low", instructions="Rubric riêng.", include_reference_date=False, timezone="Asia/Ho_Chi_Minh", suggested_action_enabled=True, revision=1, active=True, created_by="a@example.invalid", created_at=datetime(2026, 1, 1, tzinfo=UTC), ) return AIProfileRecord(**{**fields, **overrides}) class Stub: def __init__(self) -> None: self.calls: list[dict] = [] async def assess(self, **kwargs): self.calls.append(kwargs) return Assessment([Signal("positive", "s", "d", "message-789")], 80, "Tóm tắt", "Gọi lại") def _request(canonical_request, **rule) -> dict: canonical_request["scoring_config"].update(rule_weight=0.5, ai_weight=0.5) canonical_request["scoring_config"]["rules"][0].update(rule) return canonical_request @pytest.mark.asyncio @pytest.mark.parametrize("description", [None, " ", "Khách chủ động hỏi điều kiện đăng ký"]) async def test_prompt_carries_profile_instructions_and_only_rule_descriptions( canonical_request, description ): stub = Stub() request = _request(canonical_request, id="private-rule-id", description=description) result = await evaluate(request, _profile(), "key", lambda p, k: stub) call = stub.calls[0] assert result.result_mode == "hybrid" and call["reasoning_effort"] == "low" assert call["system_prompt"].startswith("Rubric riêng.") has_description = bool(description and description.strip()) assert bool(description and description in call["user_prompt"]) is has_description for excluded in ('"points"', '"rule_weight"', "private-rule-id", "key"): assert excluded not in call["user_prompt"] @pytest.mark.asyncio async def test_profile_switches_control_reference_date_and_suggested_action(canonical_request): stub = Stub() request = _request(canonical_request) on = _profile(include_reference_date=True) off = _profile(suggested_action_enabled=False) dated = await evaluate(request, on, "key", lambda p, k: stub) quiet = await evaluate(request, off, "key", lambda p, k: stub) assert ( "Thời điểm chấm: Thứ Năm, 25/09/2025 17:00 (Asia/Ho_Chi_Minh)" in stub.calls[0]["user_prompt"] ) assert "Thời điểm chấm" not in stub.calls[1]["user_prompt"] assert (dated.ai or {})["suggested_action"] == "Gọi lại" assert (quiet.ai or {})["suggested_action"] is None @pytest.mark.asyncio async def test_provider_factory_failure_and_deadline_degrade_to_rule_score(canonical_request): class Hanging: async def assess(self, **_: object): await asyncio.sleep(10) def unsafe(profile, key): raise AIProviderFailure("ai_url_not_allowed") request = _request(canonical_request) bad_url = await evaluate(request, _profile(), "key", unsafe) slow = await evaluate( request, _profile(timeout_seconds=1), "key", lambda p, k: cast(ScoringAIProvider, Hanging()) ) assert bad_url.error_code == "ai_url_not_allowed" assert (slow.result_mode, slow.error_code) == ("rules_only_fallback", "ai_timeout") assert slow.final_score == slow.rule_score