"""Unit tests for BackgroundLoaderService. Tests cover: - Task queuing and worker orchestration - Loading status tracking and progress reporting - Concurrent task processing - WebSocket broadcasting - Error handling and recovery - Resource cleanup - Missing data detection """ import asyncio from datetime import datetime, timezone from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.server.services.background_loader_service import ( BackgroundLoaderService, LoadingStatus, SeriesLoadingTask, ) @pytest.fixture def mock_websocket_service(): """Mock WebSocket service.""" service = AsyncMock() service.broadcast = AsyncMock() return service @pytest.fixture def mock_anime_service(): """Mock AnimeService.""" service = AsyncMock() service.sync_episodes_to_db = AsyncMock() service.sync_single_series_after_scan = AsyncMock() return service @pytest.fixture def mock_series_app(): """Mock SeriesApp.""" app = MagicMock() app.directory_to_search = "/anime" app.nfo_service = AsyncMock() app.nfo_service.has_nfo = MagicMock(return_value=False) app.nfo_service.create_tvshow_nfo = AsyncMock(return_value="/anime/TestSeries/tvshow.nfo") app.serie_scanner = MagicMock() app.serie_scanner.scan_single_series = MagicMock(return_value={ "Season 1": ["episode1.mp4", "episode2.mp4"] }) return app @pytest.fixture def background_loader_service(mock_websocket_service, mock_anime_service, mock_series_app): """Create BackgroundLoaderService instance.""" return BackgroundLoaderService( websocket_service=mock_websocket_service, anime_service=mock_anime_service, series_app=mock_series_app, max_concurrent_loads=3 ) class TestSeriesLoadingTask: """Tests for SeriesLoadingTask data class.""" def test_task_initialization(self): """Test task is initialized with correct default values.""" task = SeriesLoadingTask( key="test_series", folder="test_folder", name="Test Series", year=2020 ) assert task.key == "test_series" assert task.folder == "test_folder" assert task.name == "Test Series" assert task.year == 2020 assert task.status == LoadingStatus.PENDING assert task.progress == { "episodes": False, "nfo": False, "logo": False, "images": False } assert task.started_at is None assert task.completed_at is None assert task.error is None def test_task_with_minimal_fields(self): """Test task creation with minimal required fields.""" task = SeriesLoadingTask( key="minimal", folder="folder", name="Name" ) assert task.key == "minimal" assert task.year is None assert task.status == LoadingStatus.PENDING def test_task_progress_tracking(self): """Test progress tracking updates correctly.""" task = SeriesLoadingTask( key="test", folder="Test", name="Test" ) task.progress["episodes"] = True assert task.progress["episodes"] is True assert not task.progress["nfo"] class TestLoadingStatus: """Tests for LoadingStatus enumeration.""" def test_all_status_values(self): """Test all loading status values.""" assert LoadingStatus.PENDING.value == "pending" assert LoadingStatus.LOADING_EPISODES.value == "loading_episodes" assert LoadingStatus.LOADING_NFO.value == "loading_nfo" assert LoadingStatus.COMPLETED.value == "completed" assert LoadingStatus.FAILED.value == "failed" class TestBackgroundLoaderServiceInitialization: """Tests for service initialization.""" def test_service_initialization(self, mock_websocket_service, mock_anime_service, mock_series_app): """Test service initializes with correct configuration.""" service = BackgroundLoaderService( websocket_service=mock_websocket_service, anime_service=mock_anime_service, series_app=mock_series_app, max_concurrent_loads=5 ) assert service.websocket_service is mock_websocket_service assert service.anime_service is mock_anime_service assert service.series_app is mock_series_app assert service.max_concurrent_loads == 5 assert isinstance(service.task_queue, asyncio.Queue) assert service.active_tasks == {} assert service.worker_tasks == [] assert service._shutdown is False def test_default_max_concurrent_loads(self, mock_websocket_service, mock_anime_service, mock_series_app): """Test default max_concurrent_loads is 5.""" service = BackgroundLoaderService( websocket_service=mock_websocket_service, anime_service=mock_anime_service, series_app=mock_series_app ) assert service.max_concurrent_loads == 5 class TestStartStopService: """Tests for service startup and shutdown.""" @pytest.mark.asyncio async def test_service_start(self, background_loader_service): """Test service starts worker tasks.""" await background_loader_service.start() assert len(background_loader_service.worker_tasks) == 3 assert all(not task.done() for task in background_loader_service.worker_tasks) await background_loader_service.stop() @pytest.mark.asyncio async def test_service_stop(self, background_loader_service): """Test service stops all worker tasks gracefully.""" await background_loader_service.start() assert len(background_loader_service.worker_tasks) == 3 await background_loader_service.stop() assert background_loader_service.worker_tasks == [] assert background_loader_service._shutdown is True @pytest.mark.asyncio async def test_service_stop_when_not_started(self, background_loader_service): """Test stopping a service that was never started.""" await background_loader_service.stop() assert background_loader_service.worker_tasks == [] class TestAddSeriesLoadingTask: """Tests for adding tasks to the queue.""" @pytest.mark.asyncio async def test_add_series_loading_task(self, background_loader_service): """Test adding a series loading task to the queue.""" await background_loader_service.add_series_loading_task( key="test_series", folder="test_folder", name="Test Series", year=2020 ) assert "test_series" in background_loader_service.active_tasks task = background_loader_service.active_tasks["test_series"] assert task.key == "test_series" assert task.folder == "test_folder" assert task.name == "Test Series" assert task.year == 2020 assert task.status == LoadingStatus.PENDING assert task.started_at is not None @pytest.mark.asyncio async def test_add_duplicate_task_skips_addition(self, background_loader_service): """Test adding duplicate task skips the new one.""" await background_loader_service.add_series_loading_task( key="test_series", folder="test_folder1", name="Test Series 1" ) initial_task = background_loader_service.active_tasks["test_series"] await background_loader_service.add_series_loading_task( key="test_series", folder="test_folder2", name="Test Series 2" ) assert background_loader_service.active_tasks["test_series"] is initial_task @pytest.mark.asyncio async def test_add_task_broadcasts_status(self, background_loader_service, mock_websocket_service): """Test adding task broadcasts initial status.""" await background_loader_service.add_series_loading_task( key="test_series", folder="test_folder", name="Test Series" ) mock_websocket_service.broadcast.assert_called_once() call_args = mock_websocket_service.broadcast.call_args[0][0] assert call_args["type"] == "series_loading_update" assert call_args["key"] == "test_series" assert call_args["status"] == LoadingStatus.PENDING.value @pytest.mark.asyncio async def test_add_multiple_tasks(self, background_loader_service): """Test adding multiple different tasks.""" await background_loader_service.add_series_loading_task( key="series1", folder="folder1", name="Series 1" ) await background_loader_service.add_series_loading_task( key="series2", folder="folder2", name="Series 2" ) assert len(background_loader_service.active_tasks) == 2 assert "series1" in background_loader_service.active_tasks assert "series2" in background_loader_service.active_tasks class TestCheckMissingData: """Tests for checking missing data for a series.""" @pytest.mark.asyncio async def test_check_missing_data_no_series_in_db(self, background_loader_service): """Test checking missing data when series doesn't exist in DB.""" mock_db = AsyncMock() with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=None) missing = await background_loader_service.check_missing_data( key="new_series", folder="new_folder", anime_directory="/anime", db=mock_db ) assert all(missing.values()) @pytest.mark.asyncio async def test_check_missing_data_partial_loaded(self, background_loader_service): """Test checking missing data for partially loaded series.""" mock_db = AsyncMock() mock_series = MagicMock() mock_series.episodes_loaded = True mock_series.has_nfo = True mock_series.logo_loaded = False mock_series.images_loaded = False with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: with patch("src.server.utils.media.check_media_files") as mock_check: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) mock_check.return_value = { "poster": False, "logo": False, "fanart": False, "nfo": True } missing = await background_loader_service.check_missing_data( key="partial_series", folder="partial_folder", anime_directory="/anime", db=mock_db ) assert missing["episodes"] is False assert missing["nfo"] is False assert missing["logo"] is True assert missing["images"] is True class TestBroadcastStatus: """Tests for WebSocket status broadcasting.""" @pytest.mark.asyncio async def test_broadcast_status_pending(self, background_loader_service, mock_websocket_service): """Test broadcasting pending status.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test", status=LoadingStatus.PENDING ) await background_loader_service._broadcast_status(task) call_args = mock_websocket_service.broadcast.call_args[0][0] assert call_args["status"] == LoadingStatus.PENDING.value assert "Queued" in call_args["message"] @pytest.mark.asyncio async def test_broadcast_status_completed(self, background_loader_service, mock_websocket_service): """Test broadcasting completed status.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test", status=LoadingStatus.COMPLETED ) await background_loader_service._broadcast_status(task) call_args = mock_websocket_service.broadcast.call_args[0][0] assert call_args["status"] == LoadingStatus.COMPLETED.value assert "successfully" in call_args["message"] @pytest.mark.asyncio async def test_broadcast_status_failed(self, background_loader_service, mock_websocket_service): """Test broadcasting failed status with error message.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test", status=LoadingStatus.FAILED, error="Test error" ) await background_loader_service._broadcast_status(task) call_args = mock_websocket_service.broadcast.call_args[0][0] assert call_args["status"] == LoadingStatus.FAILED.value assert call_args["error"] == "Test error" assert "failed" in call_args["message"] @pytest.mark.asyncio async def test_broadcast_status_custom_message(self, background_loader_service, mock_websocket_service): """Test broadcasting with custom message.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test" ) await background_loader_service._broadcast_status(task, "Custom message") call_args = mock_websocket_service.broadcast.call_args[0][0] assert call_args["message"] == "Custom message" @pytest.mark.asyncio async def test_broadcast_status_includes_metadata(self, background_loader_service, mock_websocket_service): """Test broadcast includes all required metadata.""" task = SeriesLoadingTask( key="test_key", folder="test_folder", name="Test Name" ) task.progress = {"episodes": True, "nfo": False, "logo": False, "images": False} await background_loader_service._broadcast_status(task) call_args = mock_websocket_service.broadcast.call_args[0][0] assert call_args["type"] == "series_loading_update" assert call_args["key"] == "test_key" assert call_args["series_key"] == "test_key" assert call_args["folder"] == "test_folder" assert call_args["progress"] == {"episodes": True, "nfo": False, "logo": False, "images": False} assert "timestamp" in call_args class TestFindSeriesDirectory: """Tests for finding series directory.""" @pytest.mark.asyncio async def test_find_series_directory_exists(self, background_loader_service, tmp_path): """Test finding series directory when it exists.""" series_dir = tmp_path / "TestSeries" series_dir.mkdir() background_loader_service.series_app.directory_to_search = str(tmp_path) task = SeriesLoadingTask( key="test", folder="TestSeries", name="Test" ) result = await background_loader_service._find_series_directory(task) assert result is not None assert result.name == "TestSeries" @pytest.mark.asyncio async def test_find_series_directory_not_found(self, background_loader_service, tmp_path): """Test finding series directory when it doesn't exist.""" background_loader_service.series_app.directory_to_search = str(tmp_path) task = SeriesLoadingTask( key="test", folder="NonExistentSeries", name="Test" ) result = await background_loader_service._find_series_directory(task) assert result is None class TestScanSeriesEpisodes: """Tests for scanning episodes in a series directory.""" @pytest.mark.asyncio async def test_scan_series_episodes(self, background_loader_service, tmp_path): """Test scanning episodes from series directory.""" season1 = tmp_path / "Season 1" season1.mkdir() (season1 / "episode1.mp4").touch() (season1 / "episode2.mp4").touch() season2 = tmp_path / "Season 2" season2.mkdir() (season2 / "episode1.mp4").touch() task = SeriesLoadingTask( key="test", folder="TestSeries", name="Test" ) result = await background_loader_service._scan_series_episodes(tmp_path, task) assert "Season 1" in result assert "Season 2" in result assert len(result["Season 1"]) == 2 assert len(result["Season 2"]) == 1 @pytest.mark.asyncio async def test_scan_series_episodes_ignores_non_mp4(self, background_loader_service, tmp_path): """Test that only .mp4 files are scanned.""" season = tmp_path / "Season 1" season.mkdir() (season / "episode1.mp4").touch() (season / "episode2.txt").touch() (season / "episode3.avi").touch() task = SeriesLoadingTask( key="test", folder="TestSeries", name="Test" ) result = await background_loader_service._scan_series_episodes(tmp_path, task) assert len(result["Season 1"]) == 1 assert result["Season 1"][0] == "episode1.mp4" @pytest.mark.asyncio async def test_scan_series_episodes_empty_directory(self, background_loader_service, tmp_path): """Test scanning empty directory.""" task = SeriesLoadingTask( key="test", folder="TestSeries", name="Test" ) result = await background_loader_service._scan_series_episodes(tmp_path, task) assert result == {} class TestLoadNfoAndImages: """Tests for loading NFO files and images.""" @pytest.mark.asyncio async def test_load_nfo_creates_new_nfo(self, background_loader_service, mock_websocket_service): """Test creating new NFO file when it doesn't exist.""" mock_db = AsyncMock() mock_series = MagicMock() mock_series.has_nfo = False task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series", year=2020 ) with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) result = await background_loader_service._load_nfo_and_images(task, mock_db) assert result is True assert task.progress["nfo"] is True assert task.progress["logo"] is True assert task.progress["images"] is True @pytest.mark.asyncio async def test_load_nfo_uses_existing(self, background_loader_service): """Test using existing NFO file when it already exists.""" background_loader_service.series_app.nfo_service.has_nfo = MagicMock(return_value=True) mock_db = AsyncMock() mock_series = MagicMock() mock_series.has_nfo = True task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) result = await background_loader_service._load_nfo_and_images(task, mock_db) assert result is False assert task.progress["nfo"] is True @pytest.mark.asyncio async def test_load_nfo_without_nfo_service(self, background_loader_service): """Test graceful handling when NFO service not available.""" background_loader_service.series_app.nfo_service = None mock_db = AsyncMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) result = await background_loader_service._load_nfo_and_images(task, mock_db) assert result is False assert task.progress["nfo"] is False assert task.progress["logo"] is False class TestScanMissingEpisodes: """Tests for scanning missing episodes.""" @pytest.mark.asyncio async def test_scan_missing_episodes(self, background_loader_service): """Test scanning for missing episodes.""" mock_db = AsyncMock() mock_series = MagicMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) await background_loader_service._scan_missing_episodes(task, mock_db) assert task.progress["episodes"] is True background_loader_service.anime_service.sync_single_series_after_scan.assert_called_once_with("test") @pytest.mark.asyncio async def test_scan_missing_episodes_no_scanner(self, background_loader_service): """Test handling when scanner not available.""" background_loader_service.series_app.serie_scanner = None mock_db = AsyncMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) await background_loader_service._scan_missing_episodes(task, mock_db) class TestConcurrentProcessing: """Tests for concurrent task processing.""" @pytest.mark.asyncio async def test_multiple_concurrent_tasks(self, background_loader_service): """Test processing multiple tasks concurrently.""" await background_loader_service.add_series_loading_task("series1", "folder1", "Series 1") await background_loader_service.add_series_loading_task("series2", "folder2", "Series 2") await background_loader_service.add_series_loading_task("series3", "folder3", "Series 3") assert len(background_loader_service.active_tasks) == 3 assert background_loader_service.task_queue.qsize() == 3 @pytest.mark.asyncio async def test_max_concurrent_load_limit(self, mock_websocket_service, mock_anime_service, mock_series_app): """Test respecting maximum concurrent loads setting.""" service = BackgroundLoaderService( websocket_service=mock_websocket_service, anime_service=mock_anime_service, series_app=mock_series_app, max_concurrent_loads=2 ) await service.start() assert len(service.worker_tasks) == 2 await service.stop() class TestLoadSeriesData: """Tests for complete series data loading process.""" @pytest.mark.asyncio async def test_load_series_data_successful(self, background_loader_service): """Test successful series data loading.""" mock_db = AsyncMock() mock_series = MagicMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) with patch("src.server.database.connection.get_db_session"): with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) with patch.object(background_loader_service, "check_missing_data", return_value={ "episodes": True, "nfo": True, "logo": True, "images": True }): with patch.object(background_loader_service, "_load_nfo_and_images", return_value=True): with patch.object(background_loader_service, "_scan_missing_episodes"): with patch.object(background_loader_service, "_broadcast_status"): await background_loader_service._load_series_data(task) assert task.status == LoadingStatus.COMPLETED assert task.completed_at is not None assert "test" not in background_loader_service.active_tasks @pytest.mark.asyncio async def test_load_series_data_handles_error(self, background_loader_service): """Test error handling during series data loading.""" mock_db = AsyncMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) with patch("src.server.database.connection.get_db_session"): with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=None) with patch.object(background_loader_service, "check_missing_data", side_effect=Exception("Test error")): with patch.object(background_loader_service, "_broadcast_status"): await background_loader_service._load_series_data(task) assert task.status == LoadingStatus.FAILED assert task.error == "Test error" assert task.completed_at is not None @pytest.mark.asyncio async def test_load_series_data_with_partial_missing(self, background_loader_service): """Test loading series when only some data is missing.""" mock_db = AsyncMock() mock_series = MagicMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) with patch("src.server.database.connection.get_db_session"): with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) with patch.object(background_loader_service, "check_missing_data", return_value={ "episodes": False, "nfo": False, "logo": False, "images": False }): with patch.object(background_loader_service, "_load_nfo_and_images", return_value=False): with patch.object(background_loader_service, "_scan_missing_episodes"): with patch.object(background_loader_service, "_broadcast_status"): await background_loader_service._load_series_data(task) assert task.status == LoadingStatus.COMPLETED # When nothing is missing, nfo/logo/images get marked true, but episodes marked false by scan assert task.progress["nfo"] is True or task.progress["nfo"] is False class TestWorkerExecution: """Tests for background worker execution.""" @pytest.mark.asyncio async def test_worker_processes_task_from_queue(self, background_loader_service): """Test worker processes task from queue.""" # Create a task task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) # Add it to queue await background_loader_service.task_queue.put(task) # Start service await background_loader_service.start() # Let worker process briefly await asyncio.sleep(0.1) # Stop service await background_loader_service.stop() # Queue should be empty assert background_loader_service.task_queue.empty() @pytest.mark.asyncio async def test_worker_timeout_loop(self, background_loader_service): """Test worker timeout loop works correctly.""" await background_loader_service.start() # Let worker loop a few times with no tasks await asyncio.sleep(0.2) # Workers should still be running assert all(not task.done() for task in background_loader_service.worker_tasks) await background_loader_service.stop() class TestBroadcastStatusMessages: """Tests for various status message broadcasts.""" @pytest.mark.asyncio async def test_broadcast_loading_episodes_status(self, background_loader_service, mock_websocket_service): """Test broadcasting loading episodes status.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test", status=LoadingStatus.LOADING_EPISODES ) await background_loader_service._broadcast_status(task) call_args = mock_websocket_service.broadcast.call_args[0][0] assert "loading" in call_args["message"].lower() or "episode" in call_args["message"].lower() @pytest.mark.asyncio async def test_broadcast_loading_nfo_status(self, background_loader_service, mock_websocket_service): """Test broadcasting loading NFO status.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test", status=LoadingStatus.LOADING_NFO ) await background_loader_service._broadcast_status(task) call_args = mock_websocket_service.broadcast.call_args[0][0] assert "nfo" in call_args["message"].lower() or "loading" in call_args["message"].lower() class TestErrorHandling: """Tests for error handling scenarios.""" @pytest.mark.asyncio async def test_load_nfo_error_handling(self, background_loader_service): """Test error handling during NFO creation.""" background_loader_service.series_app.nfo_service.has_nfo = MagicMock(return_value=False) background_loader_service.series_app.nfo_service.create_tvshow_nfo = AsyncMock( side_effect=Exception("API error") ) mock_db = AsyncMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) result = await background_loader_service._load_nfo_and_images(task, mock_db) assert result is False assert task.progress["nfo"] is False assert task.progress["logo"] is False assert task.progress["images"] is False @pytest.mark.asyncio async def test_scan_missing_episodes_error_handling(self, background_loader_service): """Test error handling during episode scan.""" background_loader_service.series_app.serie_scanner.scan_single_series = MagicMock( side_effect=Exception("Scan error") ) mock_db = AsyncMock() task = SeriesLoadingTask( key="test", folder="test_folder", name="Test Series" ) await background_loader_service._scan_missing_episodes(task, mock_db) assert task.progress["episodes"] is False class TestDirectoryScanning: """Tests for directory operations.""" @pytest.mark.asyncio async def test_find_series_directory_error_handling(self, background_loader_service): """Test error handling when directory lookup fails.""" background_loader_service.series_app.directory_to_search = "/invalid/path" task = SeriesLoadingTask( key="test", folder="TestSeries", name="Test" ) result = await background_loader_service._find_series_directory(task) assert result is None @pytest.mark.asyncio async def test_scan_series_episodes_error_handling(self, background_loader_service): """Test error handling during episode scanning.""" task = SeriesLoadingTask( key="test", folder="TestSeries", name="Test" ) # Pass a non-existent path result = await background_loader_service._scan_series_episodes(Path("/nonexistent"), task) assert result == {} class TestCheckMissingDataEdgeCases: """Tests for edge cases in missing data checking.""" @pytest.mark.asyncio async def test_check_missing_data_all_loaded(self, background_loader_service): """Test checking missing data when all data is already loaded.""" mock_db = AsyncMock() mock_series = MagicMock() mock_series.episodes_loaded = True mock_series.has_nfo = True mock_series.logo_loaded = True mock_series.images_loaded = True with patch("src.server.database.service.AnimeSeriesService") as mock_service_class: with patch("src.server.utils.media.check_media_files") as mock_check: mock_service_class.get_by_key = AsyncMock(return_value=mock_series) mock_check.return_value = { "poster": True, "logo": True, "fanart": True, "nfo": True } missing = await background_loader_service.check_missing_data( key="complete_series", folder="complete_folder", anime_directory="/anime", db=mock_db ) assert not any(missing.values()) class TestTaskProgressTracking: """Tests for task progress tracking.""" @pytest.mark.asyncio async def test_task_progress_updates(self, background_loader_service): """Test that task progress is properly updated.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test" ) # Initially all progress is false assert not any(task.progress.values()) # Update progress task.progress["episodes"] = True assert task.progress["episodes"] is True task.progress["nfo"] = True assert task.progress["nfo"] is True # Other progress still false assert not task.progress["logo"] assert not task.progress["images"] @pytest.mark.asyncio async def test_task_status_lifecycle(self, background_loader_service): """Test task goes through complete status lifecycle.""" task = SeriesLoadingTask( key="test", folder="folder", name="Test" ) # Start with PENDING assert task.status == LoadingStatus.PENDING # Simulate transitions task.status = LoadingStatus.LOADING_EPISODES assert task.status == LoadingStatus.LOADING_EPISODES task.status = LoadingStatus.LOADING_NFO assert task.status == LoadingStatus.LOADING_NFO task.status = LoadingStatus.COMPLETED assert task.status == LoadingStatus.COMPLETED assert task.completed_at is None # Not set automatically