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

208 lines
6.4 KiB
Python

from datetime import UTC, datetime
from typing import Any
import pytest
from ai_platform.lead_scoring.application.admin import (
AdminScoringUseCases,
AIProfileVersionConflict,
AIProviderUnavailable,
)
from ai_platform.lead_scoring.application.repository import (
AIProfileRecord,
NewAIProfile,
)
class FakeScoringRepository:
def __init__(self) -> None:
self.profiles: dict[str, AIProfileRecord] = {}
self.audit_events: list[tuple[str, str, str, datetime]] = []
async def active_profile_version(self) -> str | None:
return "profile-v1"
async def list_profiles(self):
return list(self.profiles.values())
async def find_profile(self, version: str):
return self.profiles.get(version)
async def activate_profile(self, profile: NewAIProfile):
for version, current in list(self.profiles.items()):
self.profiles[version] = AIProfileRecord(
version=current.version,
model=current.model,
system_prompt=current.system_prompt,
timeout_seconds=current.timeout_seconds,
limits=current.limits,
active=False,
provider=current.provider,
base_url=current.base_url,
)
active = AIProfileRecord(
version=profile.version,
model=profile.model,
system_prompt=profile.system_prompt,
timeout_seconds=profile.timeout_seconds,
limits=profile.limits,
active=True,
provider=profile.provider,
base_url=profile.base_url,
)
self.profiles[profile.version] = active
return active
async def audit_profile_activation(self, actor, version, provider, occurred_at):
self.audit_events.append((actor, version, provider, occurred_at))
async def list_recent_runs(self, limit: int):
return []
async def find_run(self, run_id: str):
return None
async def find_by_request(self, *_: Any, **__: Any):
return None
async def save_run(self, *_: Any, **__: Any):
raise NotImplementedError
class Transaction:
def __init__(self) -> None:
self.commits = 0
self.rollbacks = 0
async def commit(self) -> None:
self.commits += 1
async def rollback(self) -> None:
self.rollbacks += 1
async def test_admin_profile_versions_are_immutable():
repository = FakeScoringRepository()
repository.profiles["profile-v1"] = AIProfileRecord(
version="profile-v1",
model="model-old",
system_prompt="prompt",
timeout_seconds=30,
limits={},
active=True,
)
transaction = Transaction()
use_cases = AdminScoringUseCases(
repository,
transaction.commit,
transaction.rollback,
frozenset({"aihub", "openai_compatible"}),
lambda: datetime(2026, 9, 25, tzinfo=UTC),
)
with pytest.raises(AIProfileVersionConflict):
await use_cases.activate_profile(
NewAIProfile(
version="profile-v1",
model="model-new",
system_prompt="prompt",
timeout_seconds=30,
limits={},
),
actor="admin@example.invalid",
)
assert transaction.commits == 0
assert transaction.rollbacks == 1
assert repository.profiles["profile-v1"].model == "model-old"
async def test_admin_profile_activation_commits_through_use_case():
repository = FakeScoringRepository()
transaction = Transaction()
use_cases = AdminScoringUseCases(
repository,
transaction.commit,
transaction.rollback,
frozenset({"aihub", "openai_compatible"}),
lambda: datetime(2026, 9, 25, tzinfo=UTC),
)
requested = NewAIProfile(
version="profile-v2",
model="model",
system_prompt="prompt",
timeout_seconds=30,
limits={"max_tokens": 100},
)
activated = await use_cases.activate_profile(requested, actor="admin@example.invalid")
assert activated.active is True
assert activated.version == requested.version
assert repository.audit_events == [
("admin@example.invalid", requested.version, "aihub", datetime(2026, 9, 25, tzinfo=UTC))
]
assert transaction.commits == 1
assert transaction.rollbacks == 0
async def test_default_openai_url_is_frozen_in_profile_version():
repository = FakeScoringRepository()
transaction = Transaction()
cases = AdminScoringUseCases(
repository,
transaction.commit,
transaction.rollback,
frozenset({"openai_compatible"}),
lambda: datetime(2026, 9, 28, tzinfo=UTC),
openai_base_url="http://127.0.0.1:8317",
)
requested = NewAIProfile("v1", "model", "prompt", 30, {}, "openai_compatible")
activated = await cases.activate_profile(requested, "admin@example.invalid")
assert activated.base_url == "http://127.0.0.1:8317"
assert repository.profiles["v1"].base_url == activated.base_url
async def test_provider_change_requires_new_profile_version_and_configured_adapter():
repository = FakeScoringRepository()
transaction = Transaction()
use_cases = AdminScoringUseCases(
repository,
transaction.commit,
transaction.rollback,
frozenset({"aihub"}),
lambda: datetime(2026, 9, 28, tzinfo=UTC),
)
requested = NewAIProfile(
"v1",
"gemini-3.5-flash-lite",
"prompt",
30,
{},
"openai_compatible",
"http://127.0.0.1:8317",
)
with pytest.raises(AIProviderUnavailable):
await use_cases.activate_profile(requested, "admin@example.invalid")
assert transaction.commits == 0
assert repository.audit_events == []
use_cases = AdminScoringUseCases(
repository,
transaction.commit,
transaction.rollback,
frozenset({"aihub", "openai_compatible"}),
lambda: datetime(2026, 9, 28, tzinfo=UTC),
)
await use_cases.activate_profile(requested, "admin@example.invalid")
with pytest.raises(AIProfileVersionConflict):
await use_cases.activate_profile(
NewAIProfile("v1", requested.model, requested.system_prompt, 30, {}, "aihub"),
"admin@example.invalid",
)
assert transaction.commits == 1
assert len(repository.audit_events) == 1
assert repository.profiles["v1"].provider == "openai_compatible"