mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 20:31:21 +00:00
lib/builtin_content.py (321 lines moved). server.py 2,418 -> 2,098.
The calibration/diagnostic sloppaks and the starter library: _copy_builtin_packs,
_write_builtin_pack, the two seed helpers, their source tables, and the seed marker.
━━━ THE ONE SIGNATURE CHANGE, AND WHY THE CARVE IS UNSAFE WITHOUT IT ━━━
server.py has:
def _feedBack_server_root() -> Path:
return Path(__file__).resolve().parent
That is correct IN server.py: the repo root in dev, resources/feedBack when bundled — the
tree that actually holds docs/ and data/.
Move that body into lib/ unchanged and it keeps working, silently, and returns lib/. There
is no docs/diagnostics under lib/, so every seed would find nothing, log "source missing"
at debug, and return. Nothing raises. Nothing fails. The starter library simply never
appears, and the calibration sloppak is never seeded — on a fresh install, in the field.
A verbatim move whose MEANING changed because __file__ did.
So this module cannot compute a root: `server_root` is a PARAMETER, and server.py — the
only place that legitimately knows where it lives — passes it in. The trap is now
structurally impossible rather than merely avoided. (_copy_builtin_packs already took the
root that way; the two seed helpers now do too.)
Everything else is byte-identical. CONFIG_DIR is read late as appstate.config_dir and the
DLC root through dlc_paths._get_dlc_dir — the same seam every router in lib/routers/ uses,
late-bound because tests monkeypatch it.
━━━ PYFLAKES FOUND THREE MISSING IMPORTS THE TESTS WOULD HAVE FOUND ONE AT A TIME ━━━
The moved code uses `secrets`, `stat` and `tempfile`; none was in my import block. Each is
a NameError on a live path. `python3 -m pyflakes` names all three in one shot — this is the
Python twin of the no-undef gate that guarded every frontend carve, and it should run on
every server.py slice from here.
It also flagged a PRE-EXISTING one I deliberately did not touch: server.py's
TuningProviderRegistry.get_merged() calls `logger.exception(...)` in an except handler and
there is no `logger` in the module (it is `log`). So a raising tuning provider takes down
the merged-tunings call for everyone, with a NameError naming the wrong problem. Filed as
issue #899 rather than smuggled into a carve whose whole value is being behaviour-neutral.
The constants lost their underscore prefix: they cross a module boundary now (the seed
tests read them), so `_BUILTIN_STARTER_SOURCES` was a lie.
pytest 2397, pyflakes 0, Codex 0. Guarded by the plugin_context contract test (#898).
Refs #48
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
236 lines
9.3 KiB
Python
236 lines
9.3 KiB
Python
"""Tests for one-time builtin starter-content seeding into DLC."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
|
|
import builtin_content
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture()
|
|
def server_mod(tmp_path, monkeypatch, isolate_logging):
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
|
|
(tmp_path / "config").mkdir()
|
|
monkeypatch.delenv("DLC_DIR", raising=False)
|
|
sys.modules.pop("server", None)
|
|
mod = importlib.import_module("server")
|
|
yield mod
|
|
|
|
|
|
def _source(server_mod):
|
|
return (
|
|
server_mod._feedBack_server_root()
|
|
/ builtin_content.BUILTIN_STARTER_SOURCES[0][1]
|
|
)
|
|
|
|
|
|
def _dest(server_mod, dlc):
|
|
return (
|
|
dlc
|
|
/ builtin_content.BUILTIN_STARTER_SUBDIR
|
|
/ builtin_content.BUILTIN_STARTER_SOURCES[0][0]
|
|
)
|
|
|
|
|
|
def test_seed_creates_starter_content_and_marker(tmp_path, server_mod):
|
|
"""First run copies the bundled feedpak into starter/ and writes the marker."""
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
source = _source(server_mod)
|
|
if not source.is_file():
|
|
pytest.skip(f"starter source not present in checkout: {source}")
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
dest = _dest(server_mod, dlc)
|
|
assert dest.is_file()
|
|
assert dest.stat().st_size == source.stat().st_size
|
|
assert (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).is_file()
|
|
|
|
|
|
def test_seed_preserves_source_mtime(tmp_path, server_mod):
|
|
"""The seeded pack keeps the bundle's mtime so the diagnostic refresh check
|
|
(source newer than dest -> update) stays correct across both write paths."""
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
source = _source(server_mod)
|
|
if not source.is_file():
|
|
pytest.skip(f"starter source not present in checkout: {source}")
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
assert _dest(server_mod, dlc).stat().st_mtime_ns == source.stat().st_mtime_ns
|
|
|
|
|
|
def test_starter_is_not_carved_out_of_the_library():
|
|
"""`starter/` must NOT collide with the diagnostics/tutorials carve-out —
|
|
otherwise seeded songs would never appear in the library listing."""
|
|
assert "starter" not in {"diagnostics-builtin", "tutorials-builtin"}
|
|
|
|
|
|
def test_seed_runs_only_once_and_respects_deletion(tmp_path, server_mod):
|
|
"""After the first seed, deleting the song does NOT bring it back: the
|
|
marker makes starter seeding a one-time welcome."""
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
source = _source(server_mod)
|
|
if not source.is_file():
|
|
pytest.skip(f"starter source not present in checkout: {source}")
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
dest = _dest(server_mod, dlc)
|
|
assert dest.is_file()
|
|
|
|
# User removes the starter song.
|
|
dest.unlink()
|
|
|
|
# A subsequent launch must not re-seed it.
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
assert not dest.exists()
|
|
|
|
|
|
def test_seed_deferred_until_dlc_configured(tmp_path, server_mod):
|
|
"""With no DLC folder, seeding is skipped WITHOUT writing the marker, so it
|
|
retries once a library folder exists."""
|
|
source = _source(server_mod)
|
|
if not source.is_file():
|
|
pytest.skip(f"starter source not present in checkout: {source}")
|
|
|
|
# dlc is None and DLC_DIR unset -> _get_dlc_dir() returns None.
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), None)
|
|
assert not (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).exists()
|
|
|
|
# Now a DLC is configured: the deferred seed runs.
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
assert _dest(server_mod, dlc).is_file()
|
|
assert (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).is_file()
|
|
|
|
|
|
def test_seed_refuses_symlinked_seed_directory(tmp_path, server_mod):
|
|
"""A symlinked starter/ dir is refused so copies can't escape the DLC tree."""
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
source = _source(server_mod)
|
|
if not source.is_file():
|
|
pytest.skip(f"starter source not present in checkout: {source}")
|
|
|
|
outside_dir = tmp_path / "outside"
|
|
outside_dir.mkdir()
|
|
(dlc / builtin_content.BUILTIN_STARTER_SUBDIR).symlink_to(outside_dir)
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
assert list(outside_dir.iterdir()) == []
|
|
# An incomplete seed must NOT write the marker, so a later launch retries.
|
|
assert not (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).exists()
|
|
|
|
|
|
def test_seed_never_overwrites_an_existing_user_file(tmp_path, server_mod):
|
|
"""One-time starter seeding must never replace a user's own file at the
|
|
destination, even if the bundled pack has a newer mtime."""
|
|
import os as _os
|
|
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
dest = _dest(server_mod, dlc)
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
dest.write_bytes(b"user's own edited pack")
|
|
_os.utime(dest, (1_000_000, 1_000_000)) # far older than the bundled source
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
assert dest.read_bytes() == b"user's own edited pack" # untouched
|
|
# counted as already-present, so the one-time seed considers itself done
|
|
assert (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).is_file()
|
|
|
|
|
|
def test_seed_does_not_mark_when_destination_is_a_directory(tmp_path, server_mod):
|
|
"""A directory sitting at the destination name is neither clobbered nor
|
|
counted as present, so the marker stays unwritten and seeding retries."""
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
source = _source(server_mod)
|
|
if not source.is_file():
|
|
pytest.skip(f"starter source not present in checkout: {source}")
|
|
|
|
bogus = _dest(server_mod, dlc)
|
|
bogus.parent.mkdir(parents=True, exist_ok=True)
|
|
bogus.mkdir() # user (or junk) placed a directory where the pack goes
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
assert bogus.is_dir() # untouched
|
|
assert not (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).exists()
|
|
|
|
|
|
def test_seed_does_not_mark_when_source_missing(tmp_path, server_mod, monkeypatch):
|
|
"""If a starter source can't be found, the marker stays unwritten and the
|
|
seed is retried on the next launch (rather than permanently skipped)."""
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
monkeypatch.setattr(
|
|
builtin_content,
|
|
"BUILTIN_STARTER_SOURCES",
|
|
[("missing.feedpak", "content/starter/does-not-exist.feedpak")],
|
|
)
|
|
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
assert not (dlc / builtin_content.BUILTIN_STARTER_SUBDIR / "missing.feedpak").exists()
|
|
assert not (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).exists()
|
|
|
|
|
|
def test_every_starter_source_file_is_present(server_mod):
|
|
"""Every entry in _BUILTIN_STARTER_SOURCES must have its bundled file on
|
|
disk — otherwise the all-present gate never fires and NOTHING seeds (a
|
|
listed-but-missing pack silently disables starter seeding entirely). In CI
|
|
the checkout is clean, so "on disk" == committed."""
|
|
root = server_mod._feedBack_server_root()
|
|
missing = [
|
|
rel for _, rel in builtin_content.BUILTIN_STARTER_SOURCES
|
|
if not (root / rel).is_file()
|
|
]
|
|
assert not missing, f"listed starter sources missing on disk: {missing}"
|
|
|
|
|
|
def test_seed_lands_every_listed_starter_pack(tmp_path, server_mod):
|
|
"""A real seed run copies every listed pack into starter/ and marks done."""
|
|
root = server_mod._feedBack_server_root()
|
|
for _, rel in builtin_content.BUILTIN_STARTER_SOURCES:
|
|
if not (root / rel).is_file():
|
|
pytest.skip(f"starter source not present in checkout: {rel}")
|
|
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
builtin_content.seed_builtin_starter_content(server_mod._feedBack_server_root(), dlc)
|
|
|
|
for dest_name, _ in builtin_content.BUILTIN_STARTER_SOURCES:
|
|
dest = dlc / builtin_content.BUILTIN_STARTER_SUBDIR / dest_name
|
|
assert dest.is_file(), f"pack not seeded: {dest_name}"
|
|
assert (server_mod.CONFIG_DIR / builtin_content.STARTER_SEED_MARKER).is_file()
|
|
|
|
|
|
def test_no_unlisted_starter_pack_on_disk(server_mod):
|
|
"""The inverse guard: every content/starter/*.feedpak on disk must be wired
|
|
into _BUILTIN_STARTER_SOURCES. An unlisted pack bundles into builds as dead
|
|
weight and never seeds — exactly how the raw Ode-to-Joy pack slipped onto
|
|
main before being wired up. In CI the checkout is clean, so this flags any
|
|
stray/committed pack that isn't listed."""
|
|
root = server_mod._feedBack_server_root()
|
|
listed = {rel for _, rel in builtin_content.BUILTIN_STARTER_SOURCES}
|
|
if not listed:
|
|
pytest.skip("no starter sources declared")
|
|
content_dir = (root / next(iter(listed))).parent # all sources share this dir
|
|
if not content_dir.is_dir():
|
|
pytest.skip(f"starter content dir absent: {content_dir}")
|
|
on_disk = {p.relative_to(root).as_posix() for p in content_dir.glob("*.feedpak")}
|
|
unlisted = on_disk - listed
|
|
assert not unlisted, (
|
|
"committed but not in _BUILTIN_STARTER_SOURCES (would bundle as dead "
|
|
f"weight and never seed): {sorted(unlisted)}"
|
|
)
|