forked from retoor/devplacepy
docs: add CLAUDE.md with project guidelines and conventions, document new devii admin quota reset endpoints and CLI commands, update README with coverage CI and devii_admin_daily_usd config, add European date normalization to ProjectForm model
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table, set_setting
|
||||
from devplacepy.utils import generate_uid
|
||||
from devplacepy.services.devii.config import (
|
||||
effective_daily_limit, FIELD_USER_DAILY_USD, FIELD_GUEST_DAILY_USD, FIELD_ADMIN_DAILY_USD,
|
||||
)
|
||||
from devplacepy.services.devii.store import UsageLedger
|
||||
|
||||
|
||||
LEDGER = "devii_usage_ledger"
|
||||
|
||||
|
||||
def _now_iso():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _seed_ledger(owner_kind, owner_id, cost, n=1):
|
||||
table = get_table(LEDGER)
|
||||
for _ in range(n):
|
||||
table.insert({
|
||||
"owner_kind": owner_kind, "owner_id": owner_id,
|
||||
"created_at": _now_iso(), "cost_usd": cost,
|
||||
})
|
||||
|
||||
|
||||
def _ensure_devii():
|
||||
from devplacepy.services.manager import service_manager
|
||||
from devplacepy.services.devii import DeviiService
|
||||
if service_manager.get_service("devii") is None:
|
||||
service_manager.register(DeviiService())
|
||||
return service_manager.get_service("devii")
|
||||
|
||||
|
||||
def _make_user(role="Member"):
|
||||
uid = generate_uid()
|
||||
username = f"quota_{uid[-12:]}"
|
||||
get_table("users").insert({
|
||||
"uid": uid, "username": username, "email": f"{username}@t.dev",
|
||||
"api_key": generate_uid(), "role": role, "is_active": True,
|
||||
})
|
||||
return uid, username
|
||||
|
||||
|
||||
# ---------- The effective-limit resolver (single source of truth) ----------
|
||||
|
||||
def test_effective_limit_resolver():
|
||||
cfg = {FIELD_USER_DAILY_USD: 1.0, FIELD_GUEST_DAILY_USD: 0.05, FIELD_ADMIN_DAILY_USD: 0.0}
|
||||
assert effective_daily_limit(cfg, "user", False) == 1.0
|
||||
assert effective_daily_limit(cfg, "guest", False) == 0.05
|
||||
assert effective_daily_limit(cfg, "user", True) == 0.0
|
||||
cfg[FIELD_ADMIN_DAILY_USD] = 5.0
|
||||
assert effective_daily_limit(cfg, "user", True) == 5.0
|
||||
|
||||
|
||||
# ---------- Admin exempt by default; user cap still enforced ----------
|
||||
|
||||
def test_admin_exempt_by_default(local_db):
|
||||
from devplacepy.routers.profile import _ai_quota
|
||||
_ensure_devii()
|
||||
set_setting("devii_user_daily_usd", "1.0")
|
||||
set_setting("devii_admin_daily_usd", "0.0")
|
||||
try:
|
||||
uid, _ = _make_user()
|
||||
_seed_ledger("user", uid, 5.0)
|
||||
member_view = _ai_quota(uid, is_admin=False)
|
||||
admin_view = _ai_quota(uid, is_admin=True)
|
||||
assert member_view["used_pct"] == 100.0
|
||||
assert admin_view["used_pct"] == 0.0
|
||||
finally:
|
||||
set_setting("devii_user_daily_usd", "1.0")
|
||||
set_setting("devii_admin_daily_usd", "0.0")
|
||||
|
||||
|
||||
def test_admin_limit_is_zero_by_default(local_db):
|
||||
svc = _ensure_devii()
|
||||
set_setting("devii_admin_daily_usd", "0.0")
|
||||
try:
|
||||
assert svc.daily_limit_for("user", True) == 0.0
|
||||
assert svc.daily_limit_for("user", False) == 1.0
|
||||
finally:
|
||||
set_setting("devii_admin_daily_usd", "0.0")
|
||||
|
||||
|
||||
# ---------- Admin and guest caps are configurable ----------
|
||||
|
||||
def test_admin_cap_configurable(local_db):
|
||||
from devplacepy.routers.profile import _ai_quota
|
||||
_ensure_devii()
|
||||
set_setting("devii_admin_daily_usd", "2.0")
|
||||
try:
|
||||
uid, _ = _make_user()
|
||||
_seed_ledger("user", uid, 1.0)
|
||||
quota = _ai_quota(uid, is_admin=True)
|
||||
assert quota["used_pct"] == 50.0
|
||||
finally:
|
||||
set_setting("devii_admin_daily_usd", "0.0")
|
||||
|
||||
|
||||
def test_guest_cap_configurable(local_db):
|
||||
svc = _ensure_devii()
|
||||
set_setting("devii_guest_daily_usd", "0.10")
|
||||
try:
|
||||
assert svc.daily_limit_for("guest") == 0.10
|
||||
finally:
|
||||
set_setting("devii_guest_daily_usd", "0.05")
|
||||
|
||||
|
||||
# ---------- Resetting quotas (the ledger drives the rolling 24h spend) ----------
|
||||
|
||||
def test_reset_single_owner(local_db):
|
||||
ledger = UsageLedger()
|
||||
uid, _ = _make_user()
|
||||
_seed_ledger("user", uid, 0.5, n=3)
|
||||
assert ledger.spent_24h("user", uid) == 1.5
|
||||
removed = ledger.reset("user", uid)
|
||||
assert removed == 3
|
||||
assert ledger.spent_24h("user", uid) == 0.0
|
||||
|
||||
|
||||
def test_reset_guests_only_leaves_users(local_db):
|
||||
ledger = UsageLedger()
|
||||
guest_id = f"guest-{generate_uid()[:8]}"
|
||||
user_id, _ = _make_user()
|
||||
_seed_ledger("guest", guest_id, 0.02, n=2)
|
||||
_seed_ledger("user", user_id, 0.5, n=1)
|
||||
ledger.reset_owner_kind("guest")
|
||||
assert ledger.spent_24h("guest", guest_id) == 0.0
|
||||
assert ledger.spent_24h("user", user_id) == 0.5
|
||||
|
||||
|
||||
def test_reset_all(local_db):
|
||||
ledger = UsageLedger()
|
||||
guest_id = f"guest-{generate_uid()[:8]}"
|
||||
user_id, _ = _make_user()
|
||||
_seed_ledger("guest", guest_id, 0.02, n=2)
|
||||
_seed_ledger("user", user_id, 0.5, n=2)
|
||||
ledger.reset_all()
|
||||
assert ledger.spent_24h("guest", guest_id) == 0.0
|
||||
assert ledger.spent_24h("user", user_id) == 0.0
|
||||
|
||||
|
||||
# ---------- CLI: devplace devii reset-quota ----------
|
||||
|
||||
def test_cli_reset_quota_for_user(local_db):
|
||||
from devplacepy.cli import cmd_devii_reset_quota
|
||||
uid, username = _make_user()
|
||||
_seed_ledger("user", uid, 0.5, n=2)
|
||||
cmd_devii_reset_quota(SimpleNamespace(username=username, guests=False, all=False))
|
||||
assert get_table(LEDGER).count(owner_kind="user", owner_id=uid) == 0
|
||||
|
||||
|
||||
def test_cli_reset_quota_guests(local_db):
|
||||
from devplacepy.cli import cmd_devii_reset_quota
|
||||
guest_id = f"guest-{generate_uid()[:8]}"
|
||||
user_id, _ = _make_user()
|
||||
_seed_ledger("guest", guest_id, 0.02, n=2)
|
||||
_seed_ledger("user", user_id, 0.5, n=1)
|
||||
cmd_devii_reset_quota(SimpleNamespace(username=None, guests=True, all=False))
|
||||
assert get_table(LEDGER).count(owner_kind="guest", owner_id=guest_id) == 0
|
||||
assert get_table(LEDGER).count(owner_kind="user", owner_id=user_id) == 1
|
||||
|
||||
|
||||
def test_cli_reset_quota_all(local_db):
|
||||
from devplacepy.cli import cmd_devii_reset_quota
|
||||
guest_id = f"guest-{generate_uid()[:8]}"
|
||||
user_id, _ = _make_user()
|
||||
_seed_ledger("guest", guest_id, 0.02, n=1)
|
||||
_seed_ledger("user", user_id, 0.5, n=1)
|
||||
cmd_devii_reset_quota(SimpleNamespace(username=None, guests=False, all=True))
|
||||
assert get_table(LEDGER).count() == 0
|
||||
|
||||
|
||||
# ---------- Admin HTTP endpoints (end-to-end against the running app) ----------
|
||||
|
||||
def test_admin_reset_user_quota_endpoint(alice):
|
||||
page, _ = alice
|
||||
bob = get_table("users").find_one(username="bob_test")
|
||||
get_table(LEDGER).insert(
|
||||
{"owner_kind": "user", "owner_id": bob["uid"], "created_at": _now_iso(), "cost_usd": 0.5})
|
||||
resp = page.request.post(f"{BASE_URL}/admin/users/{bob['uid']}/reset-ai-quota")
|
||||
assert resp.ok
|
||||
assert get_table(LEDGER).count(owner_kind="user", owner_id=bob["uid"]) == 0
|
||||
|
||||
|
||||
def test_admin_reset_all_endpoint(alice):
|
||||
page, _ = alice
|
||||
_seed_ledger("guest", f"guest-{generate_uid()[:8]}", 0.01)
|
||||
_seed_ledger("user", _make_user()[0], 0.5)
|
||||
resp = page.request.post(f"{BASE_URL}/admin/ai-quota/reset-all")
|
||||
assert resp.ok
|
||||
assert get_table(LEDGER).count() == 0
|
||||
|
||||
|
||||
def test_non_admin_cannot_reset_quota(bob):
|
||||
page, _ = bob
|
||||
marker = f"guest-{generate_uid()[:8]}"
|
||||
get_table(LEDGER).insert(
|
||||
{"owner_kind": "guest", "owner_id": marker, "created_at": _now_iso(), "cost_usd": 0.01})
|
||||
page.request.post(f"{BASE_URL}/admin/ai-quota/reset-all")
|
||||
assert get_table(LEDGER).count(owner_kind="guest", owner_id=marker) == 1
|
||||
get_table(LEDGER).delete(owner_kind="guest", owner_id=marker)
|
||||
@@ -35,7 +35,7 @@ def _create_project(page, title):
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", title)
|
||||
page.fill("#description", "Project for testing")
|
||||
page.fill("#release_date", "2026-06-01")
|
||||
page.fill("#release_date", "01/06/2026")
|
||||
page.check("input[value='game']")
|
||||
page.locator("#platforms-input").fill("PC")
|
||||
page.locator("#platforms-input").press("Enter")
|
||||
@@ -82,7 +82,7 @@ def test_project_detail_share_button(alice):
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", "Share Project")
|
||||
page.fill("#description", "A project created for the share button test")
|
||||
page.fill("#release_date", "2026-06-01")
|
||||
page.fill("#release_date", "01/06/2026")
|
||||
page.check("input[value='game']")
|
||||
page.locator("#platforms-input").fill("PC")
|
||||
page.locator("#platforms-input").press("Enter")
|
||||
@@ -137,7 +137,7 @@ def test_create_project_full(alice):
|
||||
page.locator("#create-project-btn").click()
|
||||
page.fill("#title", "Playwright Test Game")
|
||||
page.fill("#description", "A game created by Playwright integration tests")
|
||||
page.fill("#release_date", "2026-06-01")
|
||||
page.fill("#release_date", "01/06/2026")
|
||||
page.check("input[value='game']")
|
||||
page.locator("#platforms-input").fill("PC")
|
||||
page.locator("#platforms-input").press("Enter")
|
||||
|
||||
Reference in New Issue
Block a user