soulsync/tests/imports/test_import_rejection_reason.py
dev d717f06afe test: update bit-depth guard tests to the unified quality-guard behavior
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>
2026-06-14 12:52:14 +02:00

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"