soulsync/tests/quality/test_lossless.py
BoulderBadgeDad 8aa8fcb94a test: drift guard — frontend lossless lists must match backend LOSSLESS_FORMATS (#941)
the frontend keeps its own copy of the lossless set (settings.js RT_LOSSLESS_FORMATS + the
index.html quality-profile dropdown) — runtime-fetching a yearly-changing list from the backend
isn't worth the coupling. but that duplication IS the exact root cause of #941 (a format added
to one place, not another). so instead of unifying, pin it: two tests parse the frontend lists
and assert they match the backend LOSSLESS_FORMATS. adding a new lossless format now fails CI
until it's added everywhere, instead of silently shipping a half-wired feature.

verified the guard catches drift (not a tautology): a simulated backend-only 'ape' addition
makes the equality fail. 18 lossless tests green, ruff clean.
2026-06-28 12:18:57 -07:00

163 lines
6.7 KiB
Python

"""The canonical "is this lossless?" seam + the lossy-copy overwrite invariant
(#941). All pure — no files, no ffmpeg — so the decision and the safety guard are
unit-testable in isolation. Both the import path (create_lossy_copy) and the Lossy
Converter repair job route through these, so the same rules drive both."""
from core.quality.lossless import (
LOSSLESS_FORMATS,
LOSSLESS_CANDIDATE_EXTENSIONS,
is_lossless_format,
is_lossless_audio_path,
lossy_output_would_overwrite_source,
)
from core.quality.model import AudioQuality
from core.repair_jobs.lossy_converter import _lossless_ext_where
# ── is_lossless_format ──
def test_lossless_formats_recognized():
for fmt in ('flac', 'alac', 'wav', 'dsf', 'FLAC', 'Dsf'):
assert is_lossless_format(fmt) is True
def test_lossy_formats_not_lossless():
for fmt in ('mp3', 'aac', 'ogg', 'opus', 'wma', 'unknown', '', None):
assert is_lossless_format(fmt) is False
# ── is_lossless_audio_path (the ambiguity is the whole point) ──
def test_unambiguous_extensions_decided_by_extension():
for path in ('/m/a.flac', '/m/a.wav', '/m/a.wave', '/m/a.aiff', '/m/a.aif',
'/m/a.dsf', '/m/a.dff', '/m/a.alac', '/m/A.FLAC'):
assert is_lossless_audio_path(path) is True
def test_lossy_extensions_are_not_lossless():
for path in ('/m/a.mp3', '/m/a.ogg', '/m/a.opus', '/m/a.wma', '/m/a.aac'):
assert is_lossless_audio_path(path) is False
def test_m4a_without_probe_is_not_lossless():
# The safe default: with no codec probe, an .m4a can't be proven lossless, so
# an AAC file is never misclassified as lossless and converted/deleted.
assert is_lossless_audio_path('/m/a.m4a') is False
assert is_lossless_audio_path('/m/a.mp4') is False
def test_m4a_alac_is_lossless_via_probe():
assert is_lossless_audio_path('/m/a.m4a', probe_codec=lambda _p: 'alac') is True
assert is_lossless_audio_path('/m/a.mp4', probe_codec=lambda _p: 'ALAC') is True
def test_m4a_aac_is_not_lossless_via_probe():
assert is_lossless_audio_path('/m/a.m4a', probe_codec=lambda _p: 'mp4a.40.2') is False
def test_probe_exception_is_not_lossless():
def _boom(_p):
raise RuntimeError("probe failed")
assert is_lossless_audio_path('/m/a.m4a', probe_codec=_boom) is False
# ── LOSSLESS_CANDIDATE_EXTENSIONS (the SQL pre-filter set) ──
def test_candidate_extensions_cover_lossless_plus_ambiguous():
for e in ('.flac', '.wav', '.aiff', '.dsf', '.dff', '.alac', '.m4a', '.mp4'):
assert e in LOSSLESS_CANDIDATE_EXTENSIONS
# raw lossy extensions must NOT be candidates
for e in ('.mp3', '.aac', '.ogg', '.opus', '.wma'):
assert e not in LOSSLESS_CANDIDATE_EXTENSIONS
def test_sql_where_clause_matches_candidates_only():
where = _lossless_ext_where('t.file_path')
assert "LIKE '%.flac'" in where and "LIKE '%.dsf'" in where and "LIKE '%.m4a'" in where
assert "LIKE '%.mp3'" not in where and "LIKE '%.aac'" not in where
# ── lossy_output_would_overwrite_source (the safety invariant) ──
def test_overwrite_detected_when_paths_equal():
assert lossy_output_would_overwrite_source('/m/Album/01.m4a', '/m/Album/01.m4a') is True
def test_overwrite_detected_after_normalization():
assert lossy_output_would_overwrite_source('/m/Album/../Album/01.m4a', '/m/Album/01.m4a') is True
def test_no_overwrite_for_different_extension():
assert lossy_output_would_overwrite_source('/m/Album/01.flac', '/m/Album/01.mp3') is False
assert lossy_output_would_overwrite_source('/m/Album/01.m4a', '/m/Album/01.mp3') is False
def test_overwrite_guard_handles_empty():
assert lossy_output_would_overwrite_source('', '/x.mp3') is False
assert lossy_output_would_overwrite_source('/x.flac', '') is False
# ── anti-drift: the seam must agree with the quality tier model ──
def test_lossless_set_consistent_with_tier_model():
"""If a format is in LOSSLESS_FORMATS it must out-rank every lossy format in
tier_score — guards against the two lists drifting apart (the whole reason
this seam exists)."""
lossy = ('mp3', 'aac', 'ogg', 'opus', 'wma')
worst_lossless = min(AudioQuality(f, bitrate=11290, sample_rate=44100, bit_depth=16).tier_score()
for f in LOSSLESS_FORMATS)
best_lossy = max(AudioQuality(f, bitrate=320).tier_score() for f in lossy)
assert worst_lossless > best_lossy
# ── regression: the exact bug class this guards (overwrite the original) ──
def test_regression_m4a_alac_to_aac_would_overwrite_and_is_blocked():
"""An .m4a ALAC source converted with the AAC codec lands on the SAME .m4a
path. The guard must catch it so ffmpeg -y never destroys the original."""
src = '/library/Sade/Diamond Life/01. Smooth Operator.m4a' # ALAC
out = src # AAC target → .m4a
assert is_lossless_audio_path(src, probe_codec=lambda _p: 'alac') is True
assert lossy_output_would_overwrite_source(src, out) is True # → callers skip
# ── cross-language drift guard: the frontend lossless lists must match the backend ──
# (#941's root cause: a format added to the quality profile but not the lossy-copy
# side. The lists are physically separate by choice — runtime-fetching a yearly-
# changing set isn't worth the coupling — so a test makes the drift impossible to
# ship silently instead.)
import re
from pathlib import Path
_ROOT = Path(__file__).resolve().parent.parent.parent
_SETTINGS_JS = _ROOT / "webui" / "static" / "settings.js"
_INDEX_HTML = _ROOT / "webui" / "index.html"
def _js_array(text, name):
m = re.search(rf"{name}\s*=\s*\[([^\]]*)\]", text)
assert m, f"{name} not found"
return set(re.findall(r"'([^']+)'", m.group(1)))
def test_frontend_rt_lossless_formats_matches_backend():
js = _SETTINGS_JS.read_text(encoding="utf-8")
frontend = _js_array(js, "RT_LOSSLESS_FORMATS")
assert frontend == set(LOSSLESS_FORMATS), (
f"settings.js RT_LOSSLESS_FORMATS {frontend} != backend LOSSLESS_FORMATS "
f"{set(LOSSLESS_FORMATS)} — add the new format to BOTH"
)
def test_quality_profile_dropdown_offers_every_lossless_format():
html = _INDEX_HTML.read_text(encoding="utf-8")
m = re.search(r'<optgroup label="Lossless">(.*?)</optgroup>', html, re.DOTALL)
assert m, "Lossless optgroup not found in index.html"
# concrete per-format option values (skip the 'group:lossless' convenience entry)
option_values = {v for v in re.findall(r'value="([^"]+)"', m.group(1))
if not v.startswith("group:")}
assert set(LOSSLESS_FORMATS) <= option_values, (
f"index.html lossless dropdown {option_values} is missing "
f"{set(LOSSLESS_FORMATS) - option_values}"
)