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"]