chore(comments): drop epic, sprint and review-round ids from the code

The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").

Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.

Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).

One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
This commit is contained in:
2026-10-05 00:19:39 +02:00
parent c73d8be094
commit 4aaaaa7860
669 changed files with 1831 additions and 1904 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
"""Pytest fixtures for the DB-backed integration lane (R6 S3b).
"""Pytest fixtures for the DB-backed integration lane.
The bulk of the suite is pure-logic and needs no database. This adds an OPT-IN `db` fixture
that hands a test a real session against a throwaway Postgres (the app's own engine, bound to
@@ -38,8 +38,8 @@ from app.models import User
_MISSING = object()
# A would-be silent skip (no DB, or no ffmpeg) is a hard error unless the run says otherwise — the
# suite must not print green while the DB-backed / ffmpeg tests were quietly not run. ON by default
# since E32: as an opt-in only the compose lane set, a hand-run `pytest` skipped ~60 DB tests
# suite must not print green while the DB-backed / ffmpeg tests were quietly not run. ON by default:
# as an opt-in only the compose lane set, a hand-run `pytest` skipped ~60 DB tests
# and still passed. `SIFTLODE_TEST_REQUIRE_DB=0` is the explicit "pure-logic only" run.
_REQUIRE_FULL = os.getenv("SIFTLODE_TEST_REQUIRE_DB", "1") != "0"
@@ -117,7 +117,7 @@ def _request_dep(db, unfinished: list[str]):
savepoint released inside the handler commits nothing. The test's own staged rows are flushed before the request starts — they are the
test's business, not the handler's. A request that raises is rolled back, as production's
`close()` rolls it back: without that, what a failing handler flushed but never committed stayed
readable to the rest of the test — the one state production cannot be in (E16: an issue created
readable to the rest of the test — the one state production cannot be in (an issue created
on the tracker before a failed upload was kept only by an explicit commit, and nothing saw it
missing)."""
+3 -3
View File
@@ -1,4 +1,4 @@
"""E12 · S3 — the route-level halves of SEC-12 and SEC-14.
"""The route-level halves of SEC-12 and SEC-14.
`test_urlguard.py` pins the address judgement itself; this pins that the ENQUEUE route actually
consults it and answers in the structured `{code, reason}` shape the frontend localizes, and that a
@@ -38,7 +38,7 @@ def test_a_public_url_gets_past_the_guard(db, as_user, make_user, monkeypatch):
)
# An inline spec, so nothing ELSE refuses it: without one the request stopped at the
# profile lookup (400 no_profile) and "not url_blocked" held for a request the guard never
# had to wave through (E32 S2).
# had to wave through.
r = as_user(user).post(
"/api/downloads", json={"source": "https://example.com/v", "spec": {"format": "best"}}
)
@@ -56,7 +56,7 @@ def test_youtube_sources_never_consult_the_guard(db, as_user, make_user, monkeyp
db.commit()
monkeypatch.setattr(urlguard.socket, "getaddrinfo", boom)
# The job must actually be created: a request refused for any other reason would never reach
# the guard either, and the test would pass without proving anything (E32 S2).
# the guard either, and the test would pass without proving anything.
r = as_user(user).post(
"/api/downloads", json={"source": "dQw4w9WgXcQ", "spec": {"format": "best"}}
)
+1 -1
View File
@@ -1,4 +1,4 @@
"""The admin diagnostics console's query API (E22.6 S1), DB-lane."""
"""The admin diagnostics console's query API, DB-lane."""
from datetime import datetime, timedelta, timezone
+2 -2
View File
@@ -1,4 +1,4 @@
"""The admin lifeline for a sign-up that is stuck outside (E9.5 S5, roadmap UX-25), DB-lane.
"""The admin lifeline for a sign-up that is stuck outside, DB-lane.
Approve an address BEFORE the person registers and their sign-up produces no queue row at all —
`_decide` only takes a still-pending invite — so the Access-requests screen stayed empty while they
@@ -133,7 +133,7 @@ def test_an_unconfirmed_account_is_listed_and_can_be_sent_a_fresh_link(
lambda to, lang, link, **kw: sent.append((to, link)) is None,
)
admin = make_user(email="admin@example.com", role="admin")
# Active but unconfirmed — a real state since E9.5 S1 made the two gates order-independent. It is
# Active but unconfirmed — a real state now that the two gates are order-independent. It is
# not the admin's to decide, but the mail may simply never have arrived, so it is theirs to help
# with. The approved invite is what puts them on this screen at all: the list is "the address may
# sign in, this account still can't", so an undecided request stays with the queue that owns it.
+1 -1
View File
@@ -1,4 +1,4 @@
"""E30: an admin action that counts something keeps the count as data in its audit row's `after`, under
"""An admin action that counts something keeps the count as data in its audit row's `after`, under
the key the audit log names in the reader's language (`scheduler.result.<key>`, frontend
auditColumns.tsx) — not only in the English `summary`. A renamed key would fall back to the raw word.
DB lane (Postgres).
+9 -9
View File
@@ -168,17 +168,17 @@ def test_reset_confirm_is_single_use_and_kills_existing_sessions(client, make_us
def test_approving_an_unverified_account_keeps_its_password(db, make_user):
# E9.5 S1, inverting an earlier rule. Approval used to drop a password set while the email was
# This inverts an earlier rule. Approval used to drop a password set while the email was
# unverified (SEC-10, account pre-emption). It was dropped because it destroyed the password a
# legitimate registrant had just chosen, in what is the NORMAL ordering — the admin decides
# before the user reads their mail — making "your password was cleared for security" the default
# experience.
#
# S1 ALSO argued the revoke was redundant, because password_login refuses an unverified account
# before it reads is_active. The epic's closing review refuted that: the gate holds only while
# the address is unverified, and S6's approval mail exists to get the real owner to end that
# state. Do not restore that reasoning from this comment — the residual is SEC-16, and round 2
# of the review tried and reverted a guard for it (see test_review_round.py).
# It was ALSO argued that the revoke was redundant, because password_login refuses an
# unverified account before it reads is_active. The epic's closing review refuted that: the gate
# holds only while the address is unverified, and the approval mail exists to get the real
# owner to end that state. Do not restore that reasoning from this comment — the residual is
# SEC-16, and the same review tried and reverted a guard for it (see test_review_round.py).
from app.routes.admin import _activate_user_by_email
user = make_user(
@@ -245,7 +245,7 @@ def test_verifying_afterwards_lets_the_original_password_in(client, db, make_use
def test_activating_a_verified_account_keeps_its_password(db, make_user):
# The no-SMTP shape: registration marks such an account verified up front, so it was never in
# scope for the SEC-10 guard even before E9.5 S1. Kept as the other half of the pair.
# scope for the SEC-10 guard even back then. Kept as the other half of the pair.
from app.routes.admin import _activate_user_by_email
user = make_user(
@@ -354,7 +354,7 @@ def test_an_admin_can_activate_a_pre_approved_registrant(db, monkeypatch):
db.expire_all()
assert user.is_active is True
assert user.password_hash is not None # approval never touches the password (E9.5 S1)
assert user.password_hash is not None # approval never touches the password
def test_registration_of_an_unknown_email_still_persists_account_and_pending_invite(
@@ -483,7 +483,7 @@ def test_register_route_is_refused_when_registration_is_closed(client, db, monke
# was a deliberately uniform "ok"/401. `password_revoked_at` is the trace that tells the two apart;
# these pin each step of the way back in.
#
# E9.5 S1 removed the admin-approval caller, so Google adoption is the only path that still produces
# The admin-approval caller is gone, so Google adoption is the only path that still produces
# this state — plus every account stripped on prod before that change, which is why the whole recovery
# route below must keep working. These tests manufacture the state through the surviving producer.
+2 -2
View File
@@ -1,4 +1,4 @@
"""sync/autotag's orphan cleanup (E32 S2: the module was 32% covered).
"""sync/autotag's orphan cleanup (the module was 32% covered).
A system tag no channel carries any more is deleted — unless a saved feed view filters on it. The
views hold tag ids; a deleted tag returns under a NEW id when a channel earns it again, and the feed
@@ -38,7 +38,7 @@ def test_an_orphan_system_tag_goes_but_one_a_saved_view_filters_on_stays(db, mak
def test_a_malformed_saved_view_cannot_break_the_cleanup(db, make_user):
"""A view stored around the route's shape check (or before it) must not raise for everyone's job
— `"tags": 5` raised TypeError — and `True` is not tag id 1 (E32 review)."""
— `"tags": 5` raised TypeError — and `True` is not tag id 1."""
user = make_user()
orphan = Tag(user_id=None, name="Cooking", category="topic")
db.add(orphan)
+9 -9
View File
@@ -209,12 +209,12 @@ def test_the_default_ceiling_clears_the_largest_body_a_default_route_takes():
# its route is not in ROUTE_MAX_BODY_BYTES, this middleware refuses a request the route would have
# taken — silently, in production, on a path nobody tested. The point of the list is the third entry.
#
# BOTH helpers, not just `capped_body`: P1 added `capped_stream_to` as its sibling and the registry
# BOTH helpers, not just `capped_body`: `capped_stream_to` was added later and the registry
# did not follow, so the upload route — the one this whole invariant was written for — was the one
# site the drift guard could not see (deep-review round 1). The names come from `_common.py` itself
# site the drift guard could not see. The names come from `_common.py` itself
# below, so a THIRD sibling cannot be added without this list noticing either — and a route that
# skips the helpers altogether and awaits `request.body()` lands here as `<no ceiling declared>`
# rather than vanishing (round 3).
# rather than vanishing.
#
# The module is the path relative to `app/routes/`, so a site under `plex/` is spelled
# `plex/admin.py` and cannot collide with the top-level `admin.py`.
@@ -255,7 +255,7 @@ def test_the_registry_covers_every_body_reading_helper():
# they are blind to the shape `_common.py` itself is written in. A route spelling `await
# request.body()` inline declares no ceiling, hands no connection back, and would be invisible to
# all three — reading as "no new sites" rather than as a failure, which is the exact way a drift
# guard dies quietly (deep-review round 3).
# guard dies quietly.
RAW_BODY_READS = {"body", "stream", "form", "json"}
@@ -476,7 +476,7 @@ def test_the_upload_ceiling_and_the_admin_knob_cannot_drift_apart():
constant. If the knob could be set above the edge, an admin raising it would silently start
getting 413s from a layer the admin page never mentions.
Since P1 the two count the SAME bytes — the body is the file, with no multipart envelope
Now the two count the SAME bytes — the body is the file, with no multipart envelope
wrapped round it — so the old allowance is gone and the edge is the knob's own maximum,
exactly. A gap reappearing means someone re-wrapped the upload in an envelope without saying so.
@@ -509,13 +509,13 @@ def _route_objects(root):
@pytest.mark.parametrize(("method", "template"), sorted(bodylimit.ROUTE_MAX_BODY_BYTES))
def test_no_route_with_a_raised_ceiling_declares_a_body_parameter(method, template):
"""P1's invariant, and the reason this is a test rather than a comment.
"""The upload fix's invariant, and the reason this is a test rather than a comment.
A raised ceiling is only honest when the route's own cap applies to the bytes AS THEY ARRIVE.
FastAPI reads and spools a DECLARED body (`= File(...)`, `= Form(...)`, a Pydantic model)
BEFORE the first dependency resolves — so on such a route this middleware's ceiling is the only
thing between an anonymous caller and the whole allowance, the auth dependency never gets a say, and
the route's in-handler cap measures bytes that have already landed. That is exactly what P1
the route's in-handler cap measures bytes that have already landed. That is exactly what a fix
closed on the upload route, which sat in `ROUTE_MAX_BODY_BYTES` with a 500 MiB ceiling and a
`file: UploadFile = File(...)` body for one release.
@@ -529,7 +529,7 @@ def test_no_route_with_a_raised_ceiling_declares_a_body_parameter(method, templa
itself consults when it decides to read the body. Measured on the pinned FastAPI: a body declared
inside a sub-dependency leaves `body_params` EMPTY while the body is still read in full before
auth, so the first version of this test was green over a reproduction of the very defect it
exists to forbid (deep-review round 1). The upload route's own idiom — a body read from a
exists to forbid. The upload route's own idiom — a body read from a
`yield` dependency — is exactly the shape that would have slipped through."""
from app.main import app
@@ -546,7 +546,7 @@ def test_no_route_with_a_raised_ceiling_declares_a_body_parameter(method, templa
def test_a_stored_override_outside_the_bounds_falls_back_on_READ(db):
"""`set_value` refuses an out-of-range int, but neither bound has always been what it is now —
the upload knob had no `max` before SEC-20 and its `min` was 0 before E7 — so a value that was
the upload knob had no `max` before SEC-20 and its `min` used to be 0 — so a value that was
legal when it was written can be sitting in the database. Served as-is it is exactly the drift
the test above forbids: the route would accept bytes the edge refuses.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Two rows of CLAUDE.md's canonical-patterns table, held in code (E35 S4).
"""Two rows of CLAUDE.md's canonical-patterns table, held in code.
Both had drifted while the table said otherwise: seven `db.query(...)` calls stood beside the
`select()` the table names, and four modules carried a private `_now` / `now_utc` helper (nine uses)
+2 -2
View File
@@ -1,4 +1,4 @@
"""R14 backend coverage: the channel-detail Normal/Shorts/Live counts (moved off the manager table),
"""Backend coverage: the channel-detail Normal/Shorts/Live counts (moved off the manager table),
the update_channel -> immediate-backfill wiring, and the interactive
"get full history" guard that keeps it from racing the scheduler's cursor or draining the budget."""
@@ -54,7 +54,7 @@ def test_channel_detail_returns_normal_shorts_live_counts(db, monkeypatch, overr
@pytest.mark.parametrize("hidden", [True, False])
def test_channel_detail_surfaces_subscription_hidden_flag(db, monkeypatch, hidden, override_dep):
# S6: the detail GET must expose the per-subscription `hidden` flag so the channel page can offer
# The detail GET must expose the per-subscription `hidden` flag so the channel page can offer
# the same Unhide toggle the manager has (the flag was previously omitted from the detail dict).
ch = Channel(id="UC_hid", title="Hidden?", published_at=_PUB)
db.add(ch)
@@ -1,4 +1,4 @@
"""The channel manager's sums and the feed sidebar's counts read the index alone (E31 S4d).
"""The channel manager's sums and the feed sidebar's counts read the index alone.
`GET /api/channels` counts each subscribed channel's videos and sums their durations; `/feed/summary`
and `/feed/years` count the reader's videos per channel and per year under the default filters. They
+1 -1
View File
@@ -1,4 +1,4 @@
"""DB-backed regression (R8 Part 2): GET /api/channels/{id} returns exactly THIS user's personal
"""DB-backed regression: GET /api/channels/{id} returns exactly THIS user's personal
tag links for the channel — not another user's, and not the auto/system links (user_id NULL) —
so the channel detail page can reuse the manager's tag editor. Would ship broken if the tag_ids
query ever dropped its user_id filter.
+2 -2
View File
@@ -1,4 +1,4 @@
"""E30 S3: every A→Z list the server orders is in the reader's alphabet. The database image is Alpine,
"""Every A→Z list the server orders is in the reader's alphabet. The database image is Alpine,
whose `en_US.utf8` is BYTE order: "Életrajzi" and "Ötödik" sorted after "Zorró" in the feed, the channel
list, the Plex grid, the genre list. Hungarian puts "Ö" after "O" and "Cs" after "Cu". Needs the DB."""
@@ -209,7 +209,7 @@ def test_channel_discovery_keeps_that_order_after_enriching(db, monkeypatch, ove
assert [c["title"] for c in r.json()] == HU
# --- The other lists the server puts in the reader's alphabet (E30 review): each call site, once. ---
# --- The other lists the server puts in the reader's alphabet: each call site, once. ---
def _others(db, n_offset: int = 0) -> list[User]:
@@ -1,4 +1,4 @@
"""E3 S3 (B-core-10): the config/state mutators flush but do NOT self-commit, so the caller owns the
"""The config/state mutators flush but do NOT self-commit, so the caller owns the
transaction and a change lands together with its audit row. These lock that contract — a rollback
before the caller commits discards the change (proving no hidden internal commit), while a same-session
read still sees it (flush visibility). DB lane (`db` fixture) — skipped without Postgres.
+2 -2
View File
@@ -1,6 +1,6 @@
"""Smoke test proving the DB-backed lane works (R6 S3b): a real commit round-trips, and each
"""Smoke test proving the DB-backed lane works: a real commit round-trips, and each
test starts from a clean database (TRUNCATE isolation), which is what lets the transaction-
ownership tests (R6 S2) trust what they commit."""
ownership tests trust what they commit."""
from sqlalchemy import func, select
+3 -3
View File
@@ -1,4 +1,4 @@
"""E16 S1 — debug sessions: Start → reproduce → Stop, the tag on every line, the screenshots.
"""Debug sessions: Start → reproduce → Stop, the tag on every line, the screenshots.
Four parts:
* the session's life: one open per user, a second Start joins it, a Stop ends it, the cap ends it
@@ -774,11 +774,11 @@ def test_the_export_is_the_saved_draft_the_evidence_and_the_screenshots(db, make
assert z.read("screenshot-1.png") == _PNG
# --- the issue tracker (S4) -----------------------------------------------------------------
# --- the issue tracker ----------------------------------------------------------------------
class FakeGitea:
"""Gitea 1.27 as measured (plans/e16 S0): a private repository, one file per asset upload, and
"""Gitea 1.27 as measured: a private repository, one file per asset upload, and
a way to make one upload fail."""
def __init__(self):
+1 -1
View File
@@ -1,4 +1,4 @@
"""E22 S5 — a user's consented diagnostics level, and the browser log it lets in.
"""A user's consented diagnostics level, and the browser log it lets in.
Three parts:
* the level itself: the server computes its expiry, a lapsed one reads as the base (DB lane,
+6 -6
View File
@@ -1,4 +1,4 @@
"""E24 S1 — the Home surface's read-model.
"""The Home surface's read-model.
Covers the four things that can silently rot here: the time-budget bands (pure), the cross-source
resume rail's merge order, the hero's tier ladder, and the comfort shelf (its schema-level one-of
@@ -632,7 +632,7 @@ def test_a_pinned_video_survives_leaving_the_feed_scope(db):
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
"10 perc". The YouTube half of this rule has long had its own test; 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)
@@ -923,7 +923,7 @@ def test_pinning_a_plex_show_resolves_to_the_episode_that_would_play(db, as_user
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."""
distinct dict, not the tile aliased into itself. Long covered for a video, never for a movie."""
user, _ = _seed_yt(db)
_seed_plex(db, user)
_enable_plex(db)
@@ -941,7 +941,7 @@ def test_the_reroll_endpoint_parses_its_whole_exclusion_list_and_its_press_count
hero comes back.
NOT the `MAX_EXCLUDE` clamp, which this sends three keys against a cap of 32 and cannot see. The
docstring used to claim it did (round 2). The clamp is deliberately left unpinned: it guards
docstring used to claim it did. The clamp is deliberately left unpinned: it guards
nothing the route would otherwise do — see the constant's own comment — and a test that sent 33
keys could only assert that the request still answers, which the comma split already proves."""
user, _ = _seed_yt(db)
@@ -975,7 +975,7 @@ def test_pinning_something_that_does_not_exist_is_a_404(db, as_user):
def test_a_pin_whose_target_vanishes_mid_request_is_a_404_not_a_500(db, as_user, monkeypatch):
"""`add_pin`'s `except IntegrityError` arm was reached by no test at all, so the 500→404
narrowing could be reverted with the whole suite green (round 2). The 404 above comes from the
narrowing could be reverted with the whole suite green. The 404 above comes from the
`resolve_ref` PRE-CHECK, a different branch that returns before the INSERT.
The race the arm exists for — a Plex sync dropping the row between `resolve_ref` confirming it
@@ -1091,7 +1091,7 @@ def test_two_overlapping_reorders_never_share_a_slot(db, as_user):
row — two pins on one slot (deep review, debt burn). Locked, the second waits for the first and
deals from its result: here a partial list (a tab with Plex off, one pin hidden), whose left-out
pin must stay where the FIRST reorder put it — a lock taken in position order handed the waiter
fresh rows in its stale order, and it dealt that pin back to its old slot (round 2)."""
fresh rows in its stale order, and it dealt that pin back to its old slot."""
import threading
from app.db import SessionLocal
+2 -2
View File
@@ -1,4 +1,4 @@
"""S6 — per-user, per-host download cookies: host-scope, cache privacy, encryption, write-only API.
"""Per-user, per-host download cookies: host-scope, cache privacy, encryption, write-only API.
The two load-bearing security properties are covered as behaviour, not prose:
* **cache privacy** — a cookie-authed download is a PRIVATE, salted asset; a second user without a
@@ -283,7 +283,7 @@ class TestResolveCookiefile:
# --- Retry / Re-download take a cookie added later (DB) -----------------------------------------
# Measured on dev (E11.5): a Facebook reel failed on the login wall, the user added their cookie as
# Measured on dev: a Facebook reel failed on the login wall, the user added their cookie as
# the failure copy tells them to, pressed Retry — and it failed again, because the job still held the
# shared asset the worker never attaches a cookie to. Re-download forked a salt the gate could not
# recognise, so even a job downloaded WITH a cookie rebuilt without it.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Worker-child resource de-prioritization (S4): the encoder thread cap, the `-threads` flag on the
"""Worker-child resource de-prioritization: the encoder thread cap, the `-threads` flag on the
browser-safe re-encode, and the child's startup renice/ionice/OMP handicap. These keep a CPU-heavy
transcode from pegging every core on the box it shares with the API, Postgres, and Plex playback."""
+3 -3
View File
@@ -1,4 +1,4 @@
"""Category classification of raw yt-dlp / ffmpeg download-failure strings (S2)."""
"""Category classification of raw yt-dlp / ffmpeg download-failure strings."""
from app.downloads import adapters
from app.downloads import errors as dlerrors
@@ -71,7 +71,7 @@ def test_the_redacted_network_text_classifies_back_to_network():
assert classify(dlerrors.NETWORK_REDACTED_MESSAGE) == "network"
# Verbatim yt-dlp 2026.07.04 output from prod jobs, measured on dev 2026-09-13 (E11.5).
# Verbatim yt-dlp 2026.07.04 output from prod jobs, measured on dev 2026-09-13.
_FB_PARSE = (
"ERROR: [facebook] 1736360027626346: Cannot parse data; please report this issue on "
"https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template."
@@ -196,7 +196,7 @@ def test_our_post_not_shown_verdict_is_the_refused_cookie():
def test_our_post_without_a_video_verdict_names_the_group_link():
# The adapter's own text when the post page holds no single video (S4): the message and its rule and
# The adapter's own text when the post page holds no single video: the message and its rule and
# catalog entry are pinned together, or an edit to the wording orphans the copy.
msg = adapters.POST_WITHOUT_VIDEO_MESSAGE
assert classify(msg) == "format_unavailable"
@@ -1,4 +1,4 @@
"""DB-backed tests for the download failure catalog (E11.5 S1) at the places a failure is WRITTEN.
"""DB-backed tests for the download failure catalog at the places a failure is WRITTEN.
`test_download_errors` pins which entry a message matches; these pin that every writer hands the
matcher what it needs — the link and whether the user's cookie went with it — and that a stored entry
@@ -8,7 +8,7 @@ never outlives the failure it describes:
- Retry / Re-download, which requeue the job;
- the `0084` backfill of the rows that failed before the catalog existed;
- the downloads list, which is how the entry reaches the UI.
It also holds the one success-path check of the same child: the duration it stores (E11.5 S3).
It also holds the one success-path check of the same child: the duration it stores.
"""
import shutil
@@ -226,7 +226,7 @@ def test_the_downloads_list_carries_the_catalog_entry(db, as_user):
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not available")
def test_a_download_yt_dlp_gave_no_duration_stores_the_files_duration(db, monkeypatch, tmp_path):
# Facebook reel 257 (E11.5): yt-dlp's info had no duration, the asset stored NULL, and the Library
# Facebook reel 257: yt-dlp's info had no duration, the asset stored NULL, and the Library
# row lost its trim action. Through the real finalize, with a real 3-second file.
u = _user(db, "fb-duration@example.com")
a, j = _asset_and_job(db, u, sig="shared", asset_status="downloading", job_status="running")
+1 -1
View File
@@ -1,4 +1,4 @@
"""Overlapping requests on ONE job (E11.5 review) — DB-backed, two real sessions.
"""Overlapping requests on ONE job — DB-backed, two real sessions.
The routes that move a job's asset ref count (resume, cancel, delete, redownload) used to decide on an
unlocked read: two overlapping requests for one job (a double-click, a second tab, a replay after a
+2 -2
View File
@@ -1,4 +1,4 @@
"""DB-backed tests for R12 · S5 backend residue — the two download limits:
"""DB-backed tests for the two download limits:
- **#2 claim fairness** (`worker._claim_job`): users already AT their per-user concurrency limit
are excluded IN THE SQL, so one user flooding the queue past the fetch window can't starve
@@ -173,7 +173,7 @@ def test_list_downloads_is_capped(db, monkeypatch):
assert len(rows) == 2 # bounded by the cap, not the 3 rows on disk
# --- E12 · S3 · the worker-side max_bytes remainder ------------------------------------------
# --- the worker-side max_bytes remainder ----------------------------------------------------------
def _sized_asset(db, size_bytes, ref):
+1 -1
View File
@@ -1,4 +1,4 @@
"""E3 S2 characterization net — MediaAsset.ref_count enqueue/release accounting (B-sub-5).
"""Characterization net — MediaAsset.ref_count enqueue/release accounting.
`ref_count` tracks how many DownloadJobs still hold a shared cache asset; when it hits zero a READY
asset's file + row are freed. Enqueue +1's it and release_asset -1's it. Today those are Python
+1 -1
View File
@@ -1,4 +1,4 @@
"""E30: the "shared with you" notice carries the download's title in its data, so the client can name
"""The "shared with you" notice carries the download's title in its data, so the client can name
the notice in the reader's language. Its stored title is an English sentence ("Cats was shared with
you") — the only place the title lived, so a Hungarian reader got the English. Needs the DB."""
+1 -1
View File
@@ -1,4 +1,4 @@
"""E3 S2 characterization net — download job state transitions (B-sub-8).
"""Characterization net — download job state transitions.
pause / resume / cancel / delete / redownload currently live inline in the routes, each hand-rolling
the asset bookkeeping (release, error-reset, fresh-asset rebuild). This pins their OBSERVABLE HTTP
+3 -3
View File
@@ -1,4 +1,4 @@
"""One mailbox, many spellings — the canonical-email identity rule (E9.5 S3).
"""One mailbox, many spellings — the canonical-email identity rule.
Gmail delivers `x.y@gmail.com`, `xy@gmail.com` and `xy+tag@gmail.com` to ONE inbox. The invite gate
used to compare literal strings, so on 2026-09-03 an approved user's Google sign-in became a silent
@@ -351,9 +351,9 @@ def test_me_reports_the_google_address_only_when_it_differs(client, db, make_use
def test_rerunning_the_wizard_with_another_gmail_spelling_updates_the_same_admin(db):
"""The wizard can be re-run, and E9.5's premise is that two spellings of one Gmail mailbox are one
"""The wizard can be re-run, and the premise is that two spellings of one Gmail mailbox are one
person. Resolving by the literal string here would create a SECOND admin row — exactly the
duplicate the epic exists to prevent. This site was outside S3's sweep (DX-17).
duplicate canonical email exists to prevent. This site was missed by the first sweep (DX-17).
Calls the handler directly: `require_setup` is an instance-state gate, not the behaviour under
test, and the test fixtures present an already-configured instance."""
+3 -3
View File
@@ -1,11 +1,11 @@
"""An empty answer is not an emptied library (E32 S2).
"""An empty answer is not an emptied library.
Every mirror here reconciles: what the remote no longer lists is removed locally. The catalog
(`_prune_stale`) and the subscriptions (`should_prune`) had long refused to do that on an empty
listing that contradicts what we hold — a 200 with no items is a blip far more often than a user
who removed everything. Two mirrors did not: the Plex collections (a row carries the user-set
`editable` flag, and every member its keys) and the YouTube playlist mirrors. These hold both — and,
one level down (E32 review), a collection's members and a playlist's items: an empty answer that
one level down, a collection's members and a playlist's items: an empty answer that
contradicts the remote's own count is not believed either.
"""
@@ -174,7 +174,7 @@ class _YTOneGone(_YT):
def test_a_list_deleted_mid_pull_is_skipped_and_the_rest_still_sync(db, make_user, monkeypatch):
# Not "this playlist no longer exists" for a bulk pull the user never named a list in, and not
# a failure for every other list either (E32 review round 2): the next pull drops the mirror.
# a failure for every other list either: the next pull drops the mirror.
user = make_user()
db.commit()
listing = [
+5 -5
View File
@@ -1,13 +1,13 @@
"""E11.5 S3 + S4 — the Facebook download path with the user's cookie.
"""The Facebook download path with the user's cookie.
S3, measured on dev (yt-dlp 2026.07.04, the owner's cookie): reel 806379462330494 fails with the cookie
Measured on dev (yt-dlp 2026.07.04, the owner's cookie): reel 806379462330494 fails with the cookie
alone and extracts 2560p with `--impersonate chrome`; the reels that already download with the cookie
keep their plain first attempt. These pin the attempt order through the real download loop
(`pipeline._extract_with_fallback`, with yt-dlp faked at the `YoutubeDL` boundary), and that the
LOCKED curl_cffi can actually impersonate — the pair only works inside yt-dlp's own version range, and
outside it every target silently reports "unavailable".
S4, measured on dev and in the owner's Chrome: given a group post's link, yt-dlp finds no formats or
Measured on dev and in the owner's Chrome: given a group post's link, yt-dlp finds no formats or
downloads another video from the page's feed. The post's own video is the single Video node in the
page scripts that carry the post id (prod posts 66, 145, 140, 143, 221; text posts have none while the
page holds ~10 other videos), and its watch link downloads.
@@ -139,7 +139,7 @@ def test_other_facebook_failures_are_not_retried_as_chrome(ydl, tmp_path):
assert len(seen) == 1 and "impersonate" not in seen[0]
# --- S4: a group post downloads ITS video, or fails saying it has none ----------------------------
# --- a group post downloads ITS video, or fails saying it has none --------------------------------
def test_a_cookie_group_post_downloads_the_posts_own_video(ydl, tmp_path):
@@ -174,7 +174,7 @@ def test_a_group_post_without_a_single_video_fails_saying_so(ydl, tmp_path):
def test_a_group_post_page_without_the_post_is_the_refused_cookie(ydl, tmp_path):
# Measured with a refused cookie (E11.5 review): Facebook answers the post link with its login page —
# Measured with a refused cookie: Facebook answers the post link with its login page —
# a login form, no post id anywhere. Saying "this post has no video" would blame the post.
_, _, web = ydl
web["page"] = '<html><title>ChatGPT magyarul</title><form id="login_form"></form></html>'
+1 -1
View File
@@ -1,4 +1,4 @@
"""E22.5 review round 1: a failure is reported once, with its code, and a repeating one is not a
"""A failure is reported once, with its code, and a repeating one is not a
flood.
Each case here is a place where the first sweep got the line wrong in one of two directions: a
+2 -2
View File
@@ -1,4 +1,4 @@
"""Relevance ranks only the newest RANK_WINDOW matches (E31), DB-lane.
"""Relevance ranks only the newest RANK_WINDOW matches, DB-lane.
A broad word ("a") matched most of the prod library and ts_rank read every match's tsvector: 6.8 s a page.
The ranking now covers the newest RANK_WINDOW matches and the rest follow newest first. The window's start
@@ -126,7 +126,7 @@ def test_cursor_walk_matches_the_unpaged_order(db, as_user, monkeypatch, window,
)
def test_a_hand_made_relevance_cursor_is_refused_not_a_crash(db, as_user, payload):
# The page halves compare the cursor's timestamp with the window's in Python: a null or a naive
# one raised there, past the route's 400 — a 500 (E31 review 2).
# one raised there, past the route's 400 — a 500.
client = as_user(_seed(db))
cursor = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
resp = client.get(f"/api/feed?q=foo&sort=relevance&limit=2&cursor={cursor}")
+1 -1
View File
@@ -1,4 +1,4 @@
"""Every feed sort on a videos column rides an index, and an ICU upgrade rebuilds the title ones (E31 S4e).
"""Every feed sort on a videos column rides an index, and an ICU upgrade rebuilds the title ones.
Without an index in the exact ORDER BY, each feed page read the whole videos heap and kept a top-N
(prod: 350 ms a page for views/duration, 550–780 ms for title). Migration 0096 adds one per direction
+5 -5
View File
@@ -1,4 +1,4 @@
"""Feed search + watch-state views (E2 S3), DB-lane.
"""Feed search + watch-state views, DB-lane.
Regression nets for two query rewrites that must stay behaviour-equivalent:
- PERF-12: the search channel-name branch moved from an ORed `unaccent(channels.title) ILIKE` (which
@@ -178,7 +178,7 @@ _SUMMARY_CASES = [
def test_a_deleted_tags_id_filters_to_nothing_not_to_everything(db, as_user, make_user):
"""A saved view or the feed's filter state keeps a tag id after the tag is deleted. The feed used
to drop an unknown id, so that view silently showed the WHOLE feed under its old name (E32 S2).
to drop an unknown id, so that view silently showed the WHOLE feed under its old name.
A filter on a tag that no longer exists matches nothing; the chip says the tag is gone."""
user, tag_ids = _seed_tagged(db, make_user)
client = as_user(user)
@@ -196,8 +196,8 @@ def test_a_deleted_tags_id_filters_to_nothing_not_to_everything(db, as_user, mak
def test_another_users_private_tag_answers_like_one_that_does_not_exist(db, as_user, make_user):
"""The category split resolved any tag id, so a foreign PRIVATE tag landed in its real category
and the summary's facets differed from a non-existent id's: an oracle for which foreign ids exist
(E32 review). Both must now read as "missing"."""
and the summary's facets differed from a non-existent id's: an oracle for which foreign ids exist.
Both must now read as "missing"."""
user, tag_ids = _seed_tagged(db, make_user)
stranger = make_user(email="stranger@example.com")
private = Tag(user_id=stranger.id, name="Secret", category="language")
@@ -247,7 +247,7 @@ def test_untagged_filter_keeps_only_channels_the_user_has_not_tagged(db, as_user
def test_feed_years_counts_ignore_the_date_narrowing_but_honour_other_filters(
db, as_user, make_user
):
"""The year facet (item 8 / E8·S2): `/api/feed/years` returns a per-year video count, newest first,
"""The year facet: `/api/feed/years` returns a per-year video count, newest first,
with the publish-date narrowing DROPPED (so switching years stays a drill-down) but every other
filter still applied (here: a channel pick)."""
user = make_user(email="years@example.com")
+3 -3
View File
@@ -1,7 +1,7 @@
"""build_ydl_opts: EVERY download is clamped to the mp4/H.264 compat profile (E7 browser-
"""build_ydl_opts: EVERY download is clamped to the mp4/H.264 compat profile (the browser-
playability guarantee) and must SELECT H.264 (not merely prefer it), so shared downloads play in
every browser incl. iOS/Safari. Non-mp4 containers and non-H.264 vcodecs are coerced, not honoured.
See the iPad AV1 broken-player fix + E7 format narrowing."""
See the iPad AV1 broken-player fix + the format narrowing."""
from app.downloads import edit as editmod
from app.downloads import formats
@@ -58,7 +58,7 @@ def test_max_height_caps_every_branch():
def test_explicit_vcodec_is_clamped_to_the_compat_selector():
# E7 browser-playability guarantee: a non-H.264 vcodec no longer opts out — normalize() coerces
# Browser-playability guarantee: a non-H.264 vcodec no longer opts out — normalize() coerces
# vcodec→None, so the avc1 selector (and mp4 merge) is used regardless of what was requested.
opts = _opts({"mode": "av", "container": "mp4", "vcodec": "vp9"})
assert opts["format"].startswith("bestvideo[vcodec^=avc1]")
+7 -7
View File
@@ -2,15 +2,15 @@
plex/stream.py promises "a periodic reaper kills idle ones", but `reap_idle` ran only inside
POST /session: after the last playback, an abandoned session (its ffmpeg still working through the
film, its directory a whole segmented copy of it) lived until somebody pressed play again
(deep-review round 4).
film, its directory a whole segmented copy of it) lived until somebody pressed play again.
And it must not take a PAUSED viewer's session: a remux finishes long before the film does, and a
paused player with a full buffer fetches nothing, so "finished and unfetched for 30 s" does not mean
"abandoned". Round 4 bought that safety by keeping EVERY finished session for the full ten-minute
idle window — one segmented copy of a film per item, on the volume the download center shares. Round
5 replaced the blanket retention with the signal that already existed: an open player checkpoints its
position every 10 s whether or not it is playing, and that route says "still here".
"abandoned". An earlier fix bought that safety by keeping EVERY finished session for the full
ten-minute idle window — one segmented copy of a film per item, on the volume the download center
shares. Its successor replaced the blanket retention with the signal that already existed: a player
parked mid-item checkpoints its position every 10 s, paused or not, and that route says "still
here" (it stops once the item is finished, where reaping is correct).
Both call sites are pinned here, because the bug each time was that only one of them moved: the
periodic pass, and the fact that starting a session no longer sweeps OTHER viewers' sessions at all.
@@ -174,7 +174,7 @@ def test_starting_a_session_leaves_another_viewers_paused_one_alone(
):
"""The opportunistic sweep this route used to run was not scoped to the key being started: it
walked the WHOLE registry with the 30 s finished rule, so pressing play on any item killed a
paused viewer's session — the very case round 4 set out to protect (found in round 5)."""
paused viewer's session — the very case the retention fix set out to protect."""
monkeypatch.setattr(state, "_configured_cache", True)
monkeypatch.setattr(plex_paths, "local_media_path", lambda db_, fp: "/local/a.mkv")
started = _library_item(db, "starting")
+1 -1
View File
@@ -1,4 +1,4 @@
"""`users.last_login_at` (E22 S1), DB-lane.
"""`users.last_login_at`, DB-lane.
The roadmap assumed stamping it in `_establish_session` would cover Google and password alike. It
would not have: only password login called it, and the Google callback carried its own copy of the
+1 -1
View File
@@ -99,7 +99,7 @@ class TestPublicMetaAudioFlag:
class TestCreateLinkDefault:
"""D1 (E7 S3): a newly minted share link is downloadable by default. The one-click Share relies on
"""D1: a newly minted share link is downloadable by default. The one-click Share relies on
this — it POSTs no `allow_download`, so the API default decides. Forward-only lives here (the API
default), NOT in a migration: the DownloadLink column default stays False, so no existing back-
catalogue link silently flips to downloadable."""
+1 -1
View File
@@ -1,4 +1,4 @@
"""E22.5 S1 — what a log line may NOT carry.
"""What a log line may NOT carry.
`logs.redact_text` is a safety net over addresses and credential params, and it was doing the whole
job: every one of these call sites was putting user content into a line and relying on the net to
+2 -2
View File
@@ -1,9 +1,9 @@
"""E22.5 S2 — every module's logger is named after the module.
"""Every module's logger is named after the module.
The names used to be literals, and 25 of them covered 34 modules: `plex/client.py`, `plex/stream.py`,
`plex/subtitles.py`, `plex/sync.py` and two route modules all called themselves `siftlode.plex`, and
`downloads/pipeline.py` called itself `siftlode.worker`. A level pinned per logger could not separate
them, and neither can E22.6's console filter — the `logger` field is in every stored row precisely so
them, and neither can the console's filter — the `logger` field is in every stored row precisely so
it can be filtered on.
So the name is derived, not written: `logs.get_logger(__name__)`. These cases pin the derivation and
+4 -4
View File
@@ -1,10 +1,10 @@
"""What a refused sign-in SAYS (E9.5 S4), DB-lane.
"""What a refused sign-in SAYS, DB-lane.
The states behind a 403 were true and useless: "Your account is awaiting admin approval." never
revealed WHICH mailbox the account was on. On 2026-09-03 a registration as `npeter83.temu@gmail.com`
met a Google claim of `npeter83temu@gmail.com`, and that missing address hid the mismatch for a day.
So these pin the DETAIL, not just the status code — the address, and both gates, since E9.5 S1 made
the two order-independent and the screen must report whichever is actually outstanding.
So these pin the DETAIL, not just the status code — the address, and both gates, since the two are
order-independent and the screen must report whichever is actually outstanding.
The two refusals that answer BEFORE the password is proven carry nothing, and that is the security
half of the same contract: `invalid` must stay byte-identical for a real account and an unknown one.
@@ -34,7 +34,7 @@ def test_pending_approval_names_the_address_the_account_is_on(client, make_user)
def test_an_already_approved_account_still_reports_its_missing_verification(client, make_user):
# S1 made the gates order-independent, so this really happens: approved, not yet verified. A
# The gates are order-independent, so this really happens: approved, not yet verified. A
# screen that assumed a sequence would tell this user to wait for an admin who already decided.
make_user(
email="approved.first@example.com", password=_PW, email_verified=False, is_active=True
+2 -2
View File
@@ -1,4 +1,4 @@
"""E22 S2 — the one logging configuration, its redaction, and the request line that replaced
"""The one logging configuration, its redaction, and the request line that replaced
uvicorn's access log.
Redaction is the part worth guarding hardest: it is applied at the HANDLER, so these cases are the
@@ -148,7 +148,7 @@ def test_every_line_carries_the_agreed_fields(sink):
def test_a_plain_log_call_still_produces_a_well_formed_line(sink):
"""An event code is optional — 125 existing call sites have none yet (E22.5 adds them), and they
"""An event code is optional — 125 existing call sites have none yet, and they
must not be worse off than before this module."""
logging.getLogger("siftlode.test").warning("just a message")
payload = sink.objects()[-1]
+1 -1
View File
@@ -1,4 +1,4 @@
"""E22 S3 — the `log_events` sink: what reaches the table, in what shape, and what it refuses to do.
"""The `log_events` sink: what reaches the table, in what shape, and what it refuses to do.
The sink is driven synchronously (`write_pending()` on the test's own thread) so nothing here waits on
a timer. It is attached to a private logger rather than the root, for the same reason test_logs.py's
+2 -2
View File
@@ -1,4 +1,4 @@
"""E9.5 S7 — every outbound mail is written in the reader's language.
"""Every outbound mail is written in the reader's language.
Two halves. The first is the catalog invariant: HU/EN parity with identical placeholders, enforced
here because a mail is the one user-facing surface nobody can re-render by switching the UI language
@@ -160,7 +160,7 @@ def test_asking_again_from_another_language_updates_the_row(db):
db.expire_all()
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
# The newest thing they told us wins — and there is still exactly one row (S3's canonical upsert).
# The newest thing they told us wins — and there is still exactly one row (the canonical upsert).
assert inv.lang == "hu"
assert db.execute(select(Invite)).scalars().all() == [inv]
+1 -1
View File
@@ -1,4 +1,4 @@
"""sync/maintenance — the job that HARD-DELETES videos (E32 S2: 19% covered).
"""sync/maintenance — the job that HARD-DELETES videos (it was 19% covered).
Its lifecycle: a video missing from videos.list is flagged and hidden; after the grace period a
still-missing one is deleted (cascading to every playlist that held it) and each affected user
@@ -1,4 +1,4 @@
"""A concurrent index build that failed part-way, then re-run (E31 S5 review), DB-lane.
"""A concurrent index build that failed part-way, then re-run, DB-lane.
CREATE INDEX CONCURRENTLY that fails leaves an INVALID index under its name, and alembic_version is not
stamped, so the next start runs the migration again. `IF NOT EXISTS` matched the name and kept the
+10 -10
View File
@@ -1,4 +1,4 @@
"""A row that is gone answers with a reason the bilingual UI can translate (deep-review round 3).
"""A row that is gone answers with a reason the bilingual UI can translate.
The 404 a user actually meets is the one for something deleted in another tab or on another device.
`owned_or_404` / `get_or_404` sent it as a bare English string ("Unknown view", "Not found"), and the
@@ -24,8 +24,8 @@ def test_a_gone_view_tag_or_pin_says_what_is_gone(db, make_user, as_user):
def test_a_gone_channel_video_or_user_says_what_is_gone(db, make_user, as_user):
"""The siblings that still hand-wrote `db.get(...) is None → 404 "Unknown …"` (deep-review
round 4): the same gone row, but the Hungarian UI showed the English."""
"""The siblings that still hand-wrote `db.get(...) is None → 404 "Unknown …"`: the same
gone row, but the Hungarian UI showed the English."""
admin = make_user(role="admin")
client = as_user(admin)
for method, url, body, what in [
@@ -45,12 +45,12 @@ def test_a_gone_channel_video_or_user_says_what_is_gone(db, make_user, as_user):
def test_every_gone_channel_route_says_it_the_same_way(db, make_user, as_user, override_dep):
"""The round-4 sweep converted ONE of the five `db.get(Channel, …) is None -> 404 "Unknown
"""An earlier sweep converted ONE of the five `db.get(Channel, …) is None -> 404 "Unknown
channel"` sites (block), and shipped the HU/EN string with it — so the Hungarian UI said it in
Hungarian on the Block button and in English on the four beside it (deep-review round 5). The
Hungarian on the Block button and in English on the four beside it. The
asymmetric fix is the smell this repo bans, so all five are pinned together.
Literally all five: round 6 found this loop issuing three requests under a docstring claiming
Literally all five: a review found this loop issuing three requests under a docstring claiming
five, with explore silently absent and subscribe excused as "covered by the tags route" — which
reaches `_user_subscription`, a different site answering `reason: subscription`. Reverting
either of those two to its bare English string left the whole lane green."""
@@ -77,7 +77,7 @@ def test_a_gone_subscription_recipient_or_job_says_what_is_gone(db, make_user, a
verbatim into an otherwise Hungarian UI: "Not subscribed to this channel", "No user with that
email.", "Unknown job".
All three, with a request each. Round 6 found this test naming three and issuing two: the
All three, with a request each. A review found this test naming three and issuing two: the
recipient site had no test in the whole suite (nor did the `errors.notFound.recipient` keys
added beside it), and `run_job_now` rode on its PATCH sibling's coverage."""
admin = make_user(role="admin")
@@ -117,8 +117,8 @@ def test_a_gone_subscription_recipient_or_job_says_what_is_gone(db, make_user, a
def test_a_nul_in_a_request_string_is_a_422_not_a_500(db, make_user, as_user):
"""Postgres cannot hold NUL in text, so the driver refuses the query outright: a DataError, i.e.
a 500 with a traceback, wherever a `%00` or an escaped NUL reached a comparison (deep-review
round 4 — round 3 had guarded only the by-rating_key lookup). One app-wide handler now answers
a 500 with a traceback, wherever a `%00` or an escaped NUL reached a comparison (an
earlier fix had guarded only the by-rating_key lookup). One app-wide handler now answers
all of them."""
client = as_user(make_user())
r = client.get("/api/plex/playlists?contains=%00")
@@ -132,7 +132,7 @@ def test_any_other_data_error_is_still_a_500():
registered for EVERY DataError on every route and decides on a substring of the driver's message,
so only a NUL may become a 422. Without this, simplifying it to an unconditional 422 — it reads
like dead defensive code — would answer a numeric overflow or an over-long ingested title with
"your input was bad", losing the traceback that names the real fault (deep-review round 5)."""
"your input was bad", losing the traceback that names the real fault."""
import asyncio
import pytest
+1 -1
View File
@@ -1,4 +1,4 @@
"""JPEG dimension parsing for the /watch OG image tags (S2, item 6).
"""JPEG dimension parsing for the /watch OG image tags.
Facebook's scraper fetches og:image asynchronously on first scrape, so the card is image-less
unless og:image:width/height are declared — which needs the poster's real dimensions, read from the
@@ -39,7 +39,7 @@ def test_a_request_gets_the_session_that_checks_its_commit():
def test_the_commit_check_counts_only_the_root_transaction(db):
"""A savepoint released inside a handler commits nothing to the database; ending it must not
clear the check (round 2). Driven directly, as a request would drive the dependency."""
clear the check. Driven directly, as a request would drive the dependency."""
import pytest
from starlette.requests import HTTPConnection
@@ -63,7 +63,7 @@ def test_the_commit_check_counts_only_the_root_transaction(db):
def test_a_request_that_raises_leaves_nothing_it_did_not_commit(db):
"""Production closes a failing request's session, which rolls back whatever it flushed and did
not commit; the shared test session must do the same, or a later read in the test sees a write
production would have lost (E16: the tracker's issue link, kept only by an explicit commit in
production would have lost (the tracker's issue link, kept only by an explicit commit in
the handler's failure path). Driven directly, as a request would drive the dependency."""
import pytest
from sqlalchemy import select
+1 -1
View File
@@ -1,4 +1,4 @@
"""E28 S3e2 — adding a video AT a position: the list page's "Undo" of a removal.
"""Adding a video AT a position: the list page's "Undo" of a removal.
The row has to come back where it was in ONE request. The two-call shape (append, then send the
whole order) leaves it at the end of the list whenever the second call fails, and the reorder would
+1 -1
View File
@@ -1,4 +1,4 @@
"""S7 — two-way (3-way) YouTube playlist merge.
"""Two-way (3-way) YouTube playlist merge.
The load-bearing property: an item added (or deleted) on EITHER side survives the sync — the old
push was one-directional (local wins, deletes everything not local from YouTube), so YouTube-side
+2 -2
View File
@@ -1,4 +1,4 @@
"""E10 S2 — the one-step "move this video to another list" endpoint, and the `updated_at` touch
"""The one-step "move this video to another list" endpoint, and the `updated_at` touch
the rail's "recently changed" sort reads.
Two properties are load-bearing and neither is visible from the happy path alone:
@@ -238,7 +238,7 @@ def test_item_edits_bump_updated_at(db, make_user, as_user):
assert db.get(Playlist, src.id).updated_at > old
assert db.get(Playlist, dst.id).updated_at > old
# …and removing one, the fourth item-only write (it had no test: E10 review coverage debt).
# …and removing one, the fourth item-only write (it had no test).
src.updated_at = old
db.commit()
assert client.delete(f"/api/playlists/{src.id}/items/v1").status_code == 200
+2 -2
View File
@@ -1,4 +1,4 @@
"""E28 review — every request that reads a playlist's item positions and writes new ones locks the
"""Every request that reads a playlist's item positions and writes new ones locks the
playlist row first.
Without it two adds (two tabs; the rail and the page) both read the same `max(position)` and land
@@ -7,7 +7,7 @@ rows is then whatever the planner returns. A true two-connection race is not rep
demand, so the test holds the mechanism: the parent row is taken `FOR UPDATE` by each such route.
A Plex reorder that omits a row (added in another tab meanwhile) must not collide with it either.
Round 2: the lock also comes FIRST — before the request reads or writes a single item row. A remove
The lock also comes FIRST — before the request reads or writes a single item row. A remove
that deleted an item and then touched the playlist beside a reorder that went the other way was a
deadlock (a 500 for one of them), and an add that checked "already there?" before the lock let two
adds of one video both insert (a 500 on the unique key).
+3 -3
View File
@@ -1,4 +1,4 @@
"""E28 review C1 — one YouTube sync of a playlist at a time, whoever starts it.
"""One YouTube sync of a playlist at a time, whoever starts it.
A push reads YouTube and then writes both sides for seconds, committing step by step, so the
client's in-flight flag (one page's menus) and a row lock (gone at the first commit) both leave two
@@ -67,7 +67,7 @@ def test_push_and_revert_refuse_while_the_list_is_claimed(
r = c.post(f"/api/playlists/{pl.id}/{path}")
assert r.status_code == 409
assert r.json()["detail"] == {"code": "playlist", "reason": "sync_busy"}
# a delete too: a push mid-way would plan the deleted list as emptied (E28 round 4)
# a delete too: a push mid-way would plan the deleted list as emptied
r = c.delete(f"/api/playlists/{pl.id}")
assert r.status_code == 409
assert r.json()["detail"] == {"code": "playlist", "reason": "sync_busy"}
@@ -132,7 +132,7 @@ def test_the_scheduled_refresh_skips_a_mirror_being_synced(db, make_user, monkey
def test_refused_reverts_do_not_spend_the_pull_cooldown(
db, as_user, make_user, override_dep, monkeypatch
):
"""Round 2: the cooldown (three pulls per ten minutes) is checked after the claim, so clicking
"""The cooldown (three pulls per ten minutes) is checked after the claim, so clicking
Revert while a push runs is refused as busy without using up a pull."""
user = make_user()
pl = _linked(db, user)
@@ -1,7 +1,7 @@
"""E28 review round 3 — the YouTube sync paths take the playlist row BEFORE they touch an item, and
"""The YouTube sync paths take the playlist row BEFORE they touch an item, and
hold it to the commit that writes them.
Round 2 made every route edit lock the playlist first. The sync paths were left item-first (a push's
Every route edit already locked the playlist first. The sync paths were left item-first (a push's
and a re-pull's item replace, a delete's cascade), so a push beside a remove could deadlock (a 500),
and the scheduled mirror refresh re-ingested inside its lock — its commit let go of the lock before
the replace, so a remove made then was undone and the list read as synced. A push also replaced the
+1 -1
View File
@@ -1,4 +1,4 @@
"""routes/plex/admin — the connection test and the owner's watch-sync link (E32 S2: 37% covered).
"""routes/plex/admin — the connection test and the owner's watch-sync link (it was 37% covered).
The connection test is the admin's one early warning that the media mount or path map is wrong, and
it must not quote the server's URL back in a toast. The watch link runs the one-time "Plex is master"
+1 -1
View File
@@ -1,6 +1,6 @@
"""Plex card serializers (app.plex.cards) — pure model->dict projections, no DB needed.
E5 S1: movie/show cards now also emit `art` (the wide backdrop, for a Discover rail's landscape tile
Movie/show cards now also emit `art` (the wide backdrop, for a Discover rail's landscape tile
and the crossfading hero), but ONLY when the item actually has an `art_key` — otherwise the URL would
resolve to a blank image on every card that tried to use it, so it must be null and let the client fall
back to the poster."""
+3 -3
View File
@@ -1,4 +1,4 @@
"""plex/client — the one door to the Plex server (E32 S2: 31% covered).
"""plex/client — the one door to the Plex server (it was 31% covered).
What matters here is what the callers cannot see: a failure must surface as PlexError (never as an
empty answer), the paged reads must stop where they should, and the write URIs must be well formed.
@@ -79,7 +79,7 @@ def test_a_section_page_asks_for_guids_and_reports_the_server_total(plex):
assert params["includeGuids"] == "1" and params["type"] == "1"
assert params["X-Plex-Container-Start"] == "200" and params["X-Plex-Container-Size"] == "50"
# Measured on the real server (E32 review): a paged request carries `totalSize` even when the
# Measured on the real server: a paged request carries `totalSize` even when the
# page is empty, past the end, or the section holds nothing — `size: 0, totalSize: 0`.
plex.routes["/library/sections/7/all"] = {"Metadata": [], "size": 0, "totalSize": 0}
assert plex.section_items("7", 2) == ([], 0)
@@ -168,7 +168,7 @@ def test_a_collection_is_created_with_a_well_formed_uri(plex):
def test_a_server_without_a_machine_id_refuses_the_write(plex):
"""No machineIdentifier used to become `server:///…` in the write URI — a malformed request
Plex answers in its own way. It is refused here, named (E32 S2)."""
Plex answers in its own way. It is refused here, named."""
plex.routes["/identity"] = {}
plex.routes["/library/collections"] = {"Metadata": [{"ratingKey": "c1"}]}
with pytest.raises(PlexError, match="machine"):
+4 -4
View File
@@ -1,4 +1,4 @@
"""Regression (E5 S5b): the info-page rich detail (IMDb score/id, tagline, full cast) is persisted at
"""Regression: the info-page rich detail (IMDb score/id, tagline, full cast) is persisted at
rich-fetch time and served from the DB row instead of a live plex.metadata() call.
- `rich_detail`/`apply_rich_detail` are pure — parse + persist the rich-only fields.
@@ -64,7 +64,7 @@ def test_rich_detail_prefers_imdb_entry_and_builds_link():
def test_rich_detail_is_imdb_only_no_audience_fallback():
# E5 S6: imdb_rating is IMDb-ONLY. With no imdb:// Rating entry it's NULL — the Plex audience score
# Imdb_rating is IMDb-ONLY. With no imdb:// Rating entry it's NULL — the Plex audience score
# is NOT mislabelled as an IMDb score (it's served separately via the `rating` column).
d = detail.rich_detail({"audienceRating": 7.25})
assert d["imdb_rating"] is None
@@ -113,7 +113,7 @@ class _StubClient:
def test_apply_facet_fields_preserves_enriched_cast_names():
"""A full sync must NOT downgrade an already-enriched full cast back to the listing top-20 — since
sync never clears people_enriched_at, nothing would regrow it and the person-page filter would lose
the names past the listing cap (regression guard for E5 S5b's enrich gating)."""
the names past the listing cap (regression guard for the enrich gating)."""
# The cheap listing's top few cast + the (always-present) director.
thin = {
"Role": [{"tag": "Al Pacino"}, {"tag": "James Caan"}],
@@ -357,7 +357,7 @@ def test_show_detail_lazy_fills_when_unenriched(db, monkeypatch, override_dep):
def test_item_detail_names_tracks_from_their_language_not_plexs_title(
db, monkeypatch, override_dep
):
"""E30: Plex titles a track in the Plex SERVER's language ("English (AC3 5.1)", "magyar Forced",
"""Plex titles a track in the Plex SERVER's language ("English (AC3 5.1)", "magyar Forced",
"中文 (EAC3 Stereo)"), so a Hungarian reader got a mix. The route sends what the client names it
from instead — the language tag, the forced / SDH flags, the codec tail — keeping Plex's title
only as the fallback, and no invented English "Audio 2" for a track with neither."""
+1 -1
View File
@@ -1,4 +1,4 @@
"""E5 S4: the Plex Discover rail-surface. GET /api/plex/discover returns the consolidated rail
"""The Plex Discover rail-surface. GET /api/plex/discover returns the consolidated rail
payload (continue watching / recently added / genre tiles / because-you-watched) served from the DB,
and GET /api/plex/surprise returns a scope-bounded random sample. DB lane (`db` fixture) — skipped
without Postgres.
+2 -2
View File
@@ -1,4 +1,4 @@
"""DB-backed regression for the 0.53.1 hotfix (R6 S3b-test): GET /api/plex/facets?scope=show
"""DB-backed regression for the 0.53.1 hotfix: GET /api/plex/facets?scope=show
must return 200 — not the "cannot extract elements from a scalar" 500 — for a metadata-poor
library whose show has a JSONB-scalar `null` genres (a pre-0.53.1 row). The endpoint's
`jsonb_typeof(genres)='array'` read-guard is what makes it safe; this test would 500 without it.
@@ -17,7 +17,7 @@ from app.models import PlexItem, PlexLibrary, PlexShow, User
def test_facets_returns_studio_vocabulary(db, monkeypatch, override_dep):
"""E5 S7: /facets returns the studio vocabulary (distinct studio + count) for the sidebar picker."""
"""/facets returns the studio vocabulary (distinct studio + count) for the sidebar picker."""
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
db.add(lib)
db.flush()
@@ -1,6 +1,6 @@
"""E3 S1 characterization net — /library grid ⟷ /facets equivalence across watch-state scopes.
"""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
The Plex god-module split 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
@@ -218,7 +218,7 @@ def test_hidden_movie_absent_from_grid_and_facets(db, as_user):
def test_a_watch_state_scope_rolls_the_episodes_up_once(db, as_user):
"""The per-user episode rollup behind a show's watch state is a pass over every episode; each show
facet group asked it again (E31 S4e: half of a filtered facets call). It is asked once now."""
facet group asked it again (half of a filtered facets call). It is asked once now."""
from sqlalchemy import event
from app.db import engine
@@ -1,4 +1,4 @@
"""S2 · field-scoped search — the `field` param on /api/plex/library.
"""Field-scoped search — the `field` param on /api/plex/library.
The search box's scope-pill narrows a full-text query to ONE facet of the row: Everywhere (`all`,
the untouched weighted `search_vector`), Titles, Cast & crew (`people`), Descriptions (`summary`),
+7 -7
View File
@@ -5,11 +5,11 @@ seek-restart — or a second viewer of the same item — replaces the registered
viewer's next request for `seg_12.ts`, resolved by rating_key, was answered 200 with the NEW
session's twelfth segment: bytes produced from a different `-ss` offset under `-copyts`, i.e. a
timeline the client is not on. Wrong content with a success status is the one failure a player cannot
notice, and the reclaim-on-404 added in round 4 could never fire on it (deep-review round 5).
notice, and the client's reclaim-on-404 could never fire on it.
No `db` fixture: the route reads nothing from the database (its own `Depends(get_db)` went with the
removed auto-start, and `current_user` is overridden wholesale), so these must not sit in the DB
lane — there they SKIP on a machine with no Postgres while pytest prints green (round 6).
lane — there they SKIP on a machine with no Postgres while pytest prints green.
"""
import time
@@ -127,7 +127,7 @@ def test_a_segment_that_will_never_arrive_fails_at_once(client, tmp_path, sessio
"""`stream_hls_file` is a SYNC route: it runs on the ~40-slot threadpool and holds a pooled DB
connection while it waits. It used to poll out the whole 20 s for a name that could never appear
— an ordinary seek past the produced region was enough to park a worker and a connection, and
forty hopeless names parked the API (deep-review round 5). ffmpeg writes segments in order, so a
forty hopeless names parked the API. ffmpeg writes segments in order, so a
segment whose predecessor is absent is not the next one, and that is knowable at once — even
while ffmpeg is still running, which is the case `proc` alone cannot shorten."""
d = tmp_path / "m1_0_0_+0.00"
@@ -152,11 +152,11 @@ def test_a_segment_that_will_never_arrive_fails_at_once(client, tmp_path, sessio
def test_a_multi_audio_segment_beyond_the_region_fails_at_once_too(
client, tmp_path, sessions, name
):
"""The round-5 guard read `seg_<seq>_<variant>.ts`, but ffmpeg substitutes `%v` FIRST — so the
"""An earlier guard read `seg_<seq>_<variant>.ts`, but ffmpeg substitutes `%v` FIRST — so the
real names are `seg_<variant>_<seq>.ts` and every multi-audio request fell through to the full
20 s wait: the video rendition because its variant "0" parsed as segment 0, the audio renditions
(`a0`, `a1`) because they did not parse at all. Dual-audio is the common shape in this library,
so the guard was inert exactly where it was needed (round 6)."""
so the guard was inert exactly where it was needed."""
d = tmp_path / "m1_0_multi_+0.00"
d.mkdir()
(d / "seg_0_0.ts").write_bytes(b"x")
@@ -261,7 +261,7 @@ def test_the_name_ffmpeg_is_asked_for_is_a_name_the_guard_understands(tmp_path,
def test_the_argv_asks_for_exactly_the_names_the_guard_accepts(tmp_path):
"""The producer and the parser, pinned to each other through the argv itself.
Round 6's HIGH was a guard that read `seg_%v_%d.ts` sequence-first, so it was inert on every
The bug was a guard that read `seg_%v_%d.ts` sequence-first, so it was inert on every
multi-audio item, and a warm-up that waited 8 s for a name ffmpeg never writes. The fix was to
give the layout one spelling — but a comment saying so is what had just failed, so assert that
the `-hls_segment_filename` pattern in the real argv is literally what `segment_name` produces,
@@ -302,7 +302,7 @@ def test_a_name_only_the_other_argv_branch_writes_is_refused_at_once(tmp_path, c
it can never write — and vice versa. Accepting the union read them as "an HLS name, so wait and
see", and each one then held a threadpool worker and its pooled DB connection for the full
timeout: measured through this route against a live single-audio session, `master.m3u8` took
30.0 s and `seg_a0_0.ts` 20.1 s to answer 404 (deep-review round 7)."""
30.0 s and `seg_a0_0.ts` 20.1 s to answer 404."""
single = tmp_path / "m1_0_0_+0.00"
single.mkdir()
sessions[(VIEWER, "m1")] = _Session("sid-a", single, running=True)
+1 -1
View File
@@ -1,4 +1,4 @@
"""A card asks the Plex image proxy for a small cut; a detail page keeps the full one (E31 S4e).
"""A card asks the Plex image proxy for a small cut; a detail page keeps the full one.
Cards fetched the 400×600 poster for a ~144px picture and the 1280×720 backdrop for a ~212px shelf
picture. The `_sm` variants are cut by Plex at a card's size and cached apart from the full ones; an
+1 -1
View File
@@ -1,4 +1,4 @@
"""DB-backed test for the S3a data normalization (migration 0060): a JSONB scalar `null` in the
"""DB-backed test for the data normalization (migration 0060): a JSONB scalar `null` in the
plex tag columns becomes SQL NULL, while a real array is left untouched. Runs the migration's exact
UPDATE statements against seeded rows (the migration itself is a no-op in the empty test DB).
+1 -1
View File
@@ -1,4 +1,4 @@
"""E5 S5: the TMDB keyword filter. A keyword chip on the detail page sets ?keywords=<kw>, which
"""The TMDB keyword filter. A keyword chip on the detail page sets ?keywords=<kw>, which
narrows the unified library to rows whose persisted `keywords` JSONB array contains it. DB lane
(`db` fixture) — skipped without Postgres.
"""
+5 -5
View File
@@ -1,4 +1,4 @@
"""E5 S6: HU-overlay language serving. When ?lang=hu, the unified library, facets, genre filter and
"""HU-overlay language serving. When ?lang=hu, the unified library, facets, genre filter and
detail endpoints serve the Hungarian `*_hu` columns (falling back to EN where a row has no HU twin);
?lang=en (the default) serves English. Also covers the imdb_rating IMDb-only / audience-score split.
DB lane (`db` fixture) — skipped without Postgres.
@@ -119,7 +119,7 @@ def test_genre_filter_matches_active_language_vocabulary(db, monkeypatch, overri
def test_a_genre_filter_saved_in_english_still_matches_in_hungarian(db, monkeypatch, override_dep):
"""E30: the HU column holds one Hungarian word per genre. A filter saved while the HU list still
"""The HU column holds one Hungarian word per genre. A filter saved while the HU list still
offered "Action" is read through the same map, so it finds the "Akció" title instead of nothing."""
user = _seed(db)
resp = _get(
@@ -129,7 +129,7 @@ def test_a_genre_filter_saved_in_english_still_matches_in_hungarian(db, monkeypa
def test_a_genre_the_hungarian_list_offers_in_english_still_matches(db, monkeypatch, override_dep):
"""E30 review: a row the sync has not given Hungarian genres yet is listed under its English word
"""A row the sync has not given Hungarian genres yet is listed under its English word
(the facets read coalesce(genres_hu, genres)); filtering by that word must find it."""
user = _seed(db)
facets = _get(db, user, monkeypatch, override_dep, "/api/plex/facets?scope=movie&lang=hu")
@@ -161,7 +161,7 @@ def test_item_detail_language_and_rating_split(db, monkeypatch, override_dep):
body = resp.json()
assert body["title"] == "Magyar Cím"
assert body["summary"] == "Magyar összefoglaló."
assert body["tagline"] == "Magyar mottó." # E30: the HU overlay's tagline
assert body["tagline"] == "Magyar mottó." # the HU overlay's tagline
en = _get(db, user, monkeypatch, override_dep, "/api/plex/item/m1").json()
assert en["tagline"] == "English tagline."
# imdb_rating is IMDb-only (None here); the audience score is served separately as `rating`.
@@ -285,7 +285,7 @@ def _seed_rails(db):
def test_every_card_rail_serves_the_readers_language(db, monkeypatch, override_dep):
"""E30: the grid spoke Hungarian, but every other card surface (Discover rails, show page,
"""The grid spoke Hungarian, but every other card surface (Discover rails, show page,
search, collection, person page, playlist, Home) served the English title to a HU reader."""
user, pid = _seed_rails(db)
+1 -1
View File
@@ -1,4 +1,4 @@
"""S3: the "last watched" browse sort (`sort=lastwatched`). Movies rank by
"""The "last watched" browse sort (`sort=lastwatched`). Movies rank by
coalesce(progress_updated_at, watched_at); shows by max(coalesce(...)) across their episodes
(rollup.resumed_at). Never-watched titles sink to the bottom (nullslast). DB lane (`db` fixture) —
skipped without Postgres.
+5 -5
View File
@@ -1,4 +1,4 @@
"""DB-backed regression (E5 S5a): the HU-overlay sync writes Hungarian metadata onto the matched EN
"""DB-backed regression: the HU-overlay sync writes Hungarian metadata onto the matched EN
rows (paired by guid), never creating browsable HU rows, and overlay libraries are excluded from the
browse scope. Also covers the canonical-guid parse and that the generated search_vector_hu populates.
@@ -197,7 +197,7 @@ def test_overlay_reconcile_clears_stale_hu_when_twin_vanishes(db):
def test_a_listing_cut_short_keeps_the_hungarian_text_it_did_not_reach(db):
"""E32 S2: the reconcile clears HU text only after a COMPLETE listing. A walk a Plex hiccup cut
"""The reconcile clears HU text only after a COMPLETE listing. A walk a Plex hiccup cut
short (an empty page before the server's total), or an empty section, says nothing about the
titles it did not reach — clearing them dropped their Hungarian text until the next full sync."""
en_lib = PlexLibrary(plex_key="4", title="Movies", kind="movie")
@@ -304,7 +304,7 @@ class _KindStub:
def test_a_show_overlay_writes_hungarian_episode_titles_and_summaries(db):
"""E30: the HU TV section lists every episode in Hungarian; they were never read, so a Hungarian
"""The HU TV section lists every episode in Hungarian; they were never read, so a Hungarian
reader saw every episode title and summary in English."""
en_lib = PlexLibrary(plex_key="1", title="TV Shows", kind="show")
hu_lib = PlexLibrary(plex_key="9", title="Sorozatok", kind="show", metadata_lang="hu")
@@ -341,7 +341,7 @@ def test_a_show_overlay_writes_hungarian_episode_titles_and_summaries(db):
def test_the_overlay_gives_every_row_one_hungarian_word_per_genre(db):
"""E30: Plex's HU agent mixes "Action" with "Dráma", and a title with no HU twin kept only its
"""Plex's HU agent mixes "Action" with "Dráma", and a title with no HU twin kept only its
English list — so the HU genre list offered "Drama" beside "Dráma"."""
en_lib = PlexLibrary(plex_key="4", title="Movies", kind="movie")
hu_lib = PlexLibrary(plex_key="9", title="Filmek", kind="movie", metadata_lang="hu")
@@ -407,7 +407,7 @@ def test_a_twin_without_genres_keeps_the_rows_own_hungarian_list(db):
def test_every_synced_row_gets_hungarian_genres():
"""E30 review: not only an overlaid row — the HU facets and filter read genres_hu for all."""
"""Not only an overlaid row — the HU facets and filter read genres_hu for all."""
from app.plex.sync import _apply_facet_fields
row = PlexItem(title="Solo")
+1 -1
View File
@@ -1,4 +1,4 @@
"""plex/paths — the Plex-side path → local mount translation (E32 S2: 23% covered, and a wrong answer
"""plex/paths — the Plex-side path → local mount translation (it was 23% covered, and a wrong answer
here reads as "media file not found" for a whole library at once, with nothing else failing).
Pure logic: the admin map is read through sysconfig, stubbed per test.
+1 -1
View File
@@ -1,4 +1,4 @@
"""E30 S6: the people are made local ahead of time. The people_enrich job fetches linked people's TMDB
"""The people are made local ahead of time. The people_enrich job fetches linked people's TMDB
bios (the most-credited first) and caches their portraits resized through Plex; the person page says
when a bio is still on its way (`enriching`), so it can ask again instead of staying empty.
DB lane (Postgres); the portrait cache runs in a temp directory.
+2 -2
View File
@@ -1,4 +1,4 @@
"""DB-backed regression (E5 S4): GET /api/plex/person/{name} returns a cast/crew member's
"""DB-backed regression: GET /api/plex/person/{name} returns a cast/crew member's
filmography — movies + shows in the ENABLED libraries whose cast OR directors include the name,
each carrying the caller's watch-state, plus the cached headshot. Also covers `content_libs`
scoping (disabled libraries excluded) and the kind filter (episodes are not movies).
@@ -123,7 +123,7 @@ def test_person_detail_unknown_name_is_empty(db, monkeypatch, override_dep):
resp = _get(db, user, monkeypatch, override_dep, "/api/plex/person/Ghost%20Actor")
assert resp.status_code == 200, resp.text
body = resp.json()
# An unknown name has no plex_people row → no bio, no titles (E5 S6 added the bio fields, all null).
# An unknown name has no plex_people row → no bio, no titles (the bio fields are all null).
assert body == {
"name": "Ghost Actor",
"photo": None,
+2 -2
View File
@@ -1,4 +1,4 @@
"""E5 S6: person page TMDB bio enrichment. `parse_person` shapes the /person payload (bio + a
"""Person page TMDB bio enrichment. `parse_person` shapes the /person payload (bio + a
popularity-ranked, deduped `known_for`); `enrich_person` lazily fetches + persists once (guarded on a
key + a tmdb_person_id); and /person/{name} serves the bio + resolves `known_for` to library rows.
DB lane for the endpoint/enrich tests; parse_person is pure. Skipped without Postgres where DB-backed.
@@ -335,7 +335,7 @@ def test_person_detail_serves_bio_and_resolves_known_for(db, monkeypatch, overri
assert [c["title"] for c in j["known_for"]] == ["Known Movie"]
# --- E30 S4: the biography in the reader's language --------------------------------------------------
# --- the biography in the reader's language -------------------------------------------------------
# The same /person request appends `translations`; the Hungarian one is kept beside the instance-language
# biography, and a Hungarian reader is served it, falling back to the English one when TMDB has none.
+1 -1
View File
@@ -1,4 +1,4 @@
"""The playback-class filter (E7): filter the Plex feed by Direct Play / Direct Stream / Transcode·HW /
"""The playback-class filter: filter the Plex feed by Direct Play / Direct Stream / Transcode·HW /
Transcode·SW. Covers the shared classifier + Stream parsing (unit), the sync derivation + codec-change
reset, the bit-depth backfill, and the /library filter + /facets counts (DB lane).
+1 -1
View File
@@ -1,4 +1,4 @@
"""The playback-session route's mode routing (E6 S1b).
"""The playback-session route's mode routing.
`POST /stream/{rk}/session` is the single switch that decides how an item plays: a browser-native
file is served raw (`mode:"direct"`), everything else (re)starts an HLS session — and a
@@ -1,4 +1,4 @@
"""E28 S3e2 — putting removed Plex playlist rows back WHERE THEY WERE: the list page's "Undo".
"""Putting removed Plex playlist rows back WHERE THEY WERE: the list page's "Undo".
A removal can take one row, a season or a whole show, and a season's episodes need not sit together
(a reorder inside the show interleaves them), so each key carries its own index — over the ORDERED
+1 -1
View File
@@ -1,4 +1,4 @@
"""S1 characterization net for the Plex-package db.query() -> SQLAlchemy 2.0 select() conversion.
"""Characterization net for the Plex-package db.query() -> SQLAlchemy 2.0 select() conversion.
These lock the OBSERVABLE behaviour of the previously-untested hot query paths — the unified_library
union builder (+ its watch-state branches, ordering, search ranking, episode grouping), the rollup
+1 -1
View File
@@ -832,7 +832,7 @@ class TestProbeVideoKind:
class TestProbeExitStatus:
"""A probe whose ffprobe EXITS NON-ZERO is a failed probe, not an answer (E32). Each probe used to
"""A probe whose ffprobe EXITS NON-ZERO is a failed probe, not an answer. Each probe used to
read stdout without looking at the exit status, so a refusing ffprobe silently picked the fallback
— HW-safe, single audio, the requested offset — and skipped the warning its except branch promises.
Drives the REAL subprocess.run with the probe's own kwargs against a stand-in command that prints a
+1 -1
View File
@@ -3,7 +3,7 @@
They used to sit behind `_require_session`, the cookie-only gate the poster proxies use — a signed
cookie was enough, with none of `current_user`'s checks (is_active, is_suspended, the session epoch
that "log out everywhere" bumps). Fine for a poster; not for a route that starts reads of the media
library. Found by the 2026-09 bug-hunt deep-review; the gate itself pre-dates that round.
library. Found by a 2026-09 code review; the gate itself pre-dates that review.
"""
from fastapi.testclient import TestClient
+6 -6
View File
@@ -12,7 +12,7 @@ thread sat with it), and picking the second language cost the whole read again.
These tests pin the three things that fixes it: the request never waits for ffmpeg, the extraction
publishes a window around the playhead before the whole track, and one ffmpeg run covers every
embedded text track — plus what the deep-review of that fix found (cues doubled at the window/full
embedded text track — plus what the review of that fix found (cues doubled at the window/full
seam, a truncated pass cached as complete, a poll that respawned ffmpeg every second, a sidecar stuck
behind an unrelated pass). No Postgres (the auth half is in test_plex_subtitle_auth.py, which needs
the DB). Almost every test stubs ffmpeg out, but not all: a few run a real child process, and
@@ -316,7 +316,7 @@ def test_a_cached_vtt_from_an_older_build_counts_as_complete(tmp_path):
assert subtitles.read_meta(key) == ([[0.0, 1_000_000_000.0]], True)
# --- the deep-review round: the seams between the window and the full pass ----------------------
# --- the seams between the window and the full pass ---------------------------------------------
def test_a_window_and_the_full_pass_do_not_double_a_cue():
@@ -614,7 +614,7 @@ def test_an_absurd_position_is_rejected_before_it_reaches_ffmpeg(client, monkeyp
assert pool.jobs == []
# --- the second deep-review round ---------------------------------------------------------------
# --- failed passes, seeks, saturation and shutdown ----------------------------------------------
def _window_raises(monkeypatch):
@@ -865,7 +865,7 @@ def test_a_full_pass_resumed_mid_file_lines_up_on_a_real_file(tmp_path, monkeypa
assert merged.count("-->") == 30, "a cue across the join was kept twice or lost"
# --- the third deep-review round ----------------------------------------------------------------
# --- transient failures, read-ahead, memos and slot fairness ------------------------------------
def test_a_transient_failure_is_said_but_is_not_a_final_answer(monkeypatch):
@@ -1094,7 +1094,7 @@ def test_a_nul_rating_key_names_no_row():
assert by_rating_key(None, PlexItem, "12\x0034") is None # type: ignore[arg-type]
# --- the fourth deep-review round ---------------------------------------------------------------
# --- output that could not be kept, absurd keys -------------------------------------------------
def test_a_pass_whose_output_could_not_be_kept_is_backed_off_like_a_failed_one(monkeypatch):
@@ -1154,7 +1154,7 @@ def test_an_absurd_rating_key_or_ordinal_is_refused_not_a_500(client, monkeypatc
# ENAMETOOLONG rather than the real filesystem: on Linux pathlib re-raises it (the defect), but
# Windows reports ERROR_INVALID_NAME, which pathlib's `exists()` already swallows — so on a dev
# machine this assertion passed with `_exists` reverted and only the container lane was honest
# about it (deep-review round 5).
# about it.
def _too_long(self):
raise OSError(errno.ENAMETOOLONG, "File name too long")
+1 -1
View File
@@ -1,4 +1,4 @@
"""E3 S1 catalog-prune (B-sub-6): a sync now deletes rows for titles that disappeared from Plex,
"""Catalog prune: a sync now deletes rows for titles that disappeared from Plex,
diffing each section's freshly-listed rating_keys against the stored ones. The prune is GUARDED on a
non-empty listing so a transient Plex glitch (an empty page) can never wipe a whole library.
+1 -1
View File
@@ -1,4 +1,4 @@
"""DB-backed test for R6 S2's Plex per-section commit: a PlexError on a LATER library must keep the
"""DB-backed test for the Plex per-section commit: a PlexError on a LATER library must keep the
libraries already mirrored (committed per section), not roll the whole reconcile back. Uses the DB
lane (`db` fixture) with a faked PlexClient — skipped without Postgres.
"""
+3 -3
View File
@@ -1,4 +1,4 @@
"""TMDB enrichment engine (E5 S2): the pure parser + persister, guid→tmdb-id resolution, and the
"""TMDB enrichment engine: the pure parser + persister, guid→tmdb-id resolution, and the
DB-lane batch/serve integration (enrich stamps the watermark + links people; serve_block resolves
recommendations to library rows; the whole thing no-ops without an API key).
@@ -126,7 +126,7 @@ def test_parse_movie_pulls_all_fields():
},
] # the Gaffer is filtered; the two Coppola jobs survive (dedup is by name+job)
assert d["studios"] == ["Paramount", "Alfran"]
# E30: the code beside the English name, so the client can name the country in the reader's language.
# The code beside the English name, so the client can name the country in the reader's language.
assert d["production_countries"] == [{"code": "US", "name": "United States of America"}]
assert [r["tmdb_id"] for r in d["recommendations"]] == [240, 999]
assert d["backdrops"] == ["/bd1.jpg", "/bd2.jpg"]
@@ -432,7 +432,7 @@ def test_resolve_recs_matches_by_media_type_not_bare_id(db):
assert hit[500]["in_library"] is True and hit[500]["rating_key"] == "s500"
# ---- E30: countries by ISO code -----------------------------------------------------------------
# ---- countries by ISO code -----------------------------------------------------------------------
class FakeCountries(FakeTmdb):
+5 -5
View File
@@ -1,5 +1,5 @@
"""The daily full watch reconcile (`run_plex_watch_reconcile`) — the one pass that DELETES watch state:
a mirrored row Plex no longer has a record for is read as "Plex un-watched it" and cleared. E32 S2
a mirrored row Plex no longer has a record for is read as "Plex un-watched it" and cleared. A review
found it trusting a scan a Plex hiccup had cut short; these hold what it may and may not clear.
"""
@@ -149,7 +149,7 @@ _LISTED = [
def test_a_genuinely_empty_second_section_still_lets_the_reconcile_clear(db, monkeypatch, world):
# The server SAYS the shows section holds 0: that is a complete listing, not a hiccup — before
# the E32 review it read as "incomplete" forever and un-watches never synced again.
# a review it read as "incomplete" forever and un-watches never synced again.
state, rows = world
_local_states(state)
db.commit()
@@ -181,8 +181,8 @@ def test_a_section_we_mirror_that_says_0_keeps_its_rows_and_does_not_hold_up_the
db, monkeypatch, world
):
# A total of 0 from a section our items come from is a Plex that lost the library for a moment,
# not one that un-watched all of it (E32 review round 2): its rows stay. And it is decided per
# section: before round 3 one such section (a library emptied on Plex, its mirrored items kept)
# not one that un-watched all of it: its rows stay. And it is decided per
# section: at first one such section (a library emptied on Plex, its mirrored items kept)
# made EVERY run incomplete, and un-watches in every other section never synced again.
state, rows = world
_local_states(state)
@@ -196,7 +196,7 @@ def test_a_section_we_mirror_that_says_0_keeps_its_rows_and_does_not_hold_up_the
def test_a_library_the_allowlist_leaves_out_keeps_its_watch_states(db, monkeypatch, world):
# Its items are absent from every scan because the scan never walks it — not because Plex
# un-watched them. It cleared them on every run (E32 review round 2).
# un-watched them. It cleared them on every run.
state, _rows = world
other = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True)
db.add(other)
+3 -3
View File
@@ -1,4 +1,4 @@
"""S2 watch-sync semantics: Plex is the source of truth.
"""Watch-sync semantics: Plex is the source of truth.
Two independent pieces:
- `_pull_apply` (DB lane): on a genuine divergence for an item Plex knows about, Plex now WINS —
@@ -45,7 +45,7 @@ class _FakePlex:
def test_pull_plex_wins_over_diverging_synced_local_resume(db):
"""The core S2 change: a SYNCED local row (its state already reached Plex) that later diverges from
"""The core change: a SYNCED local row (its state already reached Plex) that later diverges from
Plex = a real external Plex-side change, so the incremental pull resolves to PLEX's position — Plex
is authoritative, no LWW. (A DIRTY local row is protected instead — see the dirty-guard test.)"""
item_id, user_id = _seed_item_and_user(db)
@@ -210,7 +210,7 @@ def test_pull_settles_flag_on_matching_dirty_row(db):
def test_movie_probe_flips_stale_in_progress_to_watched(db):
"""S1 core: a local in-progress MOVIE that Plex has since finished (viewCount>0) and dropped from
"""Core: a local in-progress MOVIE that Plex has since finished (viewCount>0) and dropped from
Continue-Watching flips to watched on the incremental pass — without waiting for the daily reconcile.
This is the "movie stuck in progress after scrubbing the credits" bug."""
item_id, user_id = _seed_item_and_user(db)
+1 -1
View File
@@ -1,4 +1,4 @@
"""The Python major is a contract, not a coincidence — the backend half (E27 S3).
"""The Python major is a contract, not a coincidence — the backend half.
`0.93.2` was released to fix exactly this bug on the JavaScript side: four machines built or checked
the app and none of them agreed on which Node they ran, the server gate went red for 73 of its last
+1 -1
View File
@@ -1,4 +1,4 @@
"""DB-backed tests for R6 S2 quota transaction ownership: quota accounting writes in its OWN
"""DB-backed tests for quota transaction ownership: quota accounting writes in its OWN
session, so a spend is DURABLE even when the calling job rolls back (the units were really consumed
at YouTube), and it NEVER commits the caller's session (the old behaviour flushed a job's partial
state mid-run behind its own rollback). Uses the DB lane (`db` fixture) — skipped without Postgres.
+2 -2
View File
@@ -1,4 +1,4 @@
"""R6 S1b: the request models turn payloads that used to 500 into clean validation errors,
"""The request models turn payloads that used to 500 into clean validation errors,
and PATCH models distinguish an absent field from an explicit null. Pure-logic (no DB) — it
exercises the Pydantic models the route modules define, not the endpoints."""
@@ -51,7 +51,7 @@ def test_admin_quota_rejects_non_numeric_limit():
def test_progress_accepts_fractional_seconds():
# The player may post a raw currentTime (a float). The field must accept it (truncated in the
# handler) — a plain `int` field would 422 on the progress hot path. (code-review round 1.)
# handler) — a plain `int` field would 422 on the progress hot path.
assert VideoProgressIn(position_seconds=12.7).position_seconds == 12.7
assert int(VideoProgressIn(position_seconds=12.7).position_seconds) == 12
with pytest.raises(ValidationError):
+1 -1
View File
@@ -1,4 +1,4 @@
"""What a dead email link says, and what it costs to ask (E9.5 S4 follow-up), DB-lane.
"""What a dead email link says, and what it costs to ask, DB-lane.
Reported 2026-09-05: delete an account, then open its password-reset link. The card drew a full
"set a new password" form — because a `#reset=` fragment was present, nothing more — and only after
+2 -2
View File
@@ -1,7 +1,7 @@
"""No test sends a request and throws the response away (E32 S2).
"""No test sends a request and throws the response away.
A discarded `client.post(...)` passes whatever it got back. As a SETUP step, a failure leaves the
test asserting on a state it never reached; as the step UNDER test, the test proves nothing. E32
test asserting on a state it never reached; as the step UNDER test, the test proves nothing. We
found `test_youtube_sources_never_consult_the_guard` passing on a 400 that stopped the request
before the code it meant to exercise, and 53 more discarded responses across the suite. Each one
now states the status it expects; this keeps it that way.
+5 -5
View File
@@ -1,10 +1,10 @@
"""E9.5 review round — the invariants the epic's own review found unpinned.
"""Review round — the invariants the canonical-email work's own review found unpinned.
Every test here corresponds to a defect a reviewer could still introduce with the whole suite green:
a mail whose body never renders in any test, an activation that only works for the exact spelling, a
"we already wrote to them" stamp for a mail that was dropped, an env allow-list that answers a
different question than the gate it backs, and the address pre-emption that E9.5 S1's argument
covered only while the address stayed unverified.
different question than the gate it backs, and the address pre-emption that the order-independent
gates' argument covered only while the address stayed unverified.
"""
from datetime import datetime, timedelta, timezone
@@ -79,7 +79,7 @@ def test_every_mail_renders_in_every_language(wire, name, args, lang):
def test_a_weeks_link_is_described_in_days_not_hours(wire):
# The S6 approval token lives seven days, and the mail used to say "expires in 168 hours".
# The approval token lives seven days, and the mail used to say "expires in 168 hours".
email_mod.send_access_approved("to@example.com", "en", "https://x/#verify=t", True, 168)
assert "7 days" in wire[0][2] and "168" not in wire[0][2]
@@ -91,7 +91,7 @@ def test_a_weeks_link_is_described_in_days_not_hours(wire):
def test_a_later_registration_never_touches_an_existing_password(db, client, smtp_on, wire):
"""The rule E9.5's review round 2 put back, having tried the opposite and found it worse.
"""The rule a review put back, having tried the opposite and found it worse.
"While the address is unverified, the newest registration owns it" looked like a fix for address
pre-emption. It is symmetric — an attacker can always be last — so it closed nothing, and it
+1 -1
View File
@@ -70,7 +70,7 @@ def test_duplicate_name_within_a_source_conflicts(db, monkeypatch, override_dep)
def test_a_feed_views_tag_pick_must_be_a_short_list_of_ids(db, monkeypatch, override_dep):
"""The autotag job reads every view's tag pick (E32 review): the route holds it to the shape the
"""The autotag job reads every view's tag pick: the route holds it to the shape the
feed sends, on create and on update."""
user = _user(db)
c = _client(db, user, monkeypatch, override_dep)
+3 -3
View File
@@ -28,7 +28,7 @@ def test_claim_is_exclusive_until_released():
_release()
# ---- E30: the result as data -------------------------------------------------------------------
# ---- the result as data --------------------------------------------------------------------------
def _snapshot_job(job_id):
@@ -70,7 +70,7 @@ def test_a_failed_run_drops_the_previous_result(db):
def test_a_job_runs_audit_row_carries_its_result_as_data(db):
"""E30 review: the audit log names a run's result in the reader's language, as the inbox does —
"""The audit log names a run's result in the reader's language, as the inbox does —
from data, not from the English one-liner (a nested phase there was a Python dict repr)."""
from app.models import AuditLog, User
@@ -89,7 +89,7 @@ def test_a_job_runs_audit_row_carries_its_result_as_data(db):
def test_a_failure_the_job_names_rides_along_as_data(db, make_user):
"""E31: a failed RSS poll reached the Hungarian audit log as the server's English sentence. A
"""A failed RSS poll reached the Hungarian audit log as the server's English sentence. A
failure the job names (`progress.JobFailed`) is stored as data beside that sentence — on the
dashboard, the manual-run notice and the audit row — so the client says it in the reader's
language. Any other failure keeps only its own text (the test above)."""
+2 -2
View File
@@ -1,4 +1,4 @@
"""youtube/search_scrape — the zero-quota InnerTube search (E32 S2: the module was 20% covered).
"""youtube/search_scrape — the zero-quota InnerTube search (the module was 20% covered).
Everything here breaks silently when YouTube moves: a parser that stops finding videos, a cursor that
stops paging, a fallback key that outlives its usefulness. The network is an httpx MockTransport.
@@ -168,7 +168,7 @@ def test_the_live_key_is_fetched_once_and_cached(net):
def test_a_failed_key_fetch_falls_back_for_now_and_tries_again_next_time(net, results_html):
"""The fallback pair used to be CACHED for the life of the process: one failed fetch pinned
every later search to a hard-coded key, and once that went stale search returned nothing,
with no error anywhere (E32 S2)."""
with no error anywhere."""
net["handler"] = _serve(results_html=results_html)
search_scrape.search("a")
assert net["calls"][-1].url.params["key"] == search_scrape._FALLBACK_KEY
+1 -1
View File
@@ -1,4 +1,4 @@
"""E12 · S5 — the admin-issued set-password link.
"""The admin-issued set-password link.
One mechanism answering three things: SEC-15 (a no-SMTP instance auto-verifies every registration, so
a pre-empted address cannot be recovered by mail), SEC-16 (a pre-empted address stays takeable until
+1 -1
View File
@@ -1,4 +1,4 @@
"""youtube/shorts.probe_is_short — three answers, and the third is not the second (E32 S2: 33% covered).
"""youtube/shorts.probe_is_short — three answers, and the third is not the second (was 33% covered).
A Short answers 200 at /shorts/<id>; a regular video redirects to /watch. Anything else — an error
page, a network failure — is "undetermined, ask again", never "not a Short": that would file a Short
+1 -1
View File
@@ -7,7 +7,7 @@ route registration or import fails right here.
Endpoints that actually answer (/healthz, /api/version, the auth/feed/downloads routers) all touch
the database, so exercising THEM belongs in a DB-backed integration lane (needs a test Postgres +
migrations) — deferred; see siftlode-ops/ROADMAP.md R2 S2b.
migrations) — deferred.
"""
from fastapi.testclient import TestClient
+1 -1
View File
@@ -1,4 +1,4 @@
"""E22.5 S3 — every swallowed exception has been ruled on.
"""Every swallowed exception has been ruled on.
A handler that neither logs nor re-raises is a decision: someone looked at it and decided the
failure is not worth a line. 114 of them were in `app/` at the start of this epic, and almost none
+1 -1
View File
@@ -227,7 +227,7 @@ class TestRssPollResult:
with pytest.raises(RuntimeError, match="HTTP 404") as err:
self._run(monkeypatch, fetch=dead, apply_=lambda db, ch, e: 1, channels=("UC_dead",))
# E31: and as data, for the reader's language — whose feed, and the reason code that tells a
# And as data, for the reader's language — whose feed, and the reason code that tells a
# dead feed (`http_404`) from a dead egress (`transport`).
assert isinstance(err.value, progress.JobFailed)
assert (err.value.reason, err.value.params) == (

Some files were not shown because too many files have changed in this diff Show More