soulsync/tests/downloads/test_downloads_status.py
Broque Thomas 9a0e3b4011 Persist completed downloads in downloads view
Include a capped recent tail of database-backed download history in the unified Downloads page so completed Deezer and other streaming downloads remain visible after runtime tasks are cleaned up or the container restarts. Use persistent download history for the dashboard finished count, keep live tasks authoritative for active rows, avoid showing the local clear-completed action for persisted history rows, and cover history hydration/deduping/capping in status tests.
2026-05-24 10:02:00 -07:00

744 lines
26 KiB
Python

"""Tests for core/downloads/status.py — batch + unified status helpers."""
from __future__ import annotations
import pytest
from core.downloads import status as st
from core.runtime_state import (
download_batches,
download_tasks,
)
@pytest.fixture(autouse=True)
def reset_state():
download_tasks.clear()
download_batches.clear()
yield
download_tasks.clear()
download_batches.clear()
# ---------------------------------------------------------------------------
# Fakes
# ---------------------------------------------------------------------------
class _FakeConfig:
def __init__(self, values=None):
self._v = values or {}
def get(self, key, default=None):
return self._v.get(key, default)
def _build_deps(
*,
config=None,
docker_resolve=None,
find_completed=None,
make_key=None,
submit_pp=None,
cached_transfers=None,
download_orchestrator=None,
run_async=None,
persistent_history=None,
):
submitted = []
def _default_submit(task_id, batch_id):
submitted.append((task_id, batch_id))
deps = st.StatusDeps(
config_manager=config or _FakeConfig({'soulseek.download_timeout': 600}),
docker_resolve_path=docker_resolve or (lambda p: p),
find_completed_file=find_completed or (lambda *a, **kw: (None, None)),
make_context_key=make_key or (lambda u, f: f"{u}::{f}"),
submit_post_processing=submit_pp or _default_submit,
get_cached_transfer_data=cached_transfers or (lambda: {}),
download_orchestrator=download_orchestrator,
run_async=run_async,
get_persistent_download_history=persistent_history,
)
return deps, submitted
# ---------------------------------------------------------------------------
# build_batch_status_data — phase routing
# ---------------------------------------------------------------------------
def test_unknown_phase_includes_basic_fields_only():
deps, _ = _build_deps()
batch = {'phase': 'unknown', 'playlist_id': 'pl1', 'playlist_name': 'My PL'}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['phase'] == 'unknown'
assert out['playlist_id'] == 'pl1'
assert out['playlist_name'] == 'My PL'
assert 'tasks' not in out
assert 'analysis_progress' not in out
def test_analysis_phase_includes_analysis_progress_and_results():
deps, _ = _build_deps()
batch = {
'phase': 'analysis',
'analysis_total': 10,
'analysis_processed': 4,
'analysis_results': [{'track_index': 0, 'found': True}],
}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['analysis_progress'] == {'total': 10, 'processed': 4}
assert out['analysis_results'] == [{'track_index': 0, 'found': True}]
def test_album_downloading_phase_exposes_bundle_progress_without_task_safety_valve():
deps, _ = _build_deps(config=_FakeConfig({'soulseek.download_timeout': 1}))
download_tasks['t1'] = {
'track_index': 0,
'status': 'queued',
'track_info': {},
'status_change_time': 0,
}
batch = {
'phase': 'album_downloading',
'queue': ['t1'],
'album_bundle_state': 'downloading',
'album_bundle_source': 'torrent',
'album_bundle_release': 'GNX [FLAC]',
'album_bundle_progress': 0.42,
'album_bundle_speed': 2048,
'album_bundle_size': 4096,
}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['album_bundle'] == {
'state': 'downloading',
'source': 'torrent',
'release': 'GNX [FLAC]',
'progress': 0.42,
'progress_percent': 42,
'speed': 2048,
'size': 4096,
}
assert 'tasks' not in out
assert download_tasks['t1']['status'] == 'queued'
def test_complete_phase_includes_wishlist_summary_when_present():
deps, _ = _build_deps()
batch = {
'phase': 'complete',
'queue': [],
'wishlist_summary': {'added': 3, 'failed': 0},
}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['wishlist_summary'] == {'added': 3, 'failed': 0}
def test_complete_phase_omits_wishlist_summary_when_missing():
deps, _ = _build_deps()
batch = {'phase': 'complete', 'queue': []}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert 'wishlist_summary' not in out
# ---------------------------------------------------------------------------
# Task formatting
# ---------------------------------------------------------------------------
def test_downloading_phase_includes_active_count_and_max_concurrent():
deps, _ = _build_deps()
batch = {'phase': 'downloading', 'queue': [], 'active_count': 2, 'max_concurrent': 5}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['active_count'] == 2
assert out['max_concurrent'] == 5
def test_tasks_sorted_by_track_index():
deps, _ = _build_deps()
download_tasks['t1'] = {'track_index': 2, 'status': 'queued', 'track_info': {}}
download_tasks['t2'] = {'track_index': 0, 'status': 'queued', 'track_info': {}}
download_tasks['t3'] = {'track_index': 1, 'status': 'queued', 'track_info': {}}
batch = {'phase': 'downloading', 'queue': ['t1', 't2', 't3']}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert [t['track_index'] for t in out['tasks']] == [0, 1, 2]
def test_missing_task_in_queue_is_skipped():
deps, _ = _build_deps()
download_tasks['t1'] = {'track_index': 0, 'status': 'queued', 'track_info': {}}
batch = {'phase': 'downloading', 'queue': ['t1', 'absent']}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert len(out['tasks']) == 1
def test_task_status_includes_v2_state_fields():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'cancelling', 'track_info': {},
'cancel_requested': True, 'cancel_timestamp': 12345,
'ui_state': 'cancelling', 'playlist_id': 'pl1',
'error_message': 'oh no', 'cached_candidates': [{'x': 1}],
'quarantine_entry_id': '20260514_120000_song',
}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, {}, deps)
t = out['tasks'][0]
assert t['cancel_requested'] is True
assert t['cancel_timestamp'] == 12345
assert t['ui_state'] == 'cancelling'
assert t['playlist_id'] == 'pl1'
assert t['error_message'] == 'oh no'
assert t['quarantine_entry_id'] == '20260514_120000_song'
assert t['has_candidates'] is True
# ---------------------------------------------------------------------------
# Live transfer state mapping
# ---------------------------------------------------------------------------
def test_live_state_cancelled_maps_to_cancelled():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'downloading', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {'u1::song.flac': {'state': 'Cancelled', 'percentComplete': 50}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['status'] == 'cancelled'
assert download_tasks['t1']['status'] == 'cancelled' # mutates source state
def test_live_state_succeeded_with_full_bytes_marks_post_processing_and_submits():
deps, submitted = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'downloading', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {'u1::song.flac': {
'state': 'Succeeded', 'size': 100, 'bytesTransferred': 100, 'percentComplete': 100,
}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['status'] == 'post_processing'
assert download_tasks['t1']['status'] == 'post_processing'
assert submitted == [('t1', 'b1')]
def test_live_state_succeeded_with_byte_mismatch_keeps_status():
deps, submitted = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'downloading', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {'u1::song.flac': {
'state': 'Succeeded', 'size': 100, 'bytesTransferred': 50,
}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['status'] == 'downloading' # not promoted to post_processing
assert submitted == [] # not submitted
def test_live_state_inprogress_maps_to_downloading():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'queued', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {'u1::song.flac': {'state': 'InProgress', 'percentComplete': 42}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['status'] == 'downloading'
assert out['tasks'][0]['progress'] == 42
def test_live_state_errored_keeps_active_status_for_monitor():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'downloading', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {'u1::song.flac': {'state': 'Errored'}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
# Keeps current status so monitor handles retry
assert out['tasks'][0]['status'] == 'downloading'
assert download_tasks['t1']['status'] == 'downloading' # not marked failed
def test_terminal_status_not_overridden_by_live_state():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'completed', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {'u1::song.flac': {'state': 'InProgress', 'percentComplete': 50}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['status'] == 'completed'
assert out['tasks'][0]['progress'] == 100
def test_post_processing_status_progress_is_95():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'post_processing', 'track_info': {},
'filename': 'song.flac', 'username': 'u1',
}
live = {} # no live entry
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['progress'] == 95
def test_auto_torrent_without_live_entry_uses_engine_success_fallback():
class _Record:
state = 'Completed, Succeeded'
progress = 100
class _Orchestrator:
def get_download_status(self, download_id):
assert download_id == 'dl1'
return _Record()
deps, submitted = _build_deps(
download_orchestrator=_Orchestrator(),
run_async=lambda value: value,
)
download_tasks['t1'] = {
'track_index': 0,
'status': 'downloading',
'track_info': {},
'filename': 'song.flac',
'username': 'torrent',
'download_id': 'dl1',
}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['tasks'][0]['status'] == 'post_processing'
assert download_tasks['t1']['status'] == 'post_processing'
assert submitted == [('t1', 'b1')]
def test_auto_torrent_prefers_engine_success_over_live_entry():
class _Record:
state = 'Completed, Succeeded'
progress = 100
class _Orchestrator:
def get_download_status(self, download_id):
assert download_id == 'dl1'
return _Record()
deps, submitted = _build_deps(
download_orchestrator=_Orchestrator(),
run_async=lambda value: value,
)
download_tasks['t1'] = {
'track_index': 0,
'status': 'downloading',
'track_info': {},
'filename': 'song.flac',
'username': 'torrent',
'download_id': 'dl1',
}
live = {'torrent::song.flac': {
'state': 'InProgress, Downloading',
'percentComplete': 100,
}}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, live, deps)
assert out['tasks'][0]['status'] == 'post_processing'
assert download_tasks['t1']['status'] == 'post_processing'
assert submitted == [('t1', 'b1')]
def test_auto_torrent_engine_failure_does_not_bypass_monitor_retry():
class _Record:
state = 'Completed, Errored'
progress = 100
error = 'client failed'
class _Orchestrator:
def get_download_status(self, download_id):
assert download_id == 'dl1'
return _Record()
deps, submitted = _build_deps(
download_orchestrator=_Orchestrator(),
run_async=lambda value: value,
)
download_tasks['t1'] = {
'track_index': 0,
'status': 'downloading',
'track_info': {},
'filename': 'song.flac',
'username': 'torrent',
'download_id': 'dl1',
}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['tasks'][0]['status'] == 'downloading'
assert download_tasks['t1']['status'] == 'downloading'
assert submitted == []
def test_auto_torrent_engine_success_recovers_premature_failed_task():
class _Record:
state = 'Completed, Succeeded'
progress = 100
class _Orchestrator:
def get_download_status(self, download_id):
assert download_id == 'dl1'
return _Record()
deps, submitted = _build_deps(
download_orchestrator=_Orchestrator(),
run_async=lambda value: value,
)
download_tasks['t1'] = {
'track_index': 0,
'status': 'failed',
'track_info': {},
'filename': 'release-url||Release',
'username': 'torrent',
'download_id': 'dl1',
'error_message': 'premature failure',
}
batch = {'phase': 'downloading', 'queue': ['t1']}
out = st.build_batch_status_data('b1', batch, {}, deps)
assert out['tasks'][0]['status'] == 'post_processing'
assert download_tasks['t1']['status'] == 'post_processing'
assert submitted == [('t1', 'b1')]
# ---------------------------------------------------------------------------
# Safety valve (stuck task handling)
# ---------------------------------------------------------------------------
def test_safety_valve_stuck_searching_marks_not_found():
deps, _ = _build_deps(config=_FakeConfig({'soulseek.download_timeout': 1}))
download_tasks['t1'] = {
'track_index': 0, 'status': 'searching', 'track_info': {},
'status_change_time': 0, # ancient
}
batch = {'phase': 'downloading', 'queue': ['t1']}
st.build_batch_status_data('b1', batch, {}, deps)
assert download_tasks['t1']['status'] == 'not_found'
assert 'Search stuck' in download_tasks['t1']['error_message']
def test_safety_valve_stuck_downloading_with_recovered_file_routes_to_post_processing():
deps, submitted = _build_deps(
config=_FakeConfig({'soulseek.download_timeout': 1, 'soulseek.download_path': '/d', 'soulseek.transfer_path': '/t'}),
find_completed=lambda *a, **kw: ('/found.flac', 'transfer'),
)
download_tasks['t1'] = {
'track_index': 0, 'status': 'downloading', 'track_info': {},
'filename': 'song.flac',
'status_change_time': 0,
}
batch = {'phase': 'downloading', 'queue': ['t1']}
st.build_batch_status_data('b1', batch, {}, deps)
assert download_tasks['t1']['status'] == 'post_processing'
assert submitted == [('t1', 'b1')]
def test_safety_valve_stuck_downloading_no_file_marks_failed():
deps, _ = _build_deps(config=_FakeConfig({'soulseek.download_timeout': 1}))
download_tasks['t1'] = {
'track_index': 0, 'status': 'downloading', 'track_info': {},
'filename': 'song.flac',
'status_change_time': 0,
}
batch = {'phase': 'downloading', 'queue': ['t1']}
st.build_batch_status_data('b1', batch, {}, deps)
assert download_tasks['t1']['status'] == 'failed'
assert 'Task stuck' in download_tasks['t1']['error_message']
# ---------------------------------------------------------------------------
# build_single_batch_status (route helper)
# ---------------------------------------------------------------------------
def test_single_batch_status_404_for_unknown_batch():
deps, _ = _build_deps()
body, status = st.build_single_batch_status('absent', deps)
assert status == 404
assert body['error'] == 'Batch not found'
def test_single_batch_status_returns_payload_for_known_batch():
deps, _ = _build_deps()
download_batches['b1'] = {'phase': 'unknown', 'playlist_id': 'pl1'}
body, status = st.build_single_batch_status('b1', deps)
assert status == 200
assert body['phase'] == 'unknown'
assert body['playlist_id'] == 'pl1'
# ---------------------------------------------------------------------------
# build_batched_status (route helper)
# ---------------------------------------------------------------------------
def test_batched_status_filters_to_requested_ids():
deps, _ = _build_deps()
download_batches['b1'] = {'phase': 'unknown'}
download_batches['b2'] = {'phase': 'unknown'}
download_batches['b3'] = {'phase': 'unknown'}
out = st.build_batched_status(['b1', 'b3'], deps)
assert set(out['batches'].keys()) == {'b1', 'b3'}
def test_batched_status_no_filter_returns_all_batches():
deps, _ = _build_deps()
download_batches['b1'] = {'phase': 'unknown'}
download_batches['b2'] = {'phase': 'unknown'}
out = st.build_batched_status([], deps)
assert set(out['batches'].keys()) == {'b1', 'b2'}
def test_unified_downloads_response_includes_album_bundle_summary():
deps, _ = _build_deps()
download_batches['b1'] = {
'phase': 'album_downloading',
'playlist_id': 'pl1',
'playlist_name': 'GNX',
'queue': [],
'album_bundle_state': 'downloading',
'album_bundle_source': 'torrent',
'album_bundle_progress': 75,
}
out = st.build_unified_downloads_response(300, deps)
assert out['batches'][0]['album_bundle'] == {
'state': 'downloading',
'source': 'torrent',
'progress': 75,
'progress_percent': 75,
}
def test_batched_status_metadata_present():
deps, _ = _build_deps()
download_batches['b1'] = {'phase': 'unknown'}
out = st.build_batched_status([], deps)
assert out['metadata']['total_batches'] == 1
assert out['metadata']['requested_batch_ids'] == []
assert isinstance(out['metadata']['timestamp'], (int, float))
def test_batched_status_per_batch_failure_isolated():
deps, _ = _build_deps()
# b1 valid, b2 raises
download_batches['b1'] = {'phase': 'downloading', 'queue': []}
class _BadBatch(dict):
def get(self, k, default=None):
if k == 'phase':
raise RuntimeError("batch boom")
return super().get(k, default)
download_batches['b2'] = _BadBatch()
out = st.build_batched_status([], deps)
assert 'error' in out['batches']['b2']
assert out['batches']['b1'].get('phase') == 'downloading'
def test_batched_status_debug_info_pre_existing_bug_never_populates():
"""Documents a pre-existing bug: every batch payload includes
`"error": batch.get('error')` (key always present, value usually None).
The debug_info loop checks `if "error" not in batch_status:` which is
therefore always False → debug_info stays empty in production.
Lift preserves this exactly. A future PR can flip the check to
`if batch_status.get('error') is None` to fix it.
"""
deps, _ = _build_deps()
download_tasks['t1'] = {'track_index': 0, 'status': 'downloading', 'track_info': {}}
download_tasks['t2'] = {'track_index': 1, 'status': 'downloading', 'track_info': {}}
download_batches['b1'] = {
'phase': 'downloading', 'queue': ['t1', 't2'],
'active_count': 5,
'max_concurrent': 5,
}
out = st.build_batched_status([], deps)
# Bug: stays empty because "error" key is always present in payload
assert out['debug_info'] == {}
# ---------------------------------------------------------------------------
# build_unified_downloads_response
# ---------------------------------------------------------------------------
def test_unified_response_sorts_by_priority_then_recency():
deps, _ = _build_deps()
download_tasks['old_complete'] = {
'track_index': 0, 'status': 'completed', 'track_info': {'name': 'A'},
'status_change_time': 100,
}
download_tasks['new_active'] = {
'track_index': 1, 'status': 'downloading', 'track_info': {'name': 'B'},
'status_change_time': 50,
}
out = st.build_unified_downloads_response(100, deps)
# Active downloads come first regardless of timestamp
assert out['downloads'][0]['title'] == 'B'
def test_unified_response_artist_list_of_dicts_normalized():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'queued',
'track_info': {'name': 'X', 'artists': [{'name': 'Pink Floyd'}, {'name': 'Roger'}]},
}
out = st.build_unified_downloads_response(100, deps)
assert out['downloads'][0]['artist'] == 'Pink Floyd, Roger'
def test_unified_response_album_dict_extracted_to_name():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'queued',
'track_info': {'name': 'X', 'album': {'name': 'DSOTM', 'images': [{'url': 'http://thumb.jpg'}]}},
}
out = st.build_unified_downloads_response(100, deps)
assert out['downloads'][0]['album'] == 'DSOTM'
assert out['downloads'][0]['artwork'] == 'http://thumb.jpg'
def test_unified_response_completed_task_progress_is_100():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'completed',
'track_info': {'name': 'X'},
}
out = st.build_unified_downloads_response(100, deps)
assert out['downloads'][0]['progress'] == 100
def test_unified_response_post_processing_progress_is_95():
deps, _ = _build_deps()
download_tasks['t1'] = {
'track_index': 0, 'status': 'post_processing',
'track_info': {'name': 'X'},
}
out = st.build_unified_downloads_response(100, deps)
assert out['downloads'][0]['progress'] == 95
def test_unified_response_includes_batch_summaries():
deps, _ = _build_deps()
download_tasks['t1'] = {'track_index': 0, 'status': 'completed', 'track_info': {}}
download_tasks['t2'] = {'track_index': 1, 'status': 'failed', 'track_info': {}}
download_tasks['t3'] = {'track_index': 2, 'status': 'downloading', 'track_info': {}}
download_tasks['t4'] = {'track_index': 3, 'status': 'queued', 'track_info': {}}
download_batches['b1'] = {
'phase': 'downloading', 'playlist_name': 'PL',
'queue': ['t1', 't2', 't3', 't4'],
}
out = st.build_unified_downloads_response(100, deps)
bs = out['batches'][0]
assert bs['total'] == 4
assert bs['completed'] == 1
assert bs['failed'] == 1
assert bs['active'] == 1
assert bs['queued'] == 1
def test_unified_response_respects_limit():
deps, _ = _build_deps()
for i in range(20):
download_tasks[f't{i}'] = {
'track_index': i, 'status': 'completed', 'track_info': {},
}
out = st.build_unified_downloads_response(5, deps)
assert len(out['downloads']) == 5
assert out['total'] == 20 # total still reflects all
def test_unified_response_includes_persistent_download_history():
deps, _ = _build_deps(
persistent_history=lambda limit: [
{
'id': 42,
'title': 'Persisted Track',
'artist_name': 'Deezer Artist',
'album_name': 'Persistent Album',
'thumb_url': 'http://cover.jpg',
'download_source': 'Deezer',
'quality': 'FLAC',
'created_at': '2026-05-24 12:34:56',
}
]
)
out = st.build_unified_downloads_response(100, deps)
assert out['total'] == 1
item = out['downloads'][0]
assert item['task_id'] == 'history-42'
assert item['title'] == 'Persisted Track'
assert item['artist'] == 'Deezer Artist'
assert item['album'] == 'Persistent Album'
assert item['artwork'] == 'http://cover.jpg'
assert item['status'] == 'completed'
assert item['progress'] == 100
assert item['batch_name'] == 'Deezer'
assert item['is_persistent_history'] is True
def test_unified_response_dedupes_history_against_live_task():
download_tasks['live'] = {
'track_index': 0,
'status': 'completed',
'track_info': {
'name': 'Same Track',
'artist': 'Same Artist',
'album': 'Same Album',
},
}
deps, _ = _build_deps(
persistent_history=lambda limit: [
{
'id': 7,
'title': 'Same Track',
'artist_name': 'Same Artist',
'album_name': 'Same Album',
'created_at': '2026-05-24 12:34:56',
}
]
)
out = st.build_unified_downloads_response(100, deps)
assert len(out['downloads']) == 1
assert out['downloads'][0]['task_id'] == 'live'
assert out['downloads'][0]['is_persistent_history'] is False
def test_unified_response_caps_persistent_history_tail():
requested_limits = []
def _history(limit):
requested_limits.append(limit)
return [
{
'id': i,
'title': f'Track {i}',
'artist_name': 'Artist',
'album_name': 'Album',
'created_at': '2026-05-24 12:34:56',
}
for i in range(limit + 10)
]
deps, _ = _build_deps(persistent_history=_history)
out = st.build_unified_downloads_response(300, deps)
assert requested_limits == [50]
assert len(out['downloads']) == 50
assert out['total'] == 50