feat(config): job intervals live in sysconfig, every setting says when it applies, the unread SponsorBlock default goes
An interval override was a row of its own table beside the one store every other admin knob uses; it is now the `job_interval_<job>` sysconfig key (migration 0110 copies them over and drops the table). The Scheduler page stays its one editor: the Configuration page and the setup wizard neither list nor write those keys, since a write there would skip the reschedule. Each ConfigSpec now states `applies` (required, no default). Every key is `live`, held by a scan that finds no read at import time; a `restart` key is refused until the page can say so. A second scan requires a reader for every key on the Configuration page — `sponsorblock_default` had none since it was added, so it leaves the registry, the translations and the store.
This commit is contained in:
@@ -15,6 +15,9 @@ boot, sysconfig holds a stored override to it, and the admin page renders a pick
|
||||
The DB lane (`db` fixture) covers the store; the rest is pure logic.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -37,7 +40,8 @@ def test_each_group_is_one_run_of_keys():
|
||||
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 set(listed) | set(sysconfig.OWN_PAGE_GROUPS) == {s.group for s in sysconfig.SPECS}
|
||||
assert not set(listed) & set(sysconfig.OWN_PAGE_GROUPS)
|
||||
assert [sid for sid, _ in sysconfig.SECTIONS] == ["access", "youtube", "media", "system"]
|
||||
|
||||
|
||||
@@ -112,3 +116,67 @@ def test_set_value_stores_the_canonical_spelling_and_refuses_anything_else(db):
|
||||
with pytest.raises(ValueError, match="scrape, api"):
|
||||
sysconfig.set_value(db, "search_source", "bing")
|
||||
db.rollback()
|
||||
|
||||
|
||||
# --- when a changed value takes effect --------------------------------------------------------------
|
||||
|
||||
_APP = Path(__file__).resolve().parent.parent / "app"
|
||||
_READERS = {"get", "get_str", "get_int", "get_bool"}
|
||||
|
||||
|
||||
def _key_reads() -> list[tuple[str, str, bool]]:
|
||||
"""Every `sysconfig.get*(db, "<key>")` in the app outside the registry: (key, where, inside a
|
||||
function)."""
|
||||
found: list[tuple[str, str, bool]] = []
|
||||
|
||||
def walk(node: ast.AST, path: Path, in_fn: bool) -> None:
|
||||
for child in ast.iter_child_nodes(node):
|
||||
fn = in_fn or isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda))
|
||||
if (
|
||||
isinstance(child, ast.Call)
|
||||
and isinstance(child.func, ast.Attribute)
|
||||
and isinstance(child.func.value, ast.Name)
|
||||
and child.func.value.id == "sysconfig"
|
||||
and child.func.attr in _READERS
|
||||
and len(child.args) >= 2
|
||||
and isinstance(child.args[1], ast.Constant)
|
||||
and isinstance(child.args[1].value, str)
|
||||
):
|
||||
found.append((child.args[1].value, f"{path.name}:{child.lineno}", in_fn))
|
||||
walk(child, path, fn)
|
||||
|
||||
for path in _APP.rglob("*.py"):
|
||||
if path.name != "sysconfig.py":
|
||||
walk(ast.parse(path.read_text(encoding="utf-8")), path, False)
|
||||
return found
|
||||
|
||||
|
||||
def test_every_key_states_when_it_applies_and_none_needs_a_restart_yet():
|
||||
with pytest.raises(TypeError):
|
||||
sysconfig.ConfigSpec("k", "int", "quota", "quota_daily_budget") # pyright: ignore[reportCallIssue]
|
||||
# The Configuration page cannot tell the admin that a setting waits for a restart; a key that
|
||||
# does needs that first (a badge on it and a banner once it changed).
|
||||
assert {s.applies for s in sysconfig.SPECS} == {"live"}
|
||||
|
||||
|
||||
def test_a_live_key_is_read_where_it_is_used_never_at_import():
|
||||
reads = _key_reads()
|
||||
assert len(reads) > 50, len(
|
||||
reads
|
||||
) # the scan sees the reads (it would pass vacuously otherwise)
|
||||
assert [(k, at) for k, at, in_fn in reads if not in_fn] == []
|
||||
|
||||
|
||||
def test_every_key_on_the_configuration_page_has_a_reader():
|
||||
read = {k for k, _, _ in _key_reads()}
|
||||
page = {s.key for s in sysconfig.SPECS if s.group not in sysconfig.OWN_PAGE_GROUPS}
|
||||
assert sorted(page - read) == []
|
||||
|
||||
|
||||
def test_the_intervals_are_keys_of_the_scheduler_page_only():
|
||||
keys = {s.key for s in sysconfig.SPECS if s.group == "scheduler"}
|
||||
assert keys == {sysconfig.interval_key(j) for j in sysconfig.JOB_INTERVAL_DEFAULTS}
|
||||
assert all(len(k) <= 64 for k in keys) # system_config.key
|
||||
assert sysconfig.config_page_spec("job_interval_rss_poll") is None
|
||||
assert sysconfig.config_page_spec("quota_daily_budget") is not None
|
||||
assert "scheduler" not in sysconfig.describe(_FakeDb())["groups"]
|
||||
|
||||
Reference in New Issue
Block a user