From 95a7b51966a66da2b52f80b51b4079714098ea0f Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 14 Jun 2026 12:46:39 +0200 Subject: [PATCH] test(quality): guard reason content, force_import isolation, bitrate-as-threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quality guard: rejects with a 'file is X, wanted Y' reason (the string the track-detail modal surfaces), accepts when a target is met or fallback is on, skips when unprobeable. force_import isolation: the 'quality' bypass must not skip the AcoustID check and vice-versa; a quality reject persists trigger='quality' (not 'acoustid') in the sidecar — so a quality mismatch never routes through the force_import path (reserved for AcoustID version-mismatch). Model: lossy matches a MINIMUM bitrate (>=, a range); lossless matches on bit depth + sample rate, never exact bitrate, so a FLAC's varying bitrate (mono / compression) can't falsely reject it. v2->v3 migration preserves order. 47 passing across the quality + guard suites. Co-Authored-By: Claude Opus 4.8 --- tests/imports/test_quality_guard.py | 109 ++++++++++++++++++++++++++++ tests/quality/test_model.py | 73 +++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 tests/imports/test_quality_guard.py create mode 100644 tests/quality/test_model.py diff --git a/tests/imports/test_quality_guard.py b/tests/imports/test_quality_guard.py new file mode 100644 index 00000000..b180bf75 --- /dev/null +++ b/tests/imports/test_quality_guard.py @@ -0,0 +1,109 @@ +"""Quality guard + quarantine isolation. + +Locks two invariants the global-quality work depends on: + +1. ``check_quality_target`` rejects with a 'what the file IS vs what was + WANTED' reason (surfaced in the track-detail modal), and accepts when a + target is met or fallback is on. +2. A quality mismatch is isolated from the AcoustID/force_import path: it + uses ``trigger='quality'`` and the ``'quality'`` bypass flag, which must + NOT bypass the AcoustID check and vice-versa. force_imported stays + reserved for AcoustID version-mismatch acceptance. +""" + +import json +import types + +import pytest + +import core.imports.guards as guards +import core.imports.file_ops as file_ops +from core.imports.pipeline import _should_skip_quarantine_check +from core.quality.model import AudioQuality + + +class _FakeDB: + def __init__(self, profile): + self._p = profile + + def get_quality_profile(self): + return self._p + + +def _patch_guard(monkeypatch, probe_aq, profile, downsample=False): + monkeypatch.setattr(file_ops, 'probe_audio_quality', lambda fp: probe_aq) + monkeypatch.setattr(guards, 'MusicDatabase', lambda: _FakeDB(profile)) + monkeypatch.setattr( + guards, '_get_config_manager', + lambda: types.SimpleNamespace(get=lambda k, d=None: downsample if 'downsample' in k else d), + ) + + +_WANT_FLAC24 = { + 'fallback_enabled': False, + 'ranked_targets': [ + {'label': 'FLAC 24-bit/96kHz', 'format': 'flac', 'bit_depth': 24, 'min_sample_rate': 96000}, + ], +} +_WANT_FLAC24_FALLBACK = {**_WANT_FLAC24, 'fallback_enabled': True} + + +# ── check_quality_target ─────────────────────────────────────────────────── + +def test_rejects_with_wanted_vs_got_reason(monkeypatch): + _patch_guard(monkeypatch, AudioQuality('flac', sample_rate=44100, bit_depth=16), _WANT_FLAC24) + reason = guards.check_quality_target('/x/song.flac', {}) + assert reason is not None + assert 'FLAC 16-bit' in reason # what the file IS + assert 'FLAC 24-bit/96kHz' in reason # what was WANTED + + +def test_accepts_when_target_met(monkeypatch): + _patch_guard(monkeypatch, AudioQuality('flac', sample_rate=96000, bit_depth=24), _WANT_FLAC24) + assert guards.check_quality_target('/x/song.flac', {}) is None + + +def test_accepts_via_fallback(monkeypatch): + _patch_guard(monkeypatch, AudioQuality('flac', sample_rate=44100, bit_depth=16), _WANT_FLAC24_FALLBACK) + assert guards.check_quality_target('/x/song.flac', {}) is None + + +def test_skips_when_unprobeable(monkeypatch): + _patch_guard(monkeypatch, None, _WANT_FLAC24) + assert guards.check_quality_target('/x/song.flac', {}) is None + + +# ── force_import isolation ───────────────────────────────────────────────── + +def test_quality_bypass_does_not_skip_acoustid(): + ctx = {'_skip_quarantine_check': 'quality'} + assert _should_skip_quarantine_check(ctx, 'quality') is True + assert _should_skip_quarantine_check(ctx, 'acoustid') is False + + +def test_acoustid_bypass_does_not_skip_quality(): + ctx = {'_skip_quarantine_check': 'acoustid'} + assert _should_skip_quarantine_check(ctx, 'acoustid') is True + assert _should_skip_quarantine_check(ctx, 'quality') is False + + +def test_quality_quarantine_persists_quality_trigger(monkeypatch, tmp_path): + # A quality reject writes trigger='quality' (not 'acoustid') into the + # sidecar, so Approve never routes it through the force_import path. + monkeypatch.setattr( + guards, '_get_config_manager', + lambda: types.SimpleNamespace(get=lambda k, d=None: str(tmp_path) if 'download_path' in k else d), + ) + src = tmp_path / 'song.flac' + src.write_bytes(b'FLACfake') + qpath = guards.move_to_quarantine( + str(src), {}, 'Quality mismatch: file is FLAC 16-bit, wanted FLAC 24-bit/96kHz', + automation_engine=None, trigger='quality', + ) + sidecars = list((tmp_path / 'ss_quarantine').glob('*.json')) + assert len(sidecars) == 1 + meta = json.loads(sidecars[0].read_text(encoding='utf-8')) + assert meta['trigger'] == 'quality' + assert meta['trigger'] != 'acoustid' + assert 'wanted FLAC 24-bit/96kHz' in meta['quarantine_reason'] + assert qpath.endswith('.quarantined') diff --git a/tests/quality/test_model.py b/tests/quality/test_model.py new file mode 100644 index 00000000..62e5887a --- /dev/null +++ b/tests/quality/test_model.py @@ -0,0 +1,73 @@ +"""AudioQuality.matches_target + v2->v3 migration. + +Locks the bitrate-as-threshold behaviour: lossy formats match on a MINIMUM +bitrate (>=, a range), and lossless matches on bit depth + sample rate — NOT +on exact bitrate, so a FLAC's wildly-varying bitrate (stereo vs mono, FLAC +compression) never falsely rejects it. +""" + +import pytest + +from core.quality.model import ( + AudioQuality, + QualityTarget, + v2_qualities_to_ranked_targets, +) + + +# ── lossy: bitrate is a minimum threshold (a range), never exact ─────────── + +def test_mp3_meets_minimum_bitrate(): + t = QualityTarget(format='mp3', min_bitrate=320) + assert AudioQuality('mp3', bitrate=320).matches_target(t) is True + assert AudioQuality('mp3', bitrate=400).matches_target(t) is True # above floor ok + + +def test_mp3_below_minimum_bitrate_rejected(): + t = QualityTarget(format='mp3', min_bitrate=320) + assert AudioQuality('mp3', bitrate=300).matches_target(t) is False + + +def test_mp3_matches_lower_threshold(): + t = QualityTarget(format='mp3', min_bitrate=192) + assert AudioQuality('mp3', bitrate=320).matches_target(t) is True + + +# ── lossless: matched on bit depth + sample rate, NOT exact bitrate ──────── + +def test_flac_matches_on_depth_and_rate_regardless_of_bitrate(): + t = QualityTarget(format='flac', bit_depth=24, min_sample_rate=96000) + # An unusual/low bitrate (e.g. a mono or highly-compressed FLAC) must + # still match when bit depth + sample rate satisfy the target. + weird = AudioQuality('flac', bitrate=300, sample_rate=96000, bit_depth=24) + assert weird.matches_target(t) is True + + +def test_flac_below_target_sample_rate_rejected(): + t = QualityTarget(format='flac', bit_depth=24, min_sample_rate=96000) + assert AudioQuality('flac', sample_rate=44100, bit_depth=24).matches_target(t) is False + + +def test_flac_below_target_bit_depth_rejected(): + t = QualityTarget(format='flac', bit_depth=24) + assert AudioQuality('flac', sample_rate=96000, bit_depth=16).matches_target(t) is False + + +def test_format_mismatch_rejected(): + t = QualityTarget(format='flac', bit_depth=16) + assert AudioQuality('mp3', bitrate=320).matches_target(t) is False + + +# ── v2 -> v3 migration preserves the user's priority order ───────────────── + +def test_v2_to_v3_preserves_order_and_maps_fields(): + qualities = { + 'flac': {'enabled': True, 'priority': 1, 'bit_depth': '24'}, + 'mp3_320': {'enabled': True, 'priority': 2}, + 'mp3_192': {'enabled': False, 'priority': 3}, # disabled → dropped + } + targets = v2_qualities_to_ranked_targets(qualities) + formats = [t['format'] for t in targets] + assert formats == ['flac', 'mp3'] # disabled mp3_192 omitted + assert targets[0]['bit_depth'] == 24 + assert targets[1]['min_bitrate'] == 320