import asyncio import json from pathlib import Path from typing import cast import pytest from ai_platform.lead_scoring.application.ai_provider import ( AIProviderFailure, Evidence, Scoring, ScoringAIProvider, ) from ai_platform.lead_scoring.application.repository import AIProfileRecord from ai_platform.lead_scoring.application.scoring_execution import evaluate FIXTURE = Path(__file__).parent / "fixtures" / "mock_omni_run.json" class StubProvider: def __init__(self): self.calls: list[dict] = [] async def extract_evidence(self, *, model, snapshot, vkey, timeout): self.calls.append({"model": model, "vkey": vkey, "snapshot": snapshot}) return [Evidence("interest", "Asked about enrollment", "mock-message-001")] async def score(self, *, model, system_prompt, user_prompt, vkey, timeout): self.calls.append( {"model": model, "vkey": vkey, "system": system_prompt, "user": user_prompt} ) return Scoring(80, 0.8, "Evidence supports intent") def _request(**rule_overrides) -> dict: payload = json.loads(FIXTURE.read_text()) payload["scoring_config"].update(rule_weight=0.5, ai_weight=0.5) payload["scoring_config"]["rules"][0].update(rule_overrides) return payload @pytest.mark.asyncio async def test_openai_compatible_profile_uses_its_own_url_and_never_receives_the_ai_key(): provider, aihub = StubProvider(), StubProvider() profile = AIProfileRecord( "p", "local-model", "prompt", 30, {}, True, "openai_compatible", "http://127.0.0.1:8318" ) urls: list[str] = [] def provider_for_url(url: str) -> StubProvider: urls.append(url) return provider result = await evaluate(_request(), profile, "caller-key", {"aihub": aihub}, provider_for_url) assert result.result_mode == "hybrid" assert urls == ["http://127.0.0.1:8318"] assert [c["vkey"] for c in provider.calls] == ["", ""] assert not aihub.calls @pytest.mark.asyncio @pytest.mark.parametrize("description", [None, " ", "Khách chủ động hỏi điều kiện đăng ký"]) async def test_rule_descriptions_are_prompt_context_and_nothing_else_about_rules_leaks(description): provider = StubProvider() profile = AIProfileRecord("p", "model", "prompt", 30, {}, True) request = _request(id="private-rule-id", description=description) await evaluate(request, profile, "caller-key", {"aihub": provider}, None) prompt = provider.calls[1]["user"] if description and description.strip(): assert json.loads(prompt.split("\nRULE_DESCRIPTIONS:\n", 1)[1]) == [description] assert description not in provider.calls[1]["system"] else: assert "RULE_DESCRIPTIONS:" not in prompt for excluded in ('"points"', '"rule_weight"', "private-rule-id", "caller-key"): assert excluded not in prompt @pytest.mark.asyncio async def test_ai_deadline_degrades_to_rule_score(): class Hanging: async def extract_evidence(self, **_: object): await asyncio.sleep(10) async def score(self, **_: object): raise AIProviderFailure("unreachable") profile = AIProfileRecord("p", "m", "prompt", 1, {}, True) result = await evaluate( _request(), profile, "key", {"aihub": cast(ScoringAIProvider, Hanging())}, None ) assert (result.result_mode, result.error_code) == ("rules_only_fallback", "ai_timeout") assert result.final_score == result.rule_score