Files
Claude Agent feb31eaabe fix(discord-bridge): plafonul de cost e optional (COST_CAP_USD_DAY=off)
Costul raportat de CLI e pretul echivalent la API; pe abonament nu se
factureaza, deci plafonul zilnic oprea puntea fara motiv (5.71 / 5.00 USD).

- limits.parse_cap(): `off`/`none`/`nelimitat`/`0`/gol/gunoi => fara plafon
- stopped()/record_cost() nu mai opresc si nu mai alerteaza cand e dezactivat
- implicit devine `off`; /status arata „(fara plafon)"
- dashboard: /api/status si doctor nu mai crapa pe valoare ne-numerica
- README + ops/env.example explica de ce ramane off pe abonament

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
2026-08-30 21:02:43 +00:00

140 lines
4.1 KiB
Python

"""T8: 4 procese, coada per fir, rate limit, plafon de cost."""
from __future__ import annotations
import asyncio
import pytest
import limits as L
def mkl(store=None, **kw):
kw.setdefault("alerter", lambda *a, **k: None)
return L.Limits(store, **kw)
async def test_maxim_4_procese_al_cincilea_asteapta():
lim = mkl(max_procs=4)
held = []
ev = asyncio.Event()
async def hold(i):
async with lim.process_slot():
held.append(i)
await ev.wait()
tasks = [asyncio.create_task(hold(i)) for i in range(5)]
await asyncio.sleep(0.05)
assert len(held) == 4 and lim.free_slots == 0
ev.set()
await asyncio.gather(*tasks)
assert len(held) == 5 and lim.free_slots == 4
async def test_coada_per_fir_serializeaza_acelasi_fir():
lim = mkl()
ordine = []
async def tur(tag, delay):
async with lim.thread_lock("1"):
ordine.append(f"start-{tag}")
await asyncio.sleep(delay)
ordine.append(f"stop-{tag}")
await asyncio.gather(tur("a", 0.02), tur("b", 0.0))
assert ordine == ["start-a", "stop-a", "start-b", "stop-b"]
async def test_fire_diferite_merg_in_paralel():
lim = mkl()
async with lim.thread_lock("1"):
assert lim.queued("1") and not lim.queued("2")
async with lim.thread_lock("2"):
pass
def test_rate_limit_per_user():
t = [1000.0]
lim = mkl(rate_per_min=3, clock=lambda: t[0])
for _ in range(3):
lim.admit("u1")
with pytest.raises(L.RateLimited) as exc:
lim.admit("u1")
assert exc.value.retry_after == pytest.approx(60.0)
lim.admit("u2") # alt utilizator nu e afectat
t[0] += 61.0
lim.admit("u1") # fereastra a trecut
def test_plafon_de_cost_opreste_botul(store):
lim = mkl(store, cost_cap=1.0)
assert not lim.stopped() and lim.cost_remaining() == pytest.approx(1.0)
lim.record_cost(0.4, "1")
lim.admit("u1")
assert lim.record_cost(0.7, "1") == pytest.approx(1.1)
assert lim.stopped() and lim.cost_remaining() == 0.0
with pytest.raises(L.CostCapReached):
lim.admit("u1")
def test_plafonul_off_nu_opreste_nimic(store, state_dir):
import config
(state_dir / "env").write_text("COST_CAP_USD_DAY=off\n")
config.reload(state_dir)
alerte = []
lim = L.Limits(store, alerter=lambda *a, **k: alerte.append(a))
assert lim.cost_cap == 0.0 and not lim.cap_enabled
lim.record_cost(99.0, "1")
assert not lim.stopped() and lim.cost_remaining() == float("inf")
lim.admit("u1") # nu ridica CostCapReached
assert alerte == []
@pytest.mark.parametrize("raw, asteptat", [
("off", 0.0), ("", 0.0), ("nelimitat", 0.0), ("0", 0.0), ("-3", 0.0),
("5.00", 5.0), (" 2.5 ", 2.5), ("gunoi", 0.0), (None, 0.0),
])
def test_parse_cap(raw, asteptat):
assert L.parse_cap(raw, 0.0) == pytest.approx(asteptat)
def test_plafon_alerteaza_o_singura_data(store):
alerte = []
lim = mkl(store, cost_cap=0.5, alerter=lambda *a, **k: alerte.append(a))
lim.record_cost(0.6, "1")
lim.record_cost(0.1, "1")
assert len(alerte) == 1 and alerte[0][0] == "CRITICAL"
def test_plafonul_se_ridica_a_doua_zi(store):
lim = mkl(store, cost_cap=1.0)
lim.record_cost(2.0, "1")
assert lim.stopped()
store.state["cost"]["day"] = "2000-01-01" # ziua se schimba
assert not lim.stopped()
def test_costul_fara_store_e_local():
lim = mkl(None, cost_cap=1.0)
lim.record_cost(0.5)
lim.record_cost("gunoi")
assert lim.cost_today() == pytest.approx(0.5)
def test_valorile_implicite_vin_din_config(state_dir):
import config
(state_dir / "env").write_text("COST_CAP_USD_DAY=2.5\nMAX_PROCS=2 # comentariu\n")
config.reload(state_dir)
lim = mkl()
assert lim.cost_cap == 2.5 and lim.max_procs == 2
assert lim.turn_timeout == 900.0
async def test_contextul_turn_verifica_tot(store):
lim = mkl(store, max_procs=1, cost_cap=10.0, turn_timeout=42.0)
async with lim.turn("1", "u1") as timeout:
assert timeout == 42.0
assert lim.free_slots == 0
assert lim.free_slots == 1