dotb_ai-platform/tests/test_profile_provider_execution.py
hungtran24 2268b6a77a feat: replace tenant auth with app-scoped scoring access
Add app credentials, account-scoped idempotency and per-app terminal notifications. Consolidate the unreleased migration baseline, preserve rule descriptions as AI context, and regenerate provider contracts with regression coverage.
2026-10-02 10:56:58 +07:00

178 lines
6.3 KiB
Python

import json
from datetime import UTC, datetime
from pathlib import Path
import pytest
from ai_platform.lead_scoring.application.repository import AIProfileRecord
from ai_platform.lead_scoring.application.scoring_execution import ScoringExecutionUseCases
from ai_platform.lead_scoring.application.scoring_execution_repository import (
Evidence,
Scoring,
ScoringExecutionRun,
ScoringResult,
)
FIXTURE = Path(__file__).parent / "fixtures" / "mock_omni_run.json"
class RunRepository:
def __init__(self, run: ScoringExecutionRun, profile: AIProfileRecord):
self.run = run
self.profile = profile
self.result: ScoringResult | None = None
async def find_run(self, run_id: str):
return self.run
async def find_profile(self, version: str):
assert version == self.run.profile_version
return self.profile
async def mark_running(self, run_id: str):
pass
async def complete(self, run_id: str, result: ScoringResult, completed_at: datetime):
self.result = result
async def fail(self, run_id: str, completed_at: datetime, error_code: str):
pytest.fail(f"execution failed: {error_code}")
async def mark_interrupted(self, completed_at: datetime):
pytest.fail("unexpected recovery")
class StubProvider:
def __init__(self):
self.models: list[str] = []
self.keys: list[str] = []
self.user_prompts: list[str] = []
self.system_prompts: list[str] = []
self.snapshots: list[dict] = []
async def extract_evidence(self, *, model, snapshot, vkey, timeout):
self.models.append(model)
self.keys.append(vkey)
self.snapshots.append(snapshot)
return [Evidence("interest", "Asked about enrollment", "mock-message-001")]
async def score(self, *, model, system_prompt, user_prompt, vkey, timeout):
self.models.append(model)
self.keys.append(vkey)
self.user_prompts.append(user_prompt)
self.system_prompts.append(system_prompt)
return Scoring(80, 0.8, "Evidence supports intent")
@pytest.mark.asyncio
async def test_execution_uses_frozen_profile_provider_and_never_forwards_vkey_to_openai():
payload = json.loads(FIXTURE.read_text())
payload["ai_profile"] = "medium"
payload["scoring_config"].update(rule_weight=0.5, ai_weight=0.5)
run = ScoringExecutionRun("run-1", "pending", payload, "old-profile")
profile = AIProfileRecord(
"old-profile",
"gemini-3.5-flash-lite",
"prompt",
30,
{},
False,
"openai_compatible",
"http://127.0.0.1:8318",
)
repository = RunRepository(run, profile)
old_provider = StubProvider()
active_aihub_provider = StubProvider()
async def no_op():
pass
selected_urls: list[str] = []
def provider_for_url(url: str) -> StubProvider:
selected_urls.append(url)
return old_provider
cases = ScoringExecutionUseCases(
repository,
{"aihub": active_aihub_provider},
no_op,
no_op,
lambda: datetime(2026, 9, 28, tzinfo=UTC),
provider_for_url,
)
await cases.execute(run.id, "caller-vkey-should-not-reach-openai")
assert repository.result is not None and repository.result.result_mode == "hybrid"
assert selected_urls == ["http://127.0.0.1:8318"]
assert old_provider.models == ["gemini-3.5-flash-lite"] * 2
assert old_provider.keys == ["", ""]
assert active_aihub_provider.models == []
@pytest.mark.parametrize("provider_name", ["aihub", "openai_compatible"])
@pytest.mark.parametrize("description", [None, "", " ", "Khách chủ động hỏi điều kiện đăng ký"])
async def test_execution_uses_descriptions_only_as_supplemental_scoring_context(
provider_name, description
):
payload = json.loads(FIXTURE.read_text())
payload["ai_profile"] = "medium"
payload["scoring_config"].update(rule_weight=0.5, ai_weight=0.5)
payload["scoring_config"]["rules"][0].update(id="private-rule-id", description=description)
run = ScoringExecutionRun("run-1", "pending", payload, "profile-v1")
profile = AIProfileRecord("profile-v1", "model", "prompt", 30, {}, True, provider_name)
repository = RunRepository(run, profile)
provider = StubProvider()
async def no_op():
pass
cases = ScoringExecutionUseCases(
repository,
{provider_name: provider},
no_op,
no_op,
lambda: datetime(2026, 9, 28, tzinfo=UTC),
)
await cases.execute(run.id, "caller-vkey")
assert repository.result is not None and repository.result.result_mode == "hybrid"
assert provider.snapshots == [payload["snapshot"]]
assert len(provider.user_prompts) == 1
prompt = provider.user_prompts[0]
if description and description.strip():
assert json.loads(prompt.split("\nRULE_DESCRIPTIONS:\n", 1)[1]) == [description]
assert description not in provider.system_prompts[0]
else:
assert "RULE_DESCRIPTIONS:" not in prompt
for excluded in ('"points"', '"rule_weight"', '"ai_weight"', "private-rule-id", "caller-vkey"):
assert excluded not in prompt
@pytest.mark.parametrize("vkey", ["private-vkey", 'private"vkey', "private\\vkey"])
@pytest.mark.parametrize("location", ["description", "snapshot"])
async def test_vkey_in_prompt_data_falls_back_before_calling_ai(vkey, location):
payload = json.loads(FIXTURE.read_text())
payload["ai_profile"] = "medium"
payload["scoring_config"].update(rule_weight=0.5, ai_weight=0.5)
if location == "description":
payload["scoring_config"]["rules"][0]["description"] = vkey
else:
payload["snapshot"]["lead_profile"]["name"] = vkey
run = ScoringExecutionRun("run-1", "pending", payload, "profile-v1")
profile = AIProfileRecord("profile-v1", "model", "prompt", 30, {}, True)
repository = RunRepository(run, profile)
provider = StubProvider()
async def no_op():
pass
cases = ScoringExecutionUseCases(
repository, {"aihub": provider}, no_op, no_op, lambda: datetime(2026, 9, 28, tzinfo=UTC)
)
await cases.execute(run.id, vkey)
assert repository.result is not None
assert repository.result.error_code == "vkey_prompt_conflict"
assert repository.result.result_mode == "rules_only_fallback"
assert provider.models == []