fix: every first watch state survives a write beside it, and a folded Watch later keeps its pin

A season marked watched, a pull from Plex and a YouTube resume point made their first row by a read
then an add, and failed on the unique key when a first checkpoint or the watched mark made it in
between. They go through the same insert-then-read as the checkpoint (the resume point is one
upsert); a pull that meets a local change still waiting for its push leaves it.

Folding duplicate Watch later lists moved their videos but not a Home pin, which the cascade then
deleted; the first pin of a dropped copy moves onto the kept list.

Marking one notice read trimmed before its own flag was flushed, so the trim left one past the cap.
The route test now crosses the cap and reads the result from a session of its own.
This commit is contained in:
2026-10-20 01:56:30 +02:00
parent b04b2cbd62
commit c5f121139b
10 changed files with 199 additions and 68 deletions
+28
View File
@@ -14,6 +14,7 @@ from types import SimpleNamespace
from sqlalchemy import select
from app.models import User
from app.plex import queries
from app.plex.models import PlexItem, PlexLibrary, PlexState
from app.plex.watch_sync import _pull_apply, _reconcile_inprogress_movies
from app.routes.plex.playback import (
@@ -385,3 +386,30 @@ def test_should_push_resume_periodic_is_throttled():
)
# Once the window elapses, the next tick pushes again.
assert _should_push_resume(*key, now + timedelta(seconds=_PUSH_THROTTLE_S), final=False) is True
def test_a_pull_that_lost_the_race_keeps_the_local_change_it_met(db, monkeypatch):
"""The pull read "no state", and a first local checkpoint made one before it wrote: that row is
still waiting for its push, so the pull leaves it (the dirty guard), and fails on nothing."""
item_id, user_id = _seed_item_and_user(db)
now = datetime.now(timezone.utc)
db.add(PlexState(user_id=user_id, item_id=item_id, position_seconds=300, synced_to_plex=False))
db.commit()
real = queries.state
stale = [True]
monkeypatch.setattr(
queries, "state", lambda d, u, i: None if stale and stale.pop() else real(d, u, i)
)
assert _pull_apply(db, user_id, item_id, "new", 120, plex_ts=now) is None
db.commit()
rows = db.scalars(select(PlexState).where(PlexState.item_id == item_id)).all()
assert [(r.position_seconds, r.synced_to_plex) for r in rows] == [(300, False)]
def test_a_first_pull_makes_a_row_in_step_with_plex(db):
item_id, user_id = _seed_item_and_user(db)
now = datetime.now(timezone.utc)
assert _pull_apply(db, user_id, item_id, "new", 120, plex_ts=now) == "in_progress"
db.commit()
rows = db.scalars(select(PlexState).where(PlexState.item_id == item_id)).all()
assert [(r.position_seconds, r.synced_to_plex) for r in rows] == [(120, True)]