fix(e5): deep-review round — combobox Enter, parse_person collision, enrich hardening + coverage

Full-epic /deep-review (11 finders + 12 adversarial verifiers) surfaced no HIGH; every
mechanism finding calibrated to LOW on verification. Fix all 13:

Correctness / reliability:
- Combobox: Enter now commits the VISUALLY highlighted row (highlightIndex ?? activeIndex),
  not the bare keyboard activeIndex — so arrow-then-hover-then-Enter no longer toggles a
  different option than the one painted, and hover-with-no-arrow is no longer a silent no-op.
- parse_person: dedup the known_for pool by (id, media_type), not the bare id — TMDB movie
  and tv id spaces are independent, so a same-id movie+show pair is two distinct credits
  (matches the collision-safe key the rest of the module already uses).
- person-enrich: a per-person in-flight + 60s cooldown guard (people._should_schedule_enrich)
  dedups concurrent opens and rate-limits re-triggering while TMDB is degraded (a transient
  TmdbError leaves the watermark NULL), so bursts can't pile 8s tasks onto the shared threadpool.
- PlexInfo compact()/money(): format with i18n.language (keeping currency USD) instead of a
  hardcoded en/en-US, matching the app's Intl convention.
- delete the dead `plex.filter.add` i18n key (both locales) — the +Add button that used it is gone.

Reuse:
- _continue_watching reuses rollup.agg_subquery(include_resumed=True) + query.apply_show_state_filter
  instead of a third hand-inlined copy of the per-show rollup + in_progress predicate, so the
  Discover rail can't drift from the grid's definition.

Test coverage (the gaps a regression would ship through):
- resolve_library_guids scope-exclusion (a credit in a non-scoped/disabled library is dropped)
  + the []-vs-None distinction; parse_person cross-media-id retention; enrich_person transient
  (NULL watermark) and 404 (stamp) branches; _enrich_person_bg body (fresh session close/rollback,
  None-guard) + the cooldown dedup; continue_watching fully-watched-show exclusion; Combobox
  Enter-commits-highlighted + async/controlled-query mode; PopoverItem aria-checked contract;
  RatingsRow missing-IMDb + imdb==audience branches.

Backend lane 413→420, FE vitest 351→357; all gates green. No migration.
This commit is contained in:
2026-08-21 08:11:18 +02:00
parent 8f6714e678
commit bad2aae753
14 changed files with 407 additions and 63 deletions
+30
View File
@@ -293,6 +293,36 @@ def test_enrich_batch_skips_shows_without_guid(db):
assert sh.tmdb_enriched_at is None
def test_resolve_library_guids_scopes_to_given_libraries(db):
# The scoped path (person page) must keep a credit that lives in a NON-scoped library (disabled or an
# HU-overlay twin) OUT of the result; the unscoped path (detail recs) matches across all libraries;
# and an explicit empty list skips that media side entirely (distinct from None = match all).
lib_on = _lib(db, key="on", kind="movie")
lib_off = _lib(db, key="off", kind="movie")
m_on = PlexItem(rating_key="on1", library_id=lib_on.id, kind="movie", title="In Scope", guid="tmdb:100")
m_off = PlexItem(rating_key="off1", library_id=lib_off.id, kind="movie", title="Out", guid="tmdb:200")
db.add_all([m_on, m_off])
db.commit()
entries = [{"tmdb_id": 100, "media_type": "movie"}, {"tmdb_id": 200, "media_type": "movie"}]
# Scoped to lib_on: the row in lib_off is excluded.
movies, shows = tmdb.resolve_library_guids(
db, entries, movie_lib_ids=[lib_on.id], show_lib_ids=[]
)
assert set(movies) == {"tmdb:100"}
assert shows == {}
# Unscoped (None defaults): matches across all libraries.
movies_all, _ = tmdb.resolve_library_guids(db, entries)
assert set(movies_all) == {"tmdb:100", "tmdb:200"}
# Empty list for a media side skips its bucket even with candidates present (vs None = match all).
_, shows_empty = tmdb.resolve_library_guids(
db, [{"tmdb_id": 100, "media_type": "tv"}], show_lib_ids=[]
)
assert shows_empty == {}
def test_resolve_recs_matches_by_media_type_not_bare_id(db):
# TMDB movie ids and tv ids are independent, so tmdb:500 can be BOTH a show (in our library) and a
# movie recommendation. The match must respect media_type — a bare-id match would deep-link the movie