resolve_history_audio_path drives a DESTRUCTIVE delete (os.remove), but lived endpoint-bound in web_server with zero tests. Lifted to core/matching/history_paths with injected effects (exists / resolve_library_path / lookup_titled_paths) so the fallback chain — and the collision-safety that stops delete() from removing the wrong same-title file — is a clean importable seam. web_server now wraps it (DB lookup + os.path.exists + prefix resolver injected); behavior preserved. 9 tests lock it: recorded-path hit, prefix-resolve fallback, single tracks-table candidate, and the safety rules — multiple same-title candidates with NO artist -> None (refuse to guess), artist filter picks only the matching path, artist named but unmatched -> None, no-title/empty-lookup -> None. Full suite green (5906).
87 lines
3.6 KiB
Python
87 lines
3.6 KiB
Python
"""Seam tests for resolve_history_audio_path — the fallback chain a DESTRUCTIVE
|
|
delete trusts. The collision-safety rules (artist filter / single-candidate) are
|
|
what stop delete() from removing the wrong same-title file, so they're locked here."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.matching.history_paths import resolve_history_audio_path
|
|
|
|
|
|
def _resolver(existing=(), resolve_map=None, titled=None):
|
|
existing = set(existing)
|
|
resolve_map = resolve_map or {}
|
|
titled = titled or {}
|
|
return dict(
|
|
exists=lambda p: p in existing,
|
|
resolve_library_path=lambda raw: resolve_map.get(raw),
|
|
lookup_titled_paths=lambda title: list(titled.get(title.lower(), [])),
|
|
)
|
|
|
|
|
|
def test_recorded_path_used_when_it_exists():
|
|
r = resolve_history_audio_path({'file_path': '/m/a.mp3'}, **_resolver(existing={'/m/a.mp3'}))
|
|
assert r == '/m/a.mp3'
|
|
|
|
|
|
def test_falls_back_to_prefix_resolved_path():
|
|
r = resolve_history_audio_path(
|
|
{'file_path': '/transfer/a.mp3'},
|
|
**_resolver(existing={'/library/a.mp3'}, resolve_map={'/transfer/a.mp3': '/library/a.mp3'}))
|
|
assert r == '/library/a.mp3'
|
|
|
|
|
|
def test_tracks_table_single_candidate_no_artist():
|
|
r = resolve_history_audio_path(
|
|
{'file_path': '/gone.mp3', 'title': 'Song'},
|
|
**_resolver(existing={'/library/song.mp3'},
|
|
resolve_map={'/lib/song.mp3': '/library/song.mp3'},
|
|
titled={'song': ['/lib/song.mp3']}))
|
|
assert r == '/library/song.mp3'
|
|
|
|
|
|
def test_collision_no_artist_multiple_candidates_returns_none():
|
|
# THE safety rule: same title, no artist to disambiguate -> refuse to guess
|
|
# (delete() must not remove an arbitrary one of two same-title files).
|
|
r = resolve_history_audio_path(
|
|
{'file_path': '', 'title': 'Intro'},
|
|
**_resolver(existing={'/library/a/intro.mp3', '/library/b/intro.mp3'},
|
|
resolve_map={'/a/intro.mp3': '/library/a/intro.mp3', '/b/intro.mp3': '/library/b/intro.mp3'},
|
|
titled={'intro': ['/a/intro.mp3', '/b/intro.mp3']}))
|
|
assert r is None
|
|
|
|
|
|
def test_artist_filter_picks_only_the_matching_path():
|
|
# Two same-title files by different artists -> only the one whose path
|
|
# mentions the row's artist is eligible (won't delete the other artist's file).
|
|
r = resolve_history_audio_path(
|
|
{'file_path': '', 'title': 'Intro', 'artist_name': 'Alpha'},
|
|
**_resolver(existing={'/music/Alpha/intro.mp3', '/music/Beta/intro.mp3'},
|
|
resolve_map={'/Alpha/intro.mp3': '/music/Alpha/intro.mp3',
|
|
'/Beta/intro.mp3': '/music/Beta/intro.mp3'},
|
|
titled={'intro': ['/Alpha/intro.mp3', '/Beta/intro.mp3']}))
|
|
assert r == '/music/Alpha/intro.mp3'
|
|
|
|
|
|
def test_artist_named_but_no_path_mentions_it_returns_none():
|
|
r = resolve_history_audio_path(
|
|
{'file_path': '', 'title': 'Intro', 'artist_name': 'Gamma'},
|
|
**_resolver(existing={'/music/Alpha/intro.mp3'},
|
|
resolve_map={'/Alpha/intro.mp3': '/music/Alpha/intro.mp3'},
|
|
titled={'intro': ['/Alpha/intro.mp3']}))
|
|
assert r is None
|
|
|
|
|
|
def test_no_title_returns_none():
|
|
assert resolve_history_audio_path({'file_path': '/gone.mp3'}, **_resolver()) is None
|
|
|
|
|
|
def test_nothing_resolves_returns_none():
|
|
assert resolve_history_audio_path(
|
|
{'file_path': '/gone.mp3', 'title': 'X'},
|
|
**_resolver(titled={'x': ['/also/gone.mp3']})) is None
|
|
|
|
|
|
def test_empty_lookup_returns_none():
|
|
# DB-error proxy: lookup yields [] -> None (never a stray delete target).
|
|
assert resolve_history_audio_path(
|
|
{'file_path': '', 'title': 'X'}, **_resolver(titled={})) is None
|