test: unit suite for the gateway invariants plus an integration slice
Covers routing, failover, the circuit breaker, structured-output retries, the cache-prefix validator, cost computation including cached-token pricing, the daily budget breaker, the quota protocol, the task state machine and log redaction. HTTP is mocked -- no unit test spends money. The integration test runs the real slice against PostgreSQL and Redis with a faked provider.
This commit is contained in:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
"""Global daily cost circuit breaker (架构文档 §13 三道成本闸门).
|
||||
|
||||
The requirement is enforcement, not alerting: once the platform-wide daily
|
||||
spend crosses the threshold, non-critical AI calls are rejected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.llm.budget import DailyBudgetGuard, InMemoryCostCounter
|
||||
from packages.llm.errors import DailyBudgetExceededError
|
||||
|
||||
|
||||
def make_guard(
|
||||
budget: str = "100", *, enabled: bool = True, now: datetime | None = None
|
||||
) -> tuple[DailyBudgetGuard, InMemoryCostCounter]:
|
||||
counter = InMemoryCostCounter()
|
||||
guard = DailyBudgetGuard(
|
||||
counter,
|
||||
daily_budget_cents=Decimal(budget),
|
||||
enabled=enabled,
|
||||
clock=(lambda: now) if now else None,
|
||||
)
|
||||
return guard, counter
|
||||
|
||||
|
||||
async def test_allows_calls_below_the_threshold() -> None:
|
||||
guard, _ = make_guard("100")
|
||||
await guard.add_cost(Decimal("99.9"))
|
||||
await guard.ensure_within_budget(scene="agent_chat")
|
||||
|
||||
|
||||
async def test_rejects_non_critical_scenes_once_the_budget_is_reached() -> None:
|
||||
guard, _ = make_guard("100")
|
||||
await guard.add_cost(Decimal("100"))
|
||||
with pytest.raises(DailyBudgetExceededError) as excinfo:
|
||||
await guard.ensure_within_budget(scene="agent_chat")
|
||||
assert "agent_chat" in str(excinfo.value)
|
||||
|
||||
|
||||
async def test_critical_scenes_bypass_the_breaker() -> None:
|
||||
guard, _ = make_guard("100")
|
||||
await guard.add_cost(Decimal("500"))
|
||||
# Degrade the product, do not break it: 意图识别/越界拒答 must keep working.
|
||||
await guard.ensure_within_budget(scene="intent_classify", critical=True)
|
||||
|
||||
|
||||
async def test_disabled_guard_never_blocks_and_never_counts() -> None:
|
||||
guard, counter = make_guard("1", enabled=False)
|
||||
await guard.add_cost(Decimal("999"))
|
||||
await guard.ensure_within_budget(scene="agent_chat")
|
||||
assert await counter.get(guard.current_key()) == Decimal("0")
|
||||
|
||||
|
||||
async def test_cost_accumulates_across_calls() -> None:
|
||||
guard, _ = make_guard("100")
|
||||
await guard.add_cost(Decimal("1.5"))
|
||||
await guard.add_cost(Decimal("2.25"))
|
||||
assert await guard.spent_today() == Decimal("3.75")
|
||||
|
||||
|
||||
async def test_zero_cost_is_not_counted() -> None:
|
||||
guard, _ = make_guard("100")
|
||||
await guard.add_cost(Decimal("0"))
|
||||
assert await guard.spent_today() == Decimal("0")
|
||||
|
||||
|
||||
async def test_budget_day_boundary_follows_utc_plus_8() -> None:
|
||||
"""FR-BILL-01: 跨时区按 UTC+8 统一。17:00Z already belongs to the next day."""
|
||||
guard, _ = make_guard(now=datetime(2026, 8, 13, 17, 0, tzinfo=UTC))
|
||||
assert guard.current_key().endswith("2026-08-14")
|
||||
|
||||
guard_before, _ = make_guard(now=datetime(2026, 8, 13, 15, 0, tzinfo=UTC))
|
||||
assert guard_before.current_key().endswith("2026-08-13")
|
||||
|
||||
|
||||
async def test_budget_properties_are_exposed_for_dashboards() -> None:
|
||||
guard, _ = make_guard("42")
|
||||
assert guard.enabled is True
|
||||
assert guard.budget_cents == Decimal("42")
|
||||
Reference in New Issue
Block a user