dotb_ai-platform/tests/test_ai_profile_admin_api.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

113 lines
4.2 KiB
Python

from uuid import uuid4
import json
import httpx
import pytest
from fastapi.testclient import TestClient
from pwdlib import PasswordHash
from sqlalchemy import delete, func, select
from ai_platform.infrastructure.persistence.models import (
AdminSession,
AdminUser,
AIProfile,
ScoringRun,
)
PROFILE = {
"provider": "aihub",
"base_url": "http://localhost:8080/v1",
"model": "openai/gpt-6-luna",
"reasoning_effort": "low",
"instructions": "Chấm điểm lead giáo dục.",
}
COMPLETION = {
"signals": [],
"score": 70,
"summary": "Tạm ổn.",
"suggested_action": "Gọi lại.",
}
@pytest.fixture
def admin(score_env):
email, password = f"admin-{uuid4().hex}@example.invalid", "test-only-password"
async def add(db):
db.add(AdminUser(email=email, password_hash=PasswordHash.recommended().hash(password)))
async def drop(db):
await db.execute(delete(AdminSession).where(AdminSession.admin_email == email))
await db.execute(delete(AdminUser).where(AdminUser.email == email))
score_env["read"](add)
yield {"email": email, "password": password}
score_env["read"](drop)
def _login(client, admin):
login = client.post("/v1/admin/login", json=admin)
assert login.status_code == 200
return {"x-csrf-token": login.json()["csrf_token"]}
def test_save_creates_a_revision_and_activation_is_a_separate_step(score_env, admin):
app = score_env["app_for"](lambda r: httpx.Response(500))
with TestClient(app) as client:
assert client.post("/v1/admin/ai-profiles", json=PROFILE).status_code == 401
csrf = _login(client, admin)
no_csrf = client.post("/v1/admin/ai-profiles", json=PROFILE)
invalid = [
{**PROFILE, "base_url": "http://remote.example/v1"},
{**PROFILE, "timezone": "Not/AZone"},
{**PROFILE, "reasoning_effort": "extreme"},
]
rejected = [client.post("/v1/admin/ai-profiles", json=p, headers=csrf) for p in invalid]
first = client.post("/v1/admin/ai-profiles", json=PROFILE, headers=csrf).json()
second = client.post(
"/v1/admin/ai-profiles", json={**PROFILE, "model": "other"}, headers=csrf
).json()
activated = client.post(f"/v1/admin/ai-profiles/{first['revision']}/activate", headers=csrf)
missing = client.post("/v1/admin/ai-profiles/999999999/activate", headers=csrf)
listed = client.get("/v1/admin/ai-profiles").json()["profiles"]
score_env["read"](
lambda db: db.execute(
delete(AIProfile).where(AIProfile.revision.in_([first["revision"], second["revision"]]))
)
)
assert no_csrf.status_code == 403
assert [r.status_code for r in rejected] == [422, 422, 422]
assert second["revision"] > first["revision"] and not first["active"]
assert (activated.status_code, activated.json()["active"], missing.status_code) == (
200,
True,
404,
)
active = [p["revision"] for p in listed if p["active"]]
assert active == [first["revision"]]
def test_try_runs_a_draft_without_saving_a_run_or_changing_the_active_profile(
score_env, admin, canonical_request
):
canonical_request["scoring_config"].update(rule_weight=0.5, ai_weight=0.5)
before = score_env["read"](lambda db: db.scalar(select(func.count()).select_from(ScoringRun)))
handler = lambda r: httpx.Response( # noqa: E731
200, json={"choices": [{"message": {"content": json.dumps(COMPLETION)}}]}
)
with TestClient(score_env["app_for"](handler)) as client:
csrf = _login(client, admin)
response = client.post(
"/v1/admin/ai-profiles/try",
headers={**csrf, "X-AI-Key": score_env["ai_key"]},
json={"profile": PROFILE, "request": canonical_request},
)
active = [p for p in client.get("/v1/admin/ai-profiles").json()["profiles"] if p["active"]]
body = response.json()
assert (response.status_code, body["result_mode"], body["ai_score"]) == (200, "hybrid", 70)
assert before == score_env["read"](
lambda db: db.scalar(select(func.count()).select_from(ScoringRun))
)
assert [p["model"] for p in active] == ["test-model"]