Files
devplacepy/tests/api/admin/aiquota/resetall.py
T
retoor 571a0485c5 Fix circular import, primary-admin NULL trap, and add gateway quota reset
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.

Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.

Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.

Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.

Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.

Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.

Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.

Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.

Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.

Full suite: 2989 passed, 1 skipped.
2026-07-27 11:17:48 +02:00

79 lines
2.1 KiB
Python

# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone
import requests
from devplacepy.database import get_table, refresh_snapshot
from devplacepy.services.openai_gateway.usage import GATEWAY_LEDGER
from devplacepy.utils import generate_uid
from tests.api.admin.gateway.index import JSON_gateway, admin_session
from tests.conftest import BASE_URL
RESET_ALL_URL = f"{BASE_URL}/admin/ai-quota/reset-all"
_counter = [0]
def _owner():
_counter[0] += 1
return f"resetall{_counter[0]}-{generate_uid()}"
def _burn(owner_id, cost):
refresh_snapshot()
get_table(GATEWAY_LEDGER).insert(
{
"uid": generate_uid(),
"owner_kind": "user",
"owner_id": owner_id,
"app_reference": "typosaurus",
"cost_usd": cost,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
refresh_snapshot()
def _spent(owner_id):
from devplacepy.services.openai_gateway import quota
refresh_snapshot()
quota._QUOTA_CACHE.clear()
return quota.spent_24h("user", owner_id, "typosaurus")
def test_reset_all_clears_the_gateway_spend(seeded_db):
owner = _owner()
_burn(owner, 3.0)
assert _spent(owner) == 3.0
response = admin_session(seeded_db).post(RESET_ALL_URL, allow_redirects=False)
assert response.status_code == 200
assert _spent(owner) == 0.0
def test_reset_all_keeps_the_gateway_usage_history(seeded_db):
owner = _owner()
_burn(owner, 3.0)
before = get_table(GATEWAY_LEDGER).count(owner_id=owner)
admin_session(seeded_db).post(RESET_ALL_URL, allow_redirects=False)
refresh_snapshot()
assert get_table(GATEWAY_LEDGER).count(owner_id=owner) == before
def test_gateway_spend_after_reset_all_counts_again(seeded_db):
owner = _owner()
_burn(owner, 3.0)
admin_session(seeded_db).post(RESET_ALL_URL, allow_redirects=False)
_burn(owner, 0.25)
assert _spent(owner) == 0.25
def test_reset_all_requires_admin(seeded_db):
assert (
requests.post(
RESET_ALL_URL, headers=JSON_gateway, allow_redirects=False
).status_code
== 401
)