Files
siftlode/backend/tests/test_sysconfig_registry.py
T
peter e995c664c6 perf(db): the channel manager and the feed sidebar read an index, not the video heap
GET /api/channels summed every subscribed channel's videos by reading each video's heap page
(681 MB on prod): 51-61 ms warm, but 2.25 s on a first open and 6.4 s under EXPLAIN once the
table had left memory. /feed/summary and /feed/years did the same (~140 ms warm, ~800 ms cold).
Migration 0095 replaces ix_videos_channel_pub_desc with the same key INCLUDING the columns
those queries read, so they are Index Only Scans over a 25 MB index. A test holds the queries
and the index's column list together.

The admin Configuration page did a db.get per setting (two for an unset one): it now reads
every override in one query (~40 ms of ~55 on prod).
2026-10-02 03:16:37 +02:00

115 lines
4.6 KiB
Python

"""The sysconfig registry: a group's keys stay together, every group has its section, and a closed-set
knob is a CHOICE.
The admin Configuration page used to keep its own hand-written group order, which had already drifted
(`explore` was missing, so its tab sorted last). The page now follows the registry: SECTIONS orders the
groups under their headings (E31), and a group missing there would fall off the page — so each group
of SPECS sits in exactly one section.
A closed-set admin knob is a CHOICE, not free text (E22 S2 follow-up).
`log_level`, `search_source`, `download_layout` and the transcode `hw`/`preset` used to be free-text
fields: the admin could type anything and every consumer carried its own `or "auto").lower()` guess
at what a typo meant. Now the value set is a `Literal` in config.py, pydantic holds the env to it at
boot, sysconfig holds a stored override to it, and the admin page renders a picker from the same list.
The DB lane (`db` fixture) covers the store; the rest is pure logic.
"""
import pytest
from pydantic import ValidationError
from app import sysconfig
from app.config import Settings, choices_of
CHOICE_KEYS = {s.key for s in sysconfig.SPECS if s.type == "choice"}
def test_each_group_is_one_run_of_keys():
runs = [
s.group
for i, s in enumerate(sysconfig.SPECS)
if i == 0 or sysconfig.SPECS[i - 1].group != s.group
]
assert len(runs) == len(set(runs)), runs
assert runs[:2] == ["access", "email"]
def test_every_group_sits_in_exactly_one_section():
listed = [g for _, members in sysconfig.SECTIONS for g in members]
assert len(listed) == len(set(listed)), listed
assert set(listed) == {s.group for s in sysconfig.SPECS}
assert [sid for sid, _ in sysconfig.SECTIONS] == ["access", "youtube", "media", "system"]
def test_every_closed_set_knob_is_a_choice_whose_options_come_from_config():
assert CHOICE_KEYS == {
"search_source",
"download_layout",
"log_level",
"plex_transcode_hw",
"plex_transcode_preset",
}
for s in sysconfig.SPECS:
field = Settings.model_fields[s.default_attr]
if s.type == "choice":
assert s.choices, s.key
assert field.default in s.choices, s.key
else:
# A Literal-typed setting exposed as "str" would be a free-text field again.
assert s.choices == () and choices_of(field.annotation) == (), s.key
def test_the_env_accepts_any_letter_case_and_rejects_an_unknown_value():
assert Settings(log_level="debug").log_level == "DEBUG"
assert Settings(search_source=" API ").search_source == "api"
with pytest.raises(ValidationError):
Settings(log_level="verbose")
class _Row:
def __init__(self, key, value):
self.key, self.value, self.encrypted = key, value, False
class _FakeDb:
"""Just enough of a Session for sysconfig's reads: one stored override row per key."""
def __init__(self, **rows):
self.rows = {k: _Row(k, v) for k, v in rows.items()}
def get(self, _model, key):
return self.rows.get(key)
def scalars(self, _stmt):
# describe() reads every override at once (E31 S4d).
return list(self.rows.values())
def test_a_stored_value_reads_back_canonical_and_a_stale_one_falls_back_to_the_default():
assert sysconfig.get_str(_FakeDb(plex_transcode_hw="OFF"), "plex_transcode_hw") == "off"
# A value that is no longer (or never was) a choice is not passed through to the consumer.
assert sysconfig.get_str(_FakeDb(plex_transcode_hw="maybe"), "plex_transcode_hw") == "auto"
def test_describe_hands_the_picker_its_options():
items = {i["key"]: i for g in sysconfig.describe(_FakeDb())["groups"].values() for i in g}
assert items["log_level"]["choices"] == ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]
assert items["log_level"]["value"] == "INFO"
assert items["quota_daily_budget"]["choices"] == []
def test_describe_lists_every_group_under_its_section_in_order():
d = sysconfig.describe(_FakeDb())
assert [s["id"] for s in d["sections"]] == ["access", "youtube", "media", "system"]
assert d["sections"][0]["groups"] == ["access", "google", "email"]
listed = [g for s in d["sections"] for g in s["groups"]]
assert sorted(listed) == sorted(d["groups"])
def test_set_value_stores_the_canonical_spelling_and_refuses_anything_else(db):
sysconfig.set_value(db, "search_source", "API")
assert sysconfig.get_str(db, "search_source") == "api"
with pytest.raises(ValueError, match="scrape, api"):
sysconfig.set_value(db, "search_source", "bing")
db.rollback()