Files
Edu/services/ai/tests/test_usage.py
2026-07-10 18:57:39 +08:00

149 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""用量记录 + 配额管理 + Kafka 生产者测试."""
import pytest
from src.ai.errors import AIQuotaExceededError
from src.ai.usage.kafka_producer import KafkaProducer, UsageEvent
from src.ai.usage.quota_enforcer import QuotaEnforcer, QuotaStatus
from src.ai.usage.usage_recorder import UsageRecord, UsageRecorder
class TestUsageRecorder:
"""UsageRecorder 测试Redis=None 降级模式)."""
async def test_record_no_redis_skipped(self) -> None:
"""Redis=None 时跳过记录."""
recorder = UsageRecorder(redis=None)
record = UsageRecord(
user_id="u-1",
school_id="s-1",
provider="openai",
model="gpt-4o",
operation="chat",
total_tokens=100,
)
await recorder.record(record) # 不抛异常即通过
async def test_get_user_usage_no_redis(self) -> None:
"""Redis=None 时返回 0."""
recorder = UsageRecorder(redis=None)
usage = await recorder.get_user_usage("u-1")
assert usage == 0
async def test_get_school_usage_no_redis(self) -> None:
"""Redis=None 时返回 0."""
recorder = UsageRecorder(redis=None)
usage = await recorder.get_school_usage("s-1")
assert usage == 0
class TestQuotaEnforcer:
"""QuotaEnforcer 测试."""
async def test_within_budget_allowed(self) -> None:
"""预算内允许."""
recorder = UsageRecorder(redis=None)
enforcer = QuotaEnforcer(
usage_recorder=recorder,
user_monthly_budget=100_000,
school_monthly_budget=1_000_000,
)
statuses = await enforcer.check("u-1", "s-1")
assert len(statuses) == 2
assert all(s.allowed for s in statuses)
async def test_exceeds_budget_raises(self) -> None:
"""超预算抛 AIQuotaExceededError."""
recorder = UsageRecorder(redis=None)
enforcer = QuotaEnforcer(
usage_recorder=recorder,
user_monthly_budget=0, # budget=0 → used(0) < 0 is False
)
# used=0, budget=0 → allowed = 0 < 0 = False
with pytest.raises(AIQuotaExceededError):
await enforcer.check("u-1")
async def test_no_user_id_no_check(self) -> None:
"""无 user_id 返回空列表."""
recorder = UsageRecorder(redis=None)
enforcer = QuotaEnforcer(usage_recorder=recorder)
statuses = await enforcer.check("")
assert statuses == []
async def test_only_user(self) -> None:
"""仅检查 user."""
recorder = UsageRecorder(redis=None)
enforcer = QuotaEnforcer(usage_recorder=recorder)
statuses = await enforcer.check("u-1")
assert len(statuses) == 1
assert statuses[0].scope == "user"
def test_quota_status_usage_percentage(self) -> None:
"""usage_percentage 计算."""
status = QuotaStatus(allowed=True, scope="user", used=50, budget=200, remaining=150)
assert status.usage_percentage == 25.0
def test_quota_status_zero_budget(self) -> None:
"""budget=0 时 usage_percentage=0."""
status = QuotaStatus(allowed=True, scope="user", used=10, budget=0, remaining=0)
assert status.usage_percentage == 0.0
class TestUsageEvent:
"""UsageEvent dataclass 测试."""
def test_to_dict_fills_defaults(self) -> None:
"""to_dict 填充 event_id + occurred_at."""
event = UsageEvent(user_id="u-1", operation="chat")
data = event.to_dict()
assert data["event_id"] # 自动生成
assert data["occurred_at"] > 0 # 自动填充
assert data["user_id"] == "u-1"
def test_from_usage_record(self) -> None:
"""from_usage_record 工厂方法."""
event = UsageEvent.from_usage_record(
user_id="u-1",
school_id="s-1",
request_id="r-1",
provider="openai",
model="gpt-4o",
operation="chat",
prompt_tokens=10,
completion_tokens=20,
total_tokens=30,
latency_ms=500,
)
assert event.event_id
assert event.aggregate_id == "r-1"
assert event.event_type == "AIUsageRecorded"
assert event.total_tokens == 30
class TestKafkaProducer:
"""KafkaProducer 测试(降级模式,不连真实 Kafka."""
async def test_start_without_kafka_degrades(self) -> None:
"""Kafka 不可用时降级."""
producer = KafkaProducer(bootstrap_servers="localhost:9999")
await producer.start()
assert producer.is_started is False
async def test_publish_not_started_skipped(self) -> None:
"""未启动时 publish 跳过."""
producer = KafkaProducer()
event = UsageEvent(user_id="u-1")
await producer.publish(event) # 不抛异常
async def test_stop_when_not_started(self) -> None:
"""未启动时 stop 不抛异常."""
producer = KafkaProducer()
await producer.stop()
async def test_stop_after_failed_start(self) -> None:
"""启动失败后 stop 不抛异常."""
producer = KafkaProducer(bootstrap_servers="localhost:9999")
await producer.start()
await producer.stop()
assert producer.is_started is False