dotb_ai-platform/tests/test_evaluate.py
hungtran24 6577bb74be refactor: simplify scoring service, AI profile revisions and test suite
Phase 2 and 3 of the platform rework.

- Neutral request/response contract: external_id, metadata, X-AI-Key,
  run_id in the response; client-specific fields removed.
- AI profile is a revision table: saving creates an inactive revision,
  activation is separate, try-run does not store a run. One
  chat-completions gateway serves AIHub and OpenAI-compatible profiles.
- Add between and in rule operators.
- Contracts moved to contracts/lead-scoring/ with a score-response fixture.
- Admin Web updated for revisions, runs and the neutral fixture.
- Consolidate tests; remove rewrite_architect.md and the stale handoff doc;
  rewrite spec, CONTEXT and ARCHITECTURE for the current design.

Not verified: live AIHub/OpenAI behavior, Omni/EMS integration.
2026-10-09 08:23:21 +07:00

105 lines
3.7 KiB
Python

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