Coverage-first safety net for the E3 S1 Plex god-module split. The movie/show watch-state predicates are currently hand-duplicated in unified_library (the grid) and facets.wsq (the facet aggregation); this pins the invariant the facets docstring promises — with no metadata filter active, a facet's genre / content-rating counts equal the same counts over the grid's visible items — for every (scope × show) combination, deriving the expected counts from the grid's own response. Locks the two branch-sets together before they merge into one shared helper, so the merge is provably behaviour-equivalent. Mutation-verified: dropping the facets hidden-exclusion branch makes both tests fail (Drama 3≠2, R 2≠1). Green on the current code (348 DB-lane pass).
149 lines
7.2 KiB
Python
149 lines
7.2 KiB
Python
"""E3 S1 characterization net — /library grid ⟷ /facets equivalence across watch-state scopes.
|
||
|
||
The Plex god-module split (E3 S1) extracts the movie/show watch-state predicates that are currently
|
||
HAND-DUPLICATED in two places: `unified_library` (the grid) and `facets.wsq` (the facet aggregation).
|
||
The facets docstring promises the facets "reflect exactly the visible result set" — i.e. with NO
|
||
metadata filter active (only the `show` state scope), a facet's genre/content-rating counts must equal
|
||
the same counts taken over the grid's visible items. This test derives the expected counts from the
|
||
grid's own response and asserts /facets matches, for every (scope × show) combination.
|
||
|
||
That locks the two duplicated branch-sets together BEFORE they are merged into one shared helper, so
|
||
the merge is provably behaviour-equivalent (and a drift between grid and facets fails here). DB lane
|
||
(`db` fixture) — skipped without Postgres.
|
||
"""
|
||
from collections import Counter
|
||
from datetime import datetime, timezone
|
||
|
||
from app.models import (
|
||
PlexItem,
|
||
PlexLibrary,
|
||
PlexSeason,
|
||
PlexShow,
|
||
PlexState,
|
||
User,
|
||
)
|
||
|
||
SCOPES = ("movie", "show", "both")
|
||
SHOW_STATES = ("all", "watched", "in_progress", "unwatched")
|
||
|
||
|
||
def _dt(day: int) -> datetime:
|
||
return datetime(2024, 1, day, tzinfo=timezone.utc)
|
||
|
||
|
||
def _seed(db):
|
||
"""A corpus spanning every watch state, each row carrying genres + a content rating so the facet
|
||
aggregation has something to count. Movies get per-item state directly; a show's state is the
|
||
rollup of its episodes' state (new / in-progress / fully watched)."""
|
||
movie_lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
|
||
show_lib = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
|
||
db.add_all([movie_lib, show_lib])
|
||
db.flush()
|
||
|
||
# Movies — new / in-progress / watched / hidden, with overlapping genres.
|
||
m_new = PlexItem(rating_key="m_new", library_id=movie_lib.id, kind="movie", title="New Movie",
|
||
year=2001, added_at=_dt(1), genres=["Drama"], content_rating="PG")
|
||
m_prog = PlexItem(rating_key="m_prog", library_id=movie_lib.id, kind="movie", title="Prog Movie",
|
||
year=2002, added_at=_dt(2), genres=["Drama", "Comedy"], content_rating="PG-13")
|
||
m_watch = PlexItem(rating_key="m_watch", library_id=movie_lib.id, kind="movie", title="Watched Movie",
|
||
year=2003, added_at=_dt(3), genres=["Comedy"], content_rating="R")
|
||
m_hidden = PlexItem(rating_key="m_hidden", library_id=movie_lib.id, kind="movie", title="Hidden Movie",
|
||
year=2004, added_at=_dt(4), genres=["Drama"], content_rating="R")
|
||
|
||
# Shows — new / in-progress / watched (state comes from the episodes below).
|
||
sh_new = PlexShow(rating_key="sh_new", library_id=show_lib.id, title="New Show", year=2005,
|
||
added_at=_dt(5), child_count=1, genres=["Sci-Fi"], content_rating="TV-14")
|
||
sh_prog = PlexShow(rating_key="sh_prog", library_id=show_lib.id, title="Prog Show", year=2006,
|
||
added_at=_dt(6), child_count=1, genres=["Sci-Fi", "Drama"], content_rating="TV-MA")
|
||
sh_watch = PlexShow(rating_key="sh_watch", library_id=show_lib.id, title="Watched Show", year=2007,
|
||
added_at=_dt(7), child_count=1, genres=["Drama"], content_rating="TV-PG")
|
||
db.add_all([m_new, m_prog, m_watch, m_hidden, sh_new, sh_prog, sh_watch])
|
||
db.flush()
|
||
|
||
def _episode(rk, show, day, n):
|
||
se = PlexSeason(rating_key=f"se_{rk}", show_id=show.id, season_number=1)
|
||
db.add(se)
|
||
db.flush()
|
||
ep = PlexItem(rating_key=rk, library_id=show_lib.id, kind="episode", title=f"Ep {rk}",
|
||
show_id=show.id, season_id=se.id, season_number=1, episode_number=n, added_at=_dt(day))
|
||
db.add(ep)
|
||
db.flush()
|
||
return ep
|
||
|
||
_episode("e_new", sh_new, 8, 1) # no state -> show "new"
|
||
e_prog = _episode("e_prog", sh_prog, 9, 1) # in-progress episode -> show "in_progress"
|
||
_episode("e_prog2", sh_prog, 10, 2) # second episode, untouched (so watched < total)
|
||
e_watch = _episode("e_watch", sh_watch, 11, 1) # watched episode -> show "watched"
|
||
|
||
user = User(email="plexparity@example.com")
|
||
db.add(user)
|
||
db.flush()
|
||
db.add_all([
|
||
PlexState(user_id=user.id, item_id=m_prog.id, position_seconds=120),
|
||
PlexState(user_id=user.id, item_id=m_watch.id, status="watched"),
|
||
PlexState(user_id=user.id, item_id=m_hidden.id, status="hidden"),
|
||
PlexState(user_id=user.id, item_id=e_prog.id, position_seconds=90),
|
||
PlexState(user_id=user.id, item_id=e_watch.id, status="watched"),
|
||
])
|
||
db.commit()
|
||
# rating_key -> (table, genres, content_rating) so the test can total the grid's visible set.
|
||
meta = {}
|
||
for it in (m_new, m_prog, m_watch, m_hidden):
|
||
meta[it.rating_key] = (it.genres, it.content_rating)
|
||
for sh in (sh_new, sh_prog, sh_watch):
|
||
meta[sh.rating_key] = (sh.genres, sh.content_rating)
|
||
return {"user": user, "meta": meta}
|
||
|
||
|
||
def _facet_counts(body):
|
||
genres = {g["value"]: g["count"] for g in body["genres"]}
|
||
crs = {c["value"]: c["count"] for c in body["content_ratings"]}
|
||
return genres, crs
|
||
|
||
|
||
def test_facets_mirror_the_grid_visible_set(db, as_user):
|
||
"""For every (scope, show) the facet genre / content-rating counts equal the counts over the
|
||
grid's visible items — the invariant the extracted shared watch-state predicate must preserve."""
|
||
seed = _seed(db)
|
||
meta = seed["meta"]
|
||
client = as_user(seed["user"])
|
||
|
||
for scope in SCOPES:
|
||
for show in SHOW_STATES:
|
||
grid = client.get(f"/api/plex/library?scope={scope}&show={show}&limit=100")
|
||
assert grid.status_code == 200, grid.text
|
||
ids = [i["id"] for i in grid.json()["items"]]
|
||
|
||
exp_genres: Counter = Counter()
|
||
exp_crs: Counter = Counter()
|
||
for rk in ids:
|
||
genres, cr = meta[rk]
|
||
for g in genres or []:
|
||
exp_genres[g] += 1
|
||
if cr is not None:
|
||
exp_crs[cr] += 1
|
||
|
||
facets = client.get(f"/api/plex/facets?scope={scope}&show={show}")
|
||
assert facets.status_code == 200, facets.text
|
||
got_genres, got_crs = _facet_counts(facets.json())
|
||
|
||
ctx = f"scope={scope} show={show} grid_ids={ids}"
|
||
assert got_genres == dict(exp_genres), f"genre facet != grid ({ctx})"
|
||
assert got_crs == dict(exp_crs), f"content-rating facet != grid ({ctx})"
|
||
|
||
|
||
def test_hidden_movie_absent_from_grid_and_facets(db, as_user):
|
||
"""A hidden movie must not appear in the default grid NOR inflate its facets — the single most
|
||
important equivalence between the two duplicated 'exclude hidden' branches."""
|
||
seed = _seed(db)
|
||
client = as_user(seed["user"])
|
||
|
||
grid = client.get("/api/plex/library?scope=movie&show=all&limit=100")
|
||
assert "m_hidden" not in [i["id"] for i in grid.json()["items"]]
|
||
|
||
# Only the hidden movie carries content_rating "R" among the *visible* movies? No — m_watch is
|
||
# also "R". The hidden one must not add to the "R" count: visible R = {m_watch} = 1.
|
||
facets = client.get("/api/plex/facets?scope=movie&show=all")
|
||
crs = {c["value"]: c["count"] for c in facets.json()["content_ratings"]}
|
||
assert crs.get("R") == 1, crs
|