soulsync/tests/test_sync_wishlist_readd.py
BoulderBadgeDad d0966ec262 Sync wishlist re-add: build the IDENTICAL payload the auto-add uses
Root cause (the real one): the auto-add passes original_tracks_map[id] — tracks_json
run through a specific normalization (album->dict with images/album_type/total_tracks/
release_date, artists->dicts). My re-add hand-rolled a different shape, so the stored
spotify_data didn't match and the wishlist's nebula (which reads spotify_data.album.
images[0].url) had no cover, plus album/single classification could differ.

Fix: extract that normalization into one shared build_original_tracks_map() and use it
in BOTH the live sync (core.discovery.sync) and the re-add. The re-add now resolves the
track by source_track_id through the same map — byte-identical payload. Verified on a
real sync row: re-add payload == live-sync payload, album.images present. (The shared
normalizer is also copy-safe, fixing a latent tracks_json mutation in the old inline
version.)

Fallback (track absent from tracks_json) rebuilds through the same normalizer with the
cover seeded from the row's image_url. 10 tests incl. a direct parity assertion.
2026-06-24 16:41:40 -07:00

105 lines
4.6 KiB
Python

"""Re-add a synced unmatched track to the wishlist with the EXACT auto-add payload.
The live sync and the sync-detail re-add must build the identical wishlist payload
(via build_original_tracks_map), so a re-added track is indistinguishable from the
original auto-add — including its album cover, album_type, and artist shape.
"""
from __future__ import annotations
from core.sync.wishlist_readd import (
build_original_tracks_map,
normalize_wishlist_track,
reconstruct_sync_track_data,
)
def _tr(index, sid, status='wishlist', **kw):
return {"index": index, "source_track_id": sid, "download_status": status,
"name": kw.get("name", ""), "artist": kw.get("artist", ""),
"album": kw.get("album", ""), "image_url": kw.get("image_url", ""),
"duration_ms": kw.get("duration_ms", 0)}
def _full(sid, name="Song", with_images=True):
album = {"name": "Album", "album_type": "album", "total_tracks": 12}
if with_images:
album["images"] = [{"url": "http://cdn/cover.jpg", "height": 640, "width": 640}]
return {"id": sid, "name": name, "artists": [{"name": "Artist"}], "album": album,
"duration_ms": 200000, "popularity": 50}
# ── normalize_wishlist_track ──────────────────────────────────────────────────
def test_normalize_string_album_to_dict():
out = normalize_wishlist_track({"id": "a", "name": "T", "album": "My Single", "artists": ["X"]})
assert out["album"] == {"name": "My Single", "images": [], "album_type": "single",
"total_tracks": 1, "release_date": ""}
assert out["artists"] == [{"name": "X"}] # strings -> dicts
def test_normalize_dict_album_preserves_images_and_type():
out = normalize_wishlist_track(_full("a"))
assert out["album"]["images"][0]["url"] == "http://cdn/cover.jpg"
assert out["album"]["album_type"] == "album"
assert out["album"]["total_tracks"] == 12
def test_normalize_is_copy_safe():
src = {"id": "a", "album": {"name": "A"}, "artists": ["X"]}
normalize_wishlist_track(src)
assert "images" not in src["album"] # source untouched
assert src["artists"] == ["X"]
# ── reconstruct: parity with the live auto-add ────────────────────────────────
def test_payload_is_identical_to_live_sync_map():
# THE PARITY GUARANTEE: re-add payload == what build_original_tracks_map (used by
# the live sync) produces for the same track.
trs = [_tr(0, "sp_a"), _tr(1, "sp_b")]
tracks = [_full("sp_a"), _full("sp_b", name="Other")]
out = reconstruct_sync_track_data(trs, tracks, 1)
assert out == build_original_tracks_map(tracks)["sp_b"]
assert out["album"]["images"][0]["url"] == "http://cdn/cover.jpg" # cover carries through
def test_resolves_by_source_track_id_not_position():
trs = [_tr(0, "sp_a"), _tr(1, "sp_b")]
tracks = [_full("sp_b"), _full("sp_a")] # tracks_json reversed
out = reconstruct_sync_track_data(trs, tracks, 0) # row 0 -> sp_a
assert out["id"] == "sp_a"
def test_fallback_rebuilds_with_cover_when_track_missing():
# Track not in tracks_json -> rebuild from the row, seed cover from image_url,
# run through the same normalizer (album dict + artists dicts).
trs = [_tr(0, "sp_x", name="Real Love Baby", artist="Father John Misty",
album="Real Love Baby", image_url="http://img/x.jpg", duration_ms=188000)]
out = reconstruct_sync_track_data(trs, [], 0)
assert out["id"] == "sp_x"
assert out["name"] == "Real Love Baby"
assert out["artists"] == [{"name": "Father John Misty"}]
assert out["album"]["images"] == [{"url": "http://img/x.jpg"}]
assert out["album"]["name"] == "Real Love Baby"
def test_refuses_non_wishlist_row():
trs = [_tr(0, "sp_a", status="completed")]
assert reconstruct_sync_track_data(trs, [_full("sp_a")], 0) is None
def test_refuses_out_of_range_or_empty():
assert reconstruct_sync_track_data([], [], 0) is None
assert reconstruct_sync_track_data(None, None, 0) is None
assert reconstruct_sync_track_data([_tr(0, "sp_a")], [], 5) is None
assert reconstruct_sync_track_data([_tr(0, "sp_a")], [], -1) is None
def test_refuses_when_no_id_and_no_full_track():
assert reconstruct_sync_track_data([_tr(0, "")], [], 0) is None
def test_build_map_skips_idless_and_non_dicts():
m = build_original_tracks_map([_full("a"), {"name": "no id"}, "garbage", None])
assert set(m.keys()) == {"a"}