soulsync/tests/downloads/test_wishlist_aggregator.py
Broque Thomas 4555ff7eb9 Wishlist modal: surface most-advanced live phase, not least-complete
The sibling-merge aggregator from 7f751202 used "least-complete
phase wins", which made the modal appear frozen during parallel
album bundle downloads. The task table is phase-gated to
downloading/complete/error in downloads.js — so whenever any
sibling was still in album_downloading, the merged phase stayed
there and tasks for the sibling that had advanced past its bundle
never rendered. User reported: both albums downloading on slskd,
modal blank until one completes fully.

Flip the rule: surface the most-advanced live phase so the modal
renders task progress as soon as any sibling reaches it. The
all-siblings-in-album_downloading case still surfaces
album_downloading (bundle progress UI is correct there); error
stays sticky.

Updated WHATS_NEW under 2.6.3 to describe the corrected behavior.
Two new tests pin the regression:
- downloading + album_downloading → downloading
- album_downloading + album_downloading → album_downloading
2026-05-26 22:35:53 -07:00

192 lines
7.4 KiB
Python

"""Unit tests for ``core/downloads/wishlist_aggregator.merge_wishlist_run_status``.
Pins the merge contract the wishlist-modal status path depends on
(Phase 1c.2.1 follow-up): when one logical wishlist run is split
across N sub-batches, the frontend modal polls the original
batch_id and expects a unified view that covers every sibling.
"""
from __future__ import annotations
from core.downloads.wishlist_aggregator import merge_wishlist_run_status
def _status(phase, **kwargs):
"""Build a minimal per-batch status dict shaped like
``build_batch_status_data``'s output."""
base = {
'phase': phase,
'playlist_id': 'wishlist',
'playlist_name': 'Wishlist',
'active_count': 0,
'max_concurrent': 3,
}
base.update(kwargs)
return base
def test_empty_siblings_returns_primary_unchanged():
primary = _status('downloading', tasks=[{'task_id': 't1', 'track_index': 0}])
out = merge_wishlist_run_status(primary, [])
assert out is primary
def test_two_siblings_merge_tasks_with_reindexed_track_index():
"""Both siblings locally start at track_index 0 — after merge,
indices are globally unique 0..N-1."""
primary = _status(
'downloading',
analysis_results=[
{'track_index': 0, 'track': {'name': 'A1'}, 'found': False, 'confidence': 0.0},
{'track_index': 1, 'track': {'name': 'A2'}, 'found': False, 'confidence': 0.0},
],
tasks=[
{'task_id': 'task-a1', 'track_index': 0, 'status': 'downloading'},
{'task_id': 'task-a2', 'track_index': 1, 'status': 'downloading'},
],
)
sibling = _status(
'downloading',
analysis_results=[
{'track_index': 0, 'track': {'name': 'B1'}, 'found': False, 'confidence': 0.0},
],
tasks=[
{'task_id': 'task-b1', 'track_index': 0, 'status': 'searching'},
],
)
merged = merge_wishlist_run_status(primary, [sibling])
# Three globally-unique track indices.
assert [r['track_index'] for r in merged['analysis_results']] == [0, 1, 2]
# Each task's track_index re-indexed to match its analysis_result.
indices_by_task = {t['task_id']: t['track_index'] for t in merged['tasks']}
assert indices_by_task == {'task-a1': 0, 'task-a2': 1, 'task-b1': 2}
# Tasks sorted by their new track_index.
assert [t['task_id'] for t in merged['tasks']] == ['task-a1', 'task-a2', 'task-b1']
def test_phase_aggregation_most_advanced_live_phase_wins():
"""analysis + downloading + complete → downloading. Modal's task
table is phase-gated to downloading/complete/error so we must
surface that phase the moment any sibling has tasks, else the
modal stays in bundle/analysis UI and tasks stay invisible."""
primary = _status('complete')
sibling1 = _status('downloading')
sibling2 = _status('analysis')
merged = merge_wishlist_run_status(primary, [sibling1, sibling2])
assert merged['phase'] == 'downloading'
def test_phase_aggregation_downloading_wins_over_album_downloading():
"""Regression test for the parallel-bundle modal-blank bug:
one sibling past its bundle into the task stage means the
modal MUST be in 'downloading' phase, otherwise the task
rows for the advanced sibling don't render and the user
sees nothing while both albums actually download on slskd."""
primary = _status('downloading')
sibling = _status('album_downloading')
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['phase'] == 'downloading'
def test_phase_aggregation_all_album_downloading_stays_album_downloading():
"""No sibling has reached the task stage yet — bundle progress
UI is the right thing to show."""
primary = _status('album_downloading')
sibling = _status('album_downloading')
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['phase'] == 'album_downloading'
def test_phase_aggregation_album_downloading_wins_over_analysis():
primary = _status('analysis')
sibling = _status('album_downloading')
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['phase'] == 'album_downloading'
def test_phase_aggregation_all_complete_returns_complete():
primary = _status('complete')
sibling1 = _status('complete')
merged = merge_wishlist_run_status(primary, [sibling1])
assert merged['phase'] == 'complete'
def test_phase_aggregation_mixed_complete_and_other_returns_downloading():
"""A finished sibling alongside a still-downloading sibling
surfaces 'downloading' (the run isn't done)."""
primary = _status('complete')
sibling = _status('downloading')
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['phase'] == 'downloading'
def test_phase_aggregation_error_is_sticky():
"""If any sibling errored, the merged phase is 'error' even
if other siblings are still running. Modal should show the
failure so the user notices."""
primary = _status('downloading')
sibling = _status('error')
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['phase'] == 'error'
def test_analysis_progress_summed_across_siblings():
primary = _status(
'analysis',
analysis_progress={'total': 10, 'processed': 7},
)
sibling = _status(
'analysis',
analysis_progress={'total': 5, 'processed': 2},
)
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['analysis_progress'] == {'total': 15, 'processed': 9}
def test_album_bundle_picks_active_sibling_over_idle():
"""Primary is past its bundle stage (state='staged');
sibling is currently downloading_release. Merge surfaces the
active sibling's bundle so the progress bar stays useful."""
primary = _status(
'downloading',
album_bundle={'state': 'staged', 'progress': 100, 'release': 'PRISM (Deluxe)'},
)
sibling = _status(
'album_downloading',
album_bundle={'state': 'downloading_release', 'progress': 42, 'release': '1432'},
)
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['album_bundle']['release'] == '1432'
assert merged['album_bundle']['progress'] == 42
def test_album_bundle_falls_back_when_no_active_sibling():
primary = _status(
'complete',
album_bundle={'state': 'staged', 'progress': 100, 'release': 'PRISM (Deluxe)'},
)
sibling = _status(
'complete',
album_bundle={'state': 'staged', 'progress': 100, 'release': '1432'},
)
merged = merge_wishlist_run_status(primary, [sibling])
# Falls back to primary's bundle (first non-empty).
assert merged['album_bundle']['release'] == 'PRISM (Deluxe)'
def test_active_count_summed_across_siblings():
primary = _status('downloading', active_count=2)
sibling = _status('downloading', active_count=1)
merged = merge_wishlist_run_status(primary, [sibling])
assert merged['active_count'] == 3
def test_primary_playlist_id_preserved():
primary = _status('downloading', playlist_id='wishlist', playlist_name='Wishlist (Auto)')
sibling = _status('downloading', playlist_id='wishlist', playlist_name='Wishlist (Album: 1432)')
merged = merge_wishlist_run_status(primary, [sibling])
# Primary's playlist_name + playlist_id propagate (it's the row the modal opened against).
assert merged['playlist_id'] == 'wishlist'
assert merged['playlist_name'] == 'Wishlist (Auto)'