dotb_ai-platform/tests/test_evaluate.py
hungtran24 b1f6643796 refactor: score synchronously and drop webhook/background execution
POST /v1/lead-scoring/score now evaluates in the request, applies an overall
AI deadline, and stores the completed run only after scoring. Idempotency is
kept by (app_id, account_id, request_id) with a body fingerprint.

Removes the webhook outbox, scheduler/recovery loop, app lead_scoring
permission, credential audit events, seed_demo and generate_secret scripts,
and the tests that covered them. Trims tests to behavior of the new flow.
2026-10-09 02:44:24 +07:00

94 lines
3.4 KiB
Python

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