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.
This commit is contained in:
2026-07-27 11:17:48 +02:00
parent b185574760
commit 571a0485c5
33 changed files with 1290 additions and 92 deletions
+78
View File
@@ -0,0 +1,78 @@
# 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
)
+162
View File
@@ -0,0 +1,162 @@
# 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,
member_key,
)
from tests.conftest import BASE_URL
RESETS_URL = f"{BASE_URL}/admin/gateway/quota-resets"
_counter = [0]
def _owner():
_counter[0] += 1
return f"gwquota{_counter[0]}-{generate_uid()}"
def _burn(owner_id, app_reference, cost):
refresh_snapshot()
get_table(GATEWAY_LEDGER).insert(
{
"uid": generate_uid(),
"owner_kind": "user",
"owner_id": owner_id,
"app_reference": app_reference,
"cost_usd": cost,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
refresh_snapshot()
def _spent(owner_id, app_reference):
from devplacepy.services.openai_gateway import quota
refresh_snapshot()
quota._QUOTA_CACHE.clear()
return quota.spent_24h("user", owner_id, app_reference)
def test_quota_reset_requires_admin(seeded_db):
assert (
requests.post(RESETS_URL, headers=JSON_gateway, allow_redirects=False).status_code
== 401
)
key = member_key()
assert (
requests.post(
RESETS_URL,
headers={**JSON_gateway, "X-API-KEY": key},
json={},
allow_redirects=False,
).status_code
== 403
)
def test_admin_can_reset_a_scoped_spend(seeded_db):
owner = _owner()
_burn(owner, "appa", 2.5)
assert _spent(owner, "appa") == 2.5
response = admin_session(seeded_db).post(
RESETS_URL,
json={"owner_kind": "user", "owner_id": owner, "app_reference": "appa"},
)
assert response.status_code == 200
assert response.json()["ok"] is True
assert _spent(owner, "appa") == 0.0
def test_the_reset_response_echoes_the_scope(seeded_db):
owner = _owner()
_burn(owner, "appa", 1.0)
payload = admin_session(seeded_db).post(
RESETS_URL,
json={"owner_kind": "user", "owner_id": owner, "app_reference": "appa"},
).json()["reset"]
assert payload["owner_kind"] == "user"
assert payload["owner_id"] == owner
assert payload["app_reference"] == "appa"
assert payload["reset_at"]
def test_a_reset_leaves_another_caller_capped(seeded_db):
first, second = _owner(), _owner()
_burn(first, "appa", 2.0)
_burn(second, "appa", 2.0)
admin_session(seeded_db).post(
RESETS_URL,
json={"owner_kind": "user", "owner_id": first, "app_reference": "appa"},
)
assert _spent(first, "appa") == 0.0
assert _spent(second, "appa") == 2.0
def test_a_reset_keeps_the_usage_history(seeded_db):
owner = _owner()
_burn(owner, "appa", 2.0)
before = get_table(GATEWAY_LEDGER).count(owner_id=owner)
admin_session(seeded_db).post(
RESETS_URL,
json={"owner_kind": "user", "owner_id": owner, "app_reference": "appa"},
)
refresh_snapshot()
assert get_table(GATEWAY_LEDGER).count(owner_id=owner) == before
def test_an_unscoped_reset_clears_every_caller(seeded_db):
owner = _owner()
_burn(owner, "appa", 2.0)
assert admin_session(seeded_db).post(RESETS_URL, json={}).status_code == 200
assert _spent(owner, "appa") == 0.0
def test_an_unknown_owner_kind_is_rejected(seeded_db):
response = admin_session(seeded_db).post(RESETS_URL, json={"owner_kind": "wizard"})
assert response.status_code == 400
assert response.json()["ok"] is False
def test_a_malformed_app_reference_is_rejected(seeded_db):
response = admin_session(seeded_db).post(
RESETS_URL, json={"app_reference": "not a valid app!"}
)
assert response.status_code == 400
def test_the_reset_is_audited(seeded_db):
owner = _owner()
_burn(owner, "appa", 1.0)
admin = admin_session(seeded_db)
admin.post(
RESETS_URL,
json={"owner_kind": "user", "owner_id": owner, "app_reference": "appa"},
)
refresh_snapshot()
entries = admin.get(
f"{BASE_URL}/admin/audit-log",
headers=JSON_gateway,
params={"event_key": "gateway.quota.reset"},
).json()["entries"]
assert any(entry.get("target_type") == "gateway_quota" for entry in entries)
def test_spend_recorded_after_a_reset_counts_again(seeded_db):
owner = _owner()
_burn(owner, "appa", 2.0)
admin_session(seeded_db).post(
RESETS_URL,
json={"owner_kind": "user", "owner_id": owner, "app_reference": "appa"},
)
_burn(owner, "appa", 0.5)
assert _spent(owner, "appa") == 0.5
+1 -2
View File
@@ -234,7 +234,6 @@ def test_activity_comment_url_matches_notification_target(app_server):
def test_activity_post_exposes_url(app_server):
from datetime import datetime, timezone
from devplacepy.database import get_table, refresh_snapshot
owner = _seed_owner()
@@ -417,7 +416,7 @@ def test_own_profile_json_exposes_xp_fields(app_server, seeded_db):
session.post(
f"{BASE_URL}/auth/login",
data={
"username": seeded_db["alice"]["username"],
"email": seeded_db["alice"]["email"],
"password": seeded_db["alice"]["password"],
},
allow_redirects=True,
+1 -1
View File
@@ -101,7 +101,7 @@ def _create_post_direct(project_uid, user_uid, order, marker=None):
"project_uid": project_uid,
"image": None,
"stars": 0,
"created_at": (datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(seconds=order)).isoformat(),
"created_at": (datetime(2026, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=order)).isoformat(),
}
)
refresh_snapshot()
+4 -2
View File
@@ -60,7 +60,7 @@ def _seed_project_post(project_uid, user_uid, order, title=None):
"project_uid": project_uid,
"image": None,
"stars": 0,
"created_at": (datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(seconds=order)).isoformat(),
"created_at": (datetime(2026, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=order)).isoformat(),
}
)
return marker, uid
@@ -129,7 +129,9 @@ def test_devlog_shows_vote_and_comment_buttons(alice):
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
post_card = page.locator(".project-devlog .post-card").first
expect(post_card.locator(".post-action-btn")).to_be_visible()
expect(post_card.locator(".post-action-btn.vote-up")).to_be_visible()
expect(post_card.locator(".post-action-btn.vote-down")).to_be_visible()
expect(post_card.locator(".post-vote-count")).to_be_visible()
expect(post_card.locator("form[action*='/posts/delete/']")).to_be_visible()
+4 -4
View File
@@ -9,7 +9,7 @@ def test_primary_admin_ignores_accounts_without_a_signup_time(local_db):
"uid": "pa-real",
"username": "pa-real",
"role": "Admin",
"created_at": "2020-01-01T00:00:00",
"created_at": "1990-01-01T00:00:00",
"deleted_at": None,
}
)
@@ -45,9 +45,9 @@ def test_primary_admin_skips_deactivated_and_deleted_founders(local_db):
users = local_db["users"]
seeded = [
("pa-deleted", "2001-01-01T00:00:00", True, "2020-01-01T00:00:00"),
("pa-inactive", "2002-01-01T00:00:00", False, None),
("pa-usable", "2003-01-01T00:00:00", True, None),
("pa-deleted", "1991-01-01T00:00:00", True, "2020-01-01T00:00:00"),
("pa-inactive", "1992-01-01T00:00:00", False, None),
("pa-usable", "1993-01-01T00:00:00", True, None),
]
for uid, created_at, active, deleted_at in seeded:
users.insert(
+164
View File
@@ -0,0 +1,164 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timedelta, timezone
import pytest
from pydantic import ValidationError
from devplacepy.database import get_table
from devplacepy.services.openai_gateway import quota as q
from devplacepy.services.openai_gateway.usage import GATEWAY_LEDGER
from devplacepy.utils import generate_uid
_counter = [0]
def _owner():
_counter[0] += 1
return f"quotauser{_counter[0]}-{generate_uid()}"
def _burn(owner_id, app_reference, cost, minutes_ago=0):
stamp = datetime.now(timezone.utc) - timedelta(minutes=minutes_ago)
get_table(GATEWAY_LEDGER).insert(
{
"uid": generate_uid(),
"owner_kind": "user",
"owner_id": owner_id,
"app_reference": app_reference,
"cost_usd": cost,
"created_at": stamp.isoformat(),
}
)
def test_reset_requires_no_dimension(local_db):
assert q.QuotaResetIn().owner_kind is None
assert q.QuotaResetIn().owner_id is None
assert q.QuotaResetIn().app_reference is None
def test_reset_rejects_an_unknown_owner_kind(local_db):
with pytest.raises(ValidationError):
q.QuotaResetIn(owner_kind="wizard")
def test_reset_rejects_a_malformed_app_reference(local_db):
with pytest.raises(ValidationError):
q.QuotaResetIn(app_reference="not a valid app!")
def test_reset_normalizes_blanks_to_wildcards(local_db):
payload = q.QuotaResetIn(owner_kind="", owner_id=" ", app_reference="")
assert payload.owner_kind is None
assert payload.owner_id is None
assert payload.app_reference is None
def test_spend_counts_before_any_reset(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
assert q.spent_24h("user", owner, "appa") == 1.5
def test_a_scoped_reset_clears_that_scope(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=owner, app_reference="appa"))
assert q.spent_24h("user", owner, "appa") == 0.0
def test_a_scoped_reset_leaves_another_caller_alone(local_db):
first, second = _owner(), _owner()
_burn(first, "appa", 1.5)
_burn(second, "appa", 2.0)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=first, app_reference="appa"))
assert q.spent_24h("user", first, "appa") == 0.0
assert q.spent_24h("user", second, "appa") == 2.0
def test_a_scoped_reset_leaves_another_app_alone(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
_burn(owner, "appb", 2.0)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=owner, app_reference="appa"))
assert q.spent_24h("user", owner, "appa") == 0.0
assert q.spent_24h("user", owner, "appb") == 2.0
def test_a_global_reset_clears_every_scope(local_db):
first, second = _owner(), _owner()
_burn(first, "appa", 1.5)
_burn(second, "appb", 2.0)
q.reset(q.QuotaResetIn())
assert q.spent_24h("user", first, "appa") == 0.0
assert q.spent_24h("user", second, "appb") == 0.0
def test_spend_after_a_reset_counts_again(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=owner, app_reference="appa"))
_burn(owner, "appa", 0.75)
assert q.spent_24h("user", owner, "appa") == 0.75
def test_a_reset_keeps_the_usage_history(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
before = get_table(GATEWAY_LEDGER).count(owner_id=owner)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=owner, app_reference="appa"))
assert get_table(GATEWAY_LEDGER).count(owner_id=owner) == before
def test_a_narrower_reset_does_not_clear_a_broader_scope(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=owner, app_reference="appa"))
assert q.spent_24h("user", owner, None) == 1.5
def test_a_broader_reset_clears_a_narrower_scope(local_db):
owner = _owner()
_burn(owner, "appa", 1.5)
q.reset(q.QuotaResetIn(owner_kind="user", owner_id=owner))
assert q.spent_24h("user", owner, "appa") == 0.0
def test_resetting_the_same_scope_twice_reuses_one_row(local_db):
owner = _owner()
scope = q.QuotaResetIn(owner_kind="user", owner_id=owner, app_reference="appa")
first = q.reset(scope)
second = q.reset(scope)
assert first["uid"] == second["uid"]
assert second["reset_at"] >= first["reset_at"]
def test_a_rule_scope_is_unblocked_by_a_reset(local_db):
owner = _owner()
q.quota_rule_store.set(
q.QuotaRuleIn(
owner_kind="user", owner_id=owner, app_reference="appa", limit_usd=1.0
),
created_by="test",
)
_burn(owner, "appa", 1.5)
limit, scope, rule = q.resolve("user", owner, "appa", {})
assert q.spent_24h(*scope) >= limit
q.reset(
q.QuotaResetIn(
owner_kind=scope[0], owner_id=scope[1], app_reference=scope[2]
)
)
assert q.spent_24h(*scope) < limit
q.quota_rule_store.remove(rule.uid)
def test_scope_label_reads_like_the_rule_label(local_db):
scope = {"owner_kind": "user", "owner_id": "u1", "app_reference": "appa"}
assert q.scope_label(scope) == "role=user, user=u1, app=appa"
def test_scope_label_falls_back_for_a_wildcard_scope(local_db):
empty = {"owner_kind": None, "owner_id": None, "app_reference": None}
assert q.scope_label(empty, fallback="every caller") == "every caller"