test(e24): close the review's coverage gaps, and correct a comment that was untrue

`test_only_a_show_row_pays_for_a_resolution` seeded no watch state, so every rail query
inner-joined its way to zero rows and the loop body — with it the assertion — never ran. It
now seeds a video, a movie and an episode, and asserts all three kinds are present before
the loop, so it cannot go vacuous again.

The Plex half of the surprise rail had no test for either of its promises: every test that
reached `plex_surprise` passed budget="any" against a library with no state rows, so the
duration bounds and the already-started exclusion could both be deleted with the suite
green. The YouTube half has had tests for the same two rules since S1 — the asymmetric half
of one principle.

`plex_item` and `plex_show` — two of the five pinnable kinds — were pinned by no test at
all, leaving the rating_key → internal-id translation and both shelf arms uncovered,
including the self-reference cycle that once 500'd the shelf. And `GET /api/discover/pick`
was exercised only through its domain function, so the route's own query-string translation
could regress silently; the new test excludes all three candidates, which is what proves the
whole list parsed rather than one unsplit string.

`PIN_COLUMNS`/`PIN_KINDS` claimed in a comment to be the one place the pin vocabulary is
spoken and had no readers anywhere: `resolve_ref` re-spelled the column names and the route
re-typed the kind list. Both now drive off the constants.

The `_NO_TS` sentinel was introduced as a crash fix and is not one. The key it replaced,
`(ts is not None, ts)`, cannot raise — tuple comparison finds the first UNEQUAL element via
`==`, so two null keys compare equal and `None < None` is never reached; three independent
runs confirmed it over every permutation. The rationale now says what the sentinel actually
buys, lives once in `plex.rollup`, and the third rail that was still on the older spelling
has been swept to match. Its test is renamed and re-scoped to the mutation it does catch.

Frontend: the reroll's second press asserted `expect.any(Array)`, which cannot tell an
accumulating exclusion window from a replacing one — both that and SEEN_WINDOW = 1 (each
reinstating the measured nine-card cycle) left the suite green. And `onInfo`'s jump type was
untested while `onPlay`'s was pinned twice, so pointing the ⓘ affordance at the player
typechecked and shipped green.

All eight fixes mutation-checked; every mutant died on its intended test.
This commit is contained in:
2026-09-08 03:14:21 +02:00
parent fdc1eabbe0
commit cedebe0ffb
7 changed files with 219 additions and 46 deletions
+142 -7
View File
@@ -236,10 +236,14 @@ def test_the_resume_rail_obeys_the_channel_blocklist(db):
assert hero.pick(db, user, "any", 0) is None
def test_a_null_timestamp_sorts_last_instead_of_raising(db):
"""The `(ts is not None, ts)` key this rail's Plex sibling used compares `None < None` when two
rows both lack a timestamp, which raises. A missing progress timestamp must sort last, not 500
the landing page."""
def test_a_null_timestamp_sorts_last(db):
"""A missing progress timestamp sorts last rather than 500ing the landing page.
Narrower than it was written to be, and the epic-end review corrected it: the previous key,
`(ts is not None, ts)`, does NOT raise on two nulls — tuple comparison finds the first UNEQUAL
element via `==`, so `None < None` is never reached, and this test passes under that key too.
What it does discriminate is the naive `key=lambda r: r[0]`, which really does raise. See
`plex.rollup.NO_TS` for the corrected rationale."""
user, _ = _seed_yt(db)
db.add_all(
[
@@ -539,6 +543,45 @@ def test_a_pinned_video_survives_leaving_the_feed_scope(db):
assert card["next"]["id"] == "vmed"
def test_the_plex_surprise_obeys_the_time_budget(db):
"""The one control on the page has to mean what it says: a 100-minute film is not an answer to
"10 perc". The YouTube half of this rule has had its own test since S1; the Plex half's duration
bounds could be deleted with the whole suite still green, because every test that reached
`plex_surprise` passed budget="any"."""
user, _ = _seed_yt(db)
_seed_plex(db, user)
_enable_plex(db)
drawn = lambda budget: { # noqa: E731
(c["source"], c["id"]) for s in range(6) for c in rails.surprise(db, user, budget, s, 5)
}
assert ("plex", "pm1") not in drawn("short")
# …and it IS offered where it fits, so this cannot pass by the rail being empty.
assert ("plex", "pm1") in drawn("long")
def test_the_plex_surprise_never_offers_something_already_started(db):
"""A half-watched film is not a surprise, and the resume tier has already had its chance at it.
The mirror of `test_surprise_never_offers_something_already_started` for the other source — the
asymmetric half of one principle, which the house rules say to fix everywhere."""
user, _ = _seed_yt(db)
movie, _, _ = _seed_plex(db, user)
_enable_plex(db)
db.add(
PlexState(
user_id=user.id,
item_id=movie.id,
status="new",
position_seconds=600,
progress_updated_at=NOW,
)
)
db.commit()
drawn = {
(c["source"], c["id"]) for s in range(6) for c in rails.surprise(db, user, "long", s, 5)
}
assert ("plex", "pm1") not in drawn
def test_a_new_pin_lands_at_the_end_even_after_a_hole(db):
"""Positions are never renumbered on removal — `remove_pin` deletes the row, and every FK cascade
silently removes a pin when its target disappears — so the append position has to come from
@@ -603,11 +646,46 @@ def test_the_resume_rail_says_what_would_actually_play(db):
def test_only_a_show_row_pays_for_a_resolution(db):
"""`next` is the answer to "a show has no Play button". Attaching it to a movie or a video would
both cost a query for nothing and re-open the self-reference cycle the shelf already hit."""
both cost a query for nothing and re-open the self-reference cycle the shelf already hit.
The states are what make this test exist: without them every rail query inner-joins its way to
zero rows, the loop body never runs, and the assertion is vacuous — which is exactly how the
epic-end review found it."""
user, _ = _seed_yt(db)
_seed_plex(db, user)
movie, _, ep1 = _seed_plex(db, user)
_enable_plex(db)
for card in rails.continue_watching(db, user):
db.add_all(
[
VideoState(
user_id=user.id,
video_id="vmed",
status="new",
position_seconds=300,
progress_updated_at=NOW,
),
PlexState(
user_id=user.id,
item_id=movie.id,
status="new",
position_seconds=600,
progress_updated_at=NOW,
),
PlexState(
user_id=user.id,
item_id=ep1.id,
status="new",
position_seconds=120,
progress_updated_at=NOW,
),
]
)
db.commit()
rail = rails.continue_watching(db, user)
# A video, a movie and a show — one row of each kind that can appear here, so the assertion below
# actually exercises both sides of the equality.
assert {c["kind"] for c in rail} == {"video", "movie", "show"}
for card in rail:
assert ("next" in card) == (card["kind"] == "show"), card["id"]
@@ -732,6 +810,63 @@ def test_pinning_twice_is_the_same_shelf(db, as_user):
assert len(c.get("/api/discover/pins").json()["items"]) == 1
def test_pinning_a_plex_show_resolves_to_the_episode_that_would_play(db, as_user):
"""Two of the five pinnable kinds are Plex, and the rating_key → internal-id translation is
theirs alone — a wrong column or a wrong model in either arm pins the wrong row, or 404s every
Plex pin. Neither arm had a test before the epic-end review."""
user, _ = _seed_yt(db)
_, _, ep1 = _seed_plex(db, user)
_enable_plex(db)
db.add(PlexState(user_id=user.id, item_id=ep1.id, status="watched", watched_at=NOW))
db.commit()
c = as_user(user)
assert c.post("/api/discover/pins", json={"kind": "plex_show", "ref": "ps1"}).status_code == 200
(tile,) = c.get("/api/discover/pins").json()["items"]
assert (tile["kind"], tile["id"]) == ("show", "ps1")
assert tile["next"]["id"] == "ps1e2" # ep1 is finished
def test_pinning_a_plex_movie_plays_itself_without_a_cycle(db, as_user):
"""The self-reference that once 500'd the whole shelf, on the other leaf kind: `next` has to be a
distinct dict, not the tile aliased into itself. Covered for a video since S1, never for a movie."""
user, _ = _seed_yt(db)
_seed_plex(db, user)
_enable_plex(db)
c = as_user(user)
assert c.post("/api/discover/pins", json={"kind": "plex_item", "ref": "pm1"}).status_code == 200
(tile,) = c.get("/api/discover/pins").json()["items"]
assert tile["id"] == "pm1" and tile["next"]["id"] == "pm1"
def test_the_reroll_endpoint_parses_its_whole_exclusion_list_and_its_press_count(db, as_user):
"""The reroll's behaviour is covered at the domain level; the ROUTE's query-string translation —
the comma split, the cap, the `start` passthrough — was not covered at all, so a change there
could hand back the same card on every press with the suite green. Excluding all three candidates
is what proves the whole list parsed: a delimiter change leaves one unsplit string in the set,
and a hero comes back."""
user, _ = _seed_yt(db)
db.add(
VideoState(
user_id=user.id,
video_id="vmed",
status="new",
position_seconds=300,
progress_updated_at=NOW,
)
)
db.add(ComfortPin(user_id=user.id, video_id="vlong", position=0))
db.commit()
c = as_user(user)
# `start` rotates which tier answers first: 0 leads with resume, 1 with the shelf.
assert c.get("/api/discover/pick?seed=1&start=0").json()["hero"]["tier"] == "resume"
assert c.get("/api/discover/pick?seed=1&start=1").json()["hero"]["tier"] == "comfort"
skip = ",".join(f"youtube:{v}" for v in ("vshort", "vmed", "vlong"))
assert c.get(f"/api/discover/pick?seed=2&exclude={skip}").json()["hero"] is None
assert c.get("/api/discover/pick?budget=whenever").status_code == 422
def test_pinning_something_that_does_not_exist_is_a_404(db, as_user):
user, _ = _seed_yt(db)
r = as_user(user).post("/api/discover/pins", json={"kind": "video", "ref": "nope"})