fix(api): a token hiccup is not "reconnect"; reorders and approvals are one unit each
Deep review of the debt burn: - `refusal_reason` told the user to reconnect on ANY token-endpoint failure — a 5xx, a 429, a misconfigured client — while the grant stayed and the account still read as connected, with no connect button. Only `invalid_grant` (the one that drops the grant) means reconnect now. - `pins.reorder` writes only the rows that change, so two overlapping reorders each skipped rows that matched in their own stale snapshot and interleaved into two pins on one slot. It locks the user's pins first; the second waits and deals from the first's result. - Adding a whitelist entry committed the invite and the activation it implies separately, and its audit row was written before a new invite had an id (target NULL). One transaction, flushed for the id. - The mutation convention test skipped async routes and dicts returned by name: request-access, purge-discovery and the subscription sync answered without `ok`. They do now, and the test covers both. The helper-commit check looked only at `_`-names; it reads every non-route function, which listed two deliberate committers (upsert_pending_invite, ensure_welcome) — now allowlisted with their reasons.
This commit is contained in:
@@ -1069,3 +1069,49 @@ def test_a_hero_from_a_show_pin_names_the_pin_it_came_from(db, as_user):
|
||||
"id": "ps1",
|
||||
"title": "A Show",
|
||||
}
|
||||
|
||||
|
||||
def test_two_overlapping_reorders_never_share_a_slot(db, as_user):
|
||||
"""`reorder` writes only the rows that change. Unlocked, two tabs reordering from the same
|
||||
snapshot each skipped the rows that already matched in THEIR snapshot and interleaved row by
|
||||
row: p1=1, p2=2, p3=1 — two pins on one slot (deep review, debt burn). Locked, the second waits
|
||||
for the first and deals from its result."""
|
||||
import threading
|
||||
|
||||
from app.db import SessionLocal
|
||||
|
||||
user, ch = _seed_yt(db)
|
||||
c = as_user(user)
|
||||
p1 = c.post("/api/discover/pins", json={"kind": "video", "ref": "vmed"}).json()["id"]
|
||||
p2 = c.post("/api/discover/pins", json={"kind": "channel", "ref": ch.id}).json()["id"]
|
||||
p3 = c.post("/api/discover/pins", json={"kind": "video", "ref": "vlong"}).json()["id"]
|
||||
uid = user.id
|
||||
|
||||
with SessionLocal() as tab_a, SessionLocal() as tab_b, SessionLocal() as probe:
|
||||
pins.reorder(tab_a, tab_a.get(User, uid), [p2, p1, p3]) # written, not yet committed
|
||||
errors: list[BaseException] = []
|
||||
|
||||
def second_tab():
|
||||
try:
|
||||
pins.reorder(tab_b, tab_b.get(User, uid), [p1, p3, p2])
|
||||
tab_b.commit()
|
||||
except BaseException as exc: # noqa: BLE001 — re-raised in the test thread below
|
||||
errors.append(exc)
|
||||
|
||||
worker = threading.Thread(target=second_tab)
|
||||
worker.start()
|
||||
# Commit A only once B is blocked on a row lock, so B's read is the stale snapshot.
|
||||
for _ in range(100):
|
||||
waiting = probe.scalar(text("SELECT count(*) FROM pg_locks WHERE NOT granted"))
|
||||
probe.rollback()
|
||||
if waiting:
|
||||
break
|
||||
worker.join(0.05)
|
||||
tab_a.commit()
|
||||
worker.join(10)
|
||||
assert not worker.is_alive() and errors == []
|
||||
|
||||
db.expire_all()
|
||||
positions = db.scalars(select(ComfortPin.position).where(ComfortPin.user_id == uid)).all()
|
||||
assert sorted(positions) == [0, 1, 2]
|
||||
assert [p["pin_id"] for p in c.get("/api/discover/pins").json()["items"]] == [p1, p3, p2]
|
||||
|
||||
Reference in New Issue
Block a user