fix(e3): address /code-review high findings (5)
Round-1 review fixes on the E3 branch:
- prune partial-fetch (correctness): `_paginate` now reports whether it reached
the server's reported `total`; `_prune_stale` prunes only on a COMPLETE
non-empty listing. A truncated fetch (a mid-sequence empty/short page before
total — a transient Plex hiccup) no longer deletes the rows it merely failed
to list. New test: a listing that claims 5 but returns 1 prunes nothing.
- `_prune_stale` (robustness): `stats.get("pruned", 0) + 1` — no KeyError when a
caller's stats dict omits the key.
- service transitions (conventions): pause/resume/redownload use the JobStatus
StrEnum instead of loose "queued"/"paused"/"error" literals, matching cancel.
- ffmpeg progress writer (correctness): a stuck-percent heartbeat now runs on its
OWN 1/interval budget, so it can't consume the progress-write slot and delay a
real percent change by up to an interval.
- backend/app/README.md (conventions): reword the transaction-ownership rule to
quote and extend CLAUDE.md's canonical ("handler/job-wrapper commits; helpers
flush; only quota owns its session") instead of stating a contradicting
"helpers MAY commit" carve-out.
ruff clean, full DB-lane 364 pass.
This commit is contained in:
@@ -149,7 +149,9 @@ class TestRollup:
|
||||
|
||||
|
||||
def _fake_paginate(by_type):
|
||||
def _p(_plex, _key, typ):
|
||||
def _p(_plex, _key, typ, progress=None):
|
||||
if progress is not None:
|
||||
progress["complete"] = True # the fake returns the whole list — a complete listing
|
||||
return list(by_type.get(typ, []))
|
||||
return _p
|
||||
|
||||
|
||||
@@ -37,6 +37,21 @@ class _StubPlex:
|
||||
return items[start : start + size], len(items)
|
||||
|
||||
|
||||
class _TruncatedPlex:
|
||||
"""Simulates a truncated listing (a transient Plex hiccup): it reports a large `total` but returns
|
||||
only one short page, so pagination stops BEFORE reaching total. Prune must treat this as an
|
||||
incomplete fetch and delete nothing — the un-listed rows are present, just not mentioned."""
|
||||
|
||||
def __init__(self, items: list[dict], claimed_total: int):
|
||||
self._items = items
|
||||
self._total = claimed_total
|
||||
|
||||
def section_items(self, key, item_type, start, size):
|
||||
if start == 0:
|
||||
return list(self._items), self._total # one short page, total says there's more
|
||||
return [], self._total
|
||||
|
||||
|
||||
def _seed_movies(db):
|
||||
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
|
||||
db.add(lib)
|
||||
@@ -88,6 +103,22 @@ def test_empty_listing_prunes_nothing(db):
|
||||
assert stats["pruned"] == 0
|
||||
|
||||
|
||||
def test_truncated_listing_prunes_nothing(db):
|
||||
"""A partial fetch (Plex reports 5 items but the listing returns only 1 then stops) must NOT prune
|
||||
the 4 it failed to mention — the completeness guard, not just the empty-listing guard."""
|
||||
lib, m1_id, _m2_id, _user_id = _seed_movies(db)
|
||||
# Only m1 is listed, but total claims 5 → the walk never reaches total → incomplete → no prune.
|
||||
stub = _TruncatedPlex([{"ratingKey": "m1", "title": "Kept Movie"}], claimed_total=5)
|
||||
stats = {"movies": 0, "pruned": 0}
|
||||
|
||||
_sync_movies(db, stub, lib, stats)
|
||||
db.commit()
|
||||
|
||||
live = {r.rating_key for r in db.execute(select(PlexItem)).scalars()}
|
||||
assert live == {"m1", "m2"}, "a truncated listing must not prune the un-listed rows"
|
||||
assert stats["pruned"] == 0
|
||||
|
||||
|
||||
def test_show_prune_removes_stale_show_season_and_episode(db):
|
||||
lib = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
|
||||
db.add(lib)
|
||||
|
||||
Reference in New Issue
Block a user