feedBack/tests/test_songmeta.py
ChrisBeWithYou 7ef52cdd66
fix: Edit Metadata persists into .feedpak files (suffix gate predated the rename) (#725)
write_song_metadata dispatched zip-form packages on `suffix == ".sloppak"`
only, while core reads both suffixes everywhere else (sloppak.SONG_EXTS,
.feedpak being the current write extension). Editing a zip-form .feedpak's
title/artist/album/year therefore silently fell back to a DB-only update,
which looked fine until the next full library rescan re-derived metadata
from the file and reverted the edit — the exact failure this module exists
to prevent. Directory-form packages were unaffected (manifest-presence
dispatch, not suffix).

Gate on SONG_EXTS, add TestWriteSongMetadata regression coverage (both zip
suffixes, mixed-case suffix, directory form, unknown-suffix fallback), and
correct the stale scan_worker comment claiming .sloppak-suffix-only
detection (the code already accepts both via is_sloppak).


Claude-Session: https://claude.ai/code/session_01H1ZBEcZoJinde9ms5fAjwc

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:51:57 +02:00

121 lines
4.2 KiB
Python

"""Unit tests for lib/songmeta.py — metadata file-persistence helpers."""
from __future__ import annotations
import zipfile
import yaml
# songmeta lives in lib/ which is on PYTHONPATH via conftest / pyproject
from songmeta import (
_apply_to_sloppak_manifest,
_coerce_year,
write_song_metadata,
)
def _make_zip_pak(path, manifest: dict) -> None:
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("manifest.yaml", yaml.safe_dump(manifest))
z.writestr("arrangements/lead.json", "{}")
def _read_zip_manifest(path) -> dict:
with zipfile.ZipFile(path) as z:
return yaml.safe_load(z.read("manifest.yaml"))
class TestCoerceYear:
def test_int_passthrough(self):
assert _coerce_year(2024) == 2024
def test_string_digit(self):
assert _coerce_year("2024") == 2024
def test_empty_string_clears(self):
# A user clearing the year field sends "" — must map to 0 so the
# scanner reads it back as "" (str(0 or "") == "").
assert _coerce_year("") == 0
def test_none_clears(self):
assert _coerce_year(None) == 0
def test_non_numeric_string_clears(self):
assert _coerce_year("unknown") == 0
def test_zero_passthrough(self):
assert _coerce_year(0) == 0
class TestApplyToSloppakManifest:
def test_set_title(self):
manifest = {"title": "Old Title", "artist": "A"}
dirty = _apply_to_sloppak_manifest(manifest, {"title": "New Title"})
assert dirty
assert manifest["title"] == "New Title"
assert manifest["artist"] == "A" # untouched
def test_set_year(self):
manifest = {"year": 2000}
dirty = _apply_to_sloppak_manifest(manifest, {"year": "2024"})
assert dirty
assert manifest["year"] == 2024
def test_clear_year(self):
"""Clearing year (empty string) must write 0, not be silently skipped."""
manifest = {"year": 2020}
dirty = _apply_to_sloppak_manifest(manifest, {"year": ""})
assert dirty, "clearing year should mark manifest dirty"
assert manifest["year"] == 0
def test_no_year_key_leaves_existing(self):
"""If 'year' is not in fields at all, the existing value is preserved."""
manifest = {"year": 2020}
dirty = _apply_to_sloppak_manifest(manifest, {"title": "T"})
assert dirty
assert manifest["year"] == 2020 # untouched
def test_empty_fields(self):
manifest = {"title": "T"}
dirty = _apply_to_sloppak_manifest(manifest, {})
assert not dirty
class TestWriteSongMetadata:
"""Suffix dispatch — zip-form packages are writable under BOTH the current
``.feedpak`` suffix and the legacy ``.sloppak`` one. The ``.feedpak`` gate
was missed when the format was renamed, silently downgrading Edit Metadata
to a DB-only update that a full rescan reverts."""
def test_zip_feedpak_persists(self, tmp_path):
pak = tmp_path / "song.feedpak"
_make_zip_pak(pak, {"title": "Old", "artist": "A"})
assert write_song_metadata(pak, {"title": "New"})
assert _read_zip_manifest(pak)["title"] == "New"
def test_zip_sloppak_persists(self, tmp_path):
pak = tmp_path / "song.sloppak"
_make_zip_pak(pak, {"title": "Old", "artist": "A"})
assert write_song_metadata(pak, {"title": "New"})
assert _read_zip_manifest(pak)["title"] == "New"
def test_zip_suffix_case_insensitive(self, tmp_path):
pak = tmp_path / "song.FeedPak"
_make_zip_pak(pak, {"title": "Old"})
assert write_song_metadata(pak, {"title": "New"})
assert _read_zip_manifest(pak)["title"] == "New"
def test_directory_form_persists(self, tmp_path):
pak = tmp_path / "song.feedpak"
pak.mkdir()
(pak / "manifest.yaml").write_text(
yaml.safe_dump({"title": "Old"}), encoding="utf-8"
)
assert write_song_metadata(pak, {"title": "New"})
got = yaml.safe_load((pak / "manifest.yaml").read_text(encoding="utf-8"))
assert got["title"] == "New"
def test_unknown_suffix_returns_false(self, tmp_path):
f = tmp_path / "song.txt"
f.write_text("not a package")
assert not write_song_metadata(f, {"title": "New"})