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:
@@ -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)."""
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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,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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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."""
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,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,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."""
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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,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,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,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.
|
||||
"""
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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,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,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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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.
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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.
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user