check_flac_bit_depth now delegates to check_quality_target, which probes the real file and treats bit depth as a MINIMUM (24-bit satisfies a 16-bit target) — the old context-string parsing, per-quality bit_depth_fallback, and 'reject higher bit depth' semantics are gone. Rewrite the wrapper tests to the probe-based model and update the rejection-reason assertion to the unified 'quality filter' wording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
"""Tests for import_rejection_reason — the manual-import quarantine guard.
|
|
|
|
Regression for #764: the manual-import routes call
|
|
``post_process_matched_download`` directly. A quarantine (integrity / AcoustID
|
|
/ bit-depth) or race-guard rejection returns NORMALLY (no exception) and leaves
|
|
the file in ss_quarantine, not the library — but the routes counted any
|
|
no-exception return as a successful import, so the UI showed a green "Done" for
|
|
a file that had actually vanished. ``import_rejection_reason`` reads the context
|
|
flags the inner pipeline sets so the routes can report those as errors.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
from core.imports.pipeline import import_rejection_reason
|
|
from core.imports.routes import ImportRouteRuntime, process_single_import_file
|
|
|
|
|
|
def test_clean_import_returns_none():
|
|
assert import_rejection_reason({}) is None
|
|
assert import_rejection_reason({'is_album': True, 'track_info': {}}) is None
|
|
|
|
|
|
def test_integrity_failure_detected():
|
|
reason = import_rejection_reason({'_integrity_failure_msg': 'duration drift 12s'})
|
|
assert reason is not None
|
|
assert 'integrity' in reason.lower()
|
|
assert 'duration drift 12s' in reason
|
|
|
|
|
|
def test_acoustid_quarantine_detected():
|
|
reason = import_rejection_reason({
|
|
'_acoustid_quarantined': True,
|
|
'_acoustid_failure_msg': 'wrong artist: got Oasis expected Coldplay',
|
|
})
|
|
assert reason is not None
|
|
assert 'acoustid' in reason.lower()
|
|
assert 'Coldplay' in reason
|
|
|
|
|
|
def test_acoustid_quarantine_without_message_still_flags():
|
|
# The flag alone must trip it even if no message was stashed.
|
|
reason = import_rejection_reason({'_acoustid_quarantined': True})
|
|
assert reason is not None
|
|
assert 'acoustid' in reason.lower()
|
|
|
|
|
|
def test_bitdepth_rejection_detected():
|
|
# The _bitdepth_rejected flag now signals any quality-target rejection
|
|
# (bit depth, sample rate, format, bitrate) — the unified quality guard.
|
|
reason = import_rejection_reason({'_bitdepth_rejected': True})
|
|
assert reason is not None
|
|
assert 'quality filter' in reason.lower()
|
|
|
|
|
|
def test_race_guard_failure_detected():
|
|
reason = import_rejection_reason({'_race_guard_failed': True})
|
|
assert reason is not None
|
|
assert 'disappeared' in reason.lower()
|
|
|
|
|
|
def test_falsy_flags_do_not_trip():
|
|
# A flag present but falsy (e.g. integrity passed) must NOT be a rejection.
|
|
ctx = {
|
|
'_integrity_failure_msg': '',
|
|
'_acoustid_quarantined': False,
|
|
'_bitdepth_rejected': False,
|
|
'_race_guard_failed': False,
|
|
}
|
|
assert import_rejection_reason(ctx) is None
|
|
|
|
|
|
def test_integrity_takes_precedence_when_multiple_set():
|
|
# Deterministic ordering: integrity first.
|
|
reason = import_rejection_reason({
|
|
'_integrity_failure_msg': 'truncated',
|
|
'_acoustid_quarantined': True,
|
|
'_bitdepth_rejected': True,
|
|
})
|
|
assert 'integrity' in reason.lower()
|
|
|
|
|
|
# ── route-level wiring: a quarantine must NOT report as a successful import ──
|
|
|
|
|
|
def _runtime_with_post_process(post_process):
|
|
"""Build an ImportRouteRuntime wired with stub resolvers + the supplied
|
|
post_process_matched_download. Resolvers return a shared context dict so a
|
|
flag the post-processor sets is what import_rejection_reason later reads."""
|
|
ctx = {}
|
|
|
|
return ImportRouteRuntime(
|
|
get_single_track_import_context=lambda *a, **k: {"context": ctx, "source": "local"},
|
|
normalize_import_context=lambda c: c,
|
|
get_import_context_artist=lambda c: {"name": "Coldplay"},
|
|
get_import_track_info=lambda c: {"name": "Yellow"},
|
|
post_process_matched_download=post_process,
|
|
), ctx
|
|
|
|
|
|
def _tmp_audio_file():
|
|
fd, path = tempfile.mkstemp(suffix=".flac")
|
|
os.close(fd)
|
|
with open(path, "wb") as f:
|
|
f.write(b"fLaC") # only needs to exist for os.path.isfile
|
|
return path
|
|
|
|
|
|
def test_single_import_quarantine_reported_as_error():
|
|
# post-processing quarantines the file (sets the flag, returns normally).
|
|
def quarantining_post_process(context_key, context, file_path):
|
|
context['_acoustid_quarantined'] = True
|
|
context['_acoustid_failure_msg'] = 'wrong track'
|
|
|
|
runtime, _ctx = _runtime_with_post_process(quarantining_post_process)
|
|
path = _tmp_audio_file()
|
|
try:
|
|
outcome, payload = process_single_import_file(
|
|
runtime, {"full_path": path, "filename": "Coldplay - Yellow.flac",
|
|
"title": "Yellow", "artist": "Coldplay"},
|
|
)
|
|
finally:
|
|
os.remove(path)
|
|
|
|
assert outcome == "error" # NOT "ok" -> route won't count it processed
|
|
assert "AcoustID" in payload
|
|
|
|
|
|
def test_single_import_clean_reports_ok():
|
|
# post-processing succeeds (no flags) -> import counts as processed.
|
|
def clean_post_process(context_key, context, file_path):
|
|
return None
|
|
|
|
runtime, _ctx = _runtime_with_post_process(clean_post_process)
|
|
path = _tmp_audio_file()
|
|
try:
|
|
outcome, payload = process_single_import_file(
|
|
runtime, {"full_path": path, "filename": "Coldplay - Yellow.flac",
|
|
"title": "Yellow", "artist": "Coldplay"},
|
|
)
|
|
finally:
|
|
os.remove(path)
|
|
|
|
assert outcome == "ok"
|
|
assert payload == "Yellow"
|