Files
Harvey e84f2d6127 feat: robust library sync with adaptive watchdog, resume-on-failure &… (#22)
* feat: robust library sync with adaptive watchdog, resume-on-failure & parallel pre-warming

* update copy
2026-04-05 15:36:42 +01:00

115 lines
3.7 KiB
Python

"""Tests for sync resume-on-failure behaviour."""
import asyncio
import sqlite3
import tempfile
import os
import threading
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from infrastructure.persistence.sync_state_store import SyncStateStore
from services.cache_status_service import CacheStatusService
@pytest.fixture(autouse=True)
def _reset_singleton():
"""Reset CacheStatusService singleton between tests."""
CacheStatusService._instance = None
yield
CacheStatusService._instance = None
class TestResumeOnFailure:
@pytest.mark.asyncio
async def test_complete_sync_preserves_state_on_failure(self):
"""On failure, sync state should be saved but NOT cleared."""
store = AsyncMock()
store.save_sync_state = AsyncMock()
store.clear_sync_state = AsyncMock()
store.clear_processed_items = AsyncMock()
svc = CacheStatusService(store)
await svc.start_sync('artists', 100)
await svc.update_progress(50, "some artist")
await svc.complete_sync("Sync stalled: no progress")
store.save_sync_state.assert_called()
last_call = store.save_sync_state.call_args
assert last_call.kwargs.get('status') == 'failed'
store.clear_sync_state.assert_not_called()
store.clear_processed_items.assert_not_called()
assert True
@pytest.mark.asyncio
async def test_complete_sync_clears_state_on_success(self):
"""On success, both sync_state and processed_items should be cleared."""
store = AsyncMock()
store.save_sync_state = AsyncMock()
store.clear_sync_state = AsyncMock()
store.clear_processed_items = AsyncMock()
svc = CacheStatusService(store)
await svc.start_sync('artists', 10)
await svc.update_progress(10, "done")
await svc.complete_sync(None)
store.clear_sync_state.assert_called_once()
store.clear_processed_items.assert_called_once()
assert True
@pytest.mark.asyncio
async def test_last_progress_at_updates_on_progress(self):
"""get_last_progress_at should reflect the latest update_progress call."""
store = AsyncMock()
store.save_sync_state = AsyncMock()
svc = CacheStatusService(store)
await svc.start_sync('artists', 10)
t1 = svc.get_last_progress_at()
await asyncio.sleep(0.05)
await svc.update_progress(5, "artist5")
t2 = svc.get_last_progress_at()
assert t2 > t1
assert True
@pytest.mark.asyncio
async def test_last_progress_at_updates_on_phase_change(self):
"""get_last_progress_at should refresh when phase changes."""
store = AsyncMock()
store.save_sync_state = AsyncMock()
svc = CacheStatusService(store)
await svc.start_sync('artists', 10)
t1 = svc.get_last_progress_at()
await asyncio.sleep(0.05)
await svc.update_phase('albums', 50)
t2 = svc.get_last_progress_at()
assert t2 > t1
assert True
class TestSyncStateStoreClear:
@pytest.mark.asyncio
async def test_clear_processed_items_deletes_all(self):
"""clear_processed_items should execute a DELETE on processed_items."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "test.db")
write_lock = threading.Lock()
store = SyncStateStore(db_path, write_lock)
await store.mark_items_processed_batch("artist", ["mbid1", "mbid2"])
items = await store.get_processed_items("artist")
assert len(items) == 2
await store.clear_processed_items()
items = await store.get_processed_items("artist")
assert len(items) == 0
assert True