fix(activity): runs keep their true outcome and spend, the page reaches every kept row, a closing dialog leaves the server log alone
Backend - A run's outcome write sits outside the job's own try: a database blip after a run that succeeded no longer records it as failed (the row waits for the next start's sweep). - An interrupted run keeps what it spent (the sweep sums its quota rows, as finish does). - The Scheduler's ad-hoc row says running while any backfill runs, not only the newest. - recent_runs reads each job's last runs off its index instead of ranking the whole table. - An interval change is stored with its audit row in one transaction, then rescheduled. - One API instance per database is the stated contract (.env.example, jobruns). - Turning full history on for a channel already complete records no run. - Migration 0109 round-trips interrupted and ad-hoc runs; the run words' DB CHECKs are test-held to the enums; the account filter is one helper for the four activity reads. - An audit row says how many of its request's log lines are still kept. Frontend - An "All" period reaches every row still kept (runs and admin actions outlive 90 days). - Back/Forward is followed in place by the server log: a closing dialog no longer turns Live off; a pivot still opens a fresh log. - Runs moved over from the audit log read their one-liner in the reader's language. - Filter choices or accounts that failed to load say so beside the filters, with a retry. - An old server-log link keeps its account, text and range; a remembered retired page opens its tab. - A request whose lines are gone says so instead of linking to an empty list. - The log lines and the history rows share one row frame, and the server log one list frame with the other tabs; an empty list is announced; each day's quota is readable.
This commit is contained in:
@@ -7,7 +7,10 @@ with rows that share a timestamp — the case an offset or a timestamp-only curs
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.auth import admin_user
|
||||
from app.main import app
|
||||
from app.models import AuditLog, Channel, JobRun, LogEvent, QuotaEvent
|
||||
from tests.test_body_limit import _route_objects
|
||||
|
||||
T0 = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
@@ -88,6 +91,32 @@ def test_an_admin_action_keeps_the_request_it_was_made_in(db, make_user, as_user
|
||||
assert row["request_id"] == r.headers["x-request-id"]
|
||||
|
||||
|
||||
def test_an_admin_action_says_how_many_of_its_requests_lines_are_still_kept(db, make_user, as_user):
|
||||
"""A request's lines are kept days, its audit row months: a link to lines gone would open an
|
||||
empty list, so the row says how many are left, as a run does."""
|
||||
admin = make_user(role="admin")
|
||||
kept = _audit(db, "config_set", actor=admin.id, request_id="req-kept-0001")
|
||||
gone = _audit(db, "config_set", actor=admin.id, request_id="req-gone-0001")
|
||||
db.add_all(
|
||||
[
|
||||
LogEvent(
|
||||
ts=T0,
|
||||
level="INFO",
|
||||
sev=20,
|
||||
source="api",
|
||||
logger="t",
|
||||
msg="m",
|
||||
request_id="req-kept-0001",
|
||||
)
|
||||
for _ in range(3)
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
items = as_user(admin).get("/api/admin/audit").json()["items"]
|
||||
lines = {r["id"]: r["log_lines"] for r in items}
|
||||
assert (lines[kept], lines[gone]) == (3, 0)
|
||||
|
||||
|
||||
def test_the_run_list_filters_pages_and_names_the_channel_and_the_log_lines(db, make_user, as_user):
|
||||
admin = make_user(role="admin")
|
||||
db.add(Channel(id="UCchan", title="Aileen Wang"))
|
||||
@@ -146,6 +175,8 @@ def test_the_quota_breakdown_covers_its_range_and_one_account(db, make_user, as_
|
||||
|
||||
def totals(**params) -> dict:
|
||||
body = client.get("/api/quota/admin", params=params).json()
|
||||
# The chart is the same filter's: its days add up to the table's total.
|
||||
assert sum(d["total"] for d in body["daily"]) == sum(r["total"] for r in body["rows"])
|
||||
return {r["user_id"]: r["total"] for r in body["rows"]}
|
||||
|
||||
assert totals() == {admin.id: 150, None: 7} # the last 30 days by default
|
||||
@@ -153,3 +184,22 @@ def test_the_quota_breakdown_covers_its_range_and_one_account(db, make_user, as_
|
||||
assert totals(until=(now - timedelta(days=1)).isoformat()) == {admin.id: 50}
|
||||
assert totals(user="none") == {None: 7}
|
||||
assert totals(user=str(admin.id)) == {admin.id: 150}
|
||||
|
||||
|
||||
def test_every_admin_read_and_write_is_admin_only():
|
||||
"""The route table, not a list kept by hand: every /api/admin route and the quota breakdown
|
||||
resolve `admin_user`, so a dropped dependency cannot hand an account's runs, errors and e-mail
|
||||
addresses to any signed-in user."""
|
||||
routes = _route_objects(app)
|
||||
admin = {k: r for k, r in routes.items() if k[1].startswith(("/api/admin", "/api/quota/admin"))}
|
||||
assert ("GET", "/api/admin/runs") in admin and ("GET", "/api/quota/admin") in admin
|
||||
open_ = []
|
||||
for key, route in admin.items():
|
||||
stack, calls = [route.dependant], set()
|
||||
while stack:
|
||||
d = stack.pop()
|
||||
calls.add(d.call)
|
||||
stack.extend(d.dependencies)
|
||||
if admin_user not in calls:
|
||||
open_.append(key)
|
||||
assert open_ == []
|
||||
|
||||
Reference in New Issue
Block a user