Переглянути джерело

fix(backend): repeat playlists when run_mode is loop (#138)

Auto-play and MQTT start playlists with run_mode=loop, but
run_theta_rho_files only restarted the playlist when run_mode was
indefinite (the value the manual Run Playlist dialog sends). As a
result, auto-play playlists ran a single pass and stopped instead of
looping.

Accept both repeat spellings in the restart check and add unit
coverage for the single/indefinite/loop run modes (loop fails without
the fix). Also drop two unused imports flagged by ruff in the touched
test file.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Andrey 4 днів тому
батько
коміт
51eee178a2
2 змінених файлів з 56 додано та 4 видалено
  1. 2 2
      modules/core/pattern_manager.py
  2. 54 2
      tests/unit/test_pattern_manager.py

+ 2 - 2
modules/core/pattern_manager.py

@@ -1685,8 +1685,8 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
                 state.skip_requested = False
                 idx += 1
 
-            if run_mode == "indefinite":
-                logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
+            if run_mode in ("indefinite", "loop"):
+                logger.info(f"Playlist completed. Restarting as per '{run_mode}' run mode")
                 if pause_time > 0:
                     # Clear current_playing_file to report "idle" state to MQTT/HA during pause
                     state.current_playing_file = None

+ 54 - 2
tests/unit/test_pattern_manager.py

@@ -7,9 +7,8 @@ Tests the core pattern file operations:
 - Error handling for invalid files
 - Listing pattern files
 """
-import os
 import pytest
-from unittest.mock import patch, MagicMock
+from unittest.mock import AsyncMock, patch
 
 
 class TestParseTheTaRhoFile:
@@ -350,3 +349,56 @@ class TestIsClearPattern:
         assert is_clear_pattern("./patterns/circle.thr") is False
         assert is_clear_pattern("./patterns/spiral.thr") is False
         assert is_clear_pattern("./patterns/custom/my_pattern.thr") is False
+
+
+class TestPlaylistRunModes:
+    """Tests for run_theta_rho_files repeat behavior.
+
+    The manual "Run Playlist" dialog sends run_mode="indefinite", while
+    auto-play and MQTT send run_mode="loop". Both must repeat the playlist;
+    "single" must stop after one pass.
+    """
+
+    PLAYLIST = ["a.thr", "b.thr"]
+
+    async def _run_playlist(self, mock_state, run_mode, stop_after):
+        """Run a playlist with a stubbed pattern executor.
+
+        Requests a stop once the executor has run stop_after patterns (so
+        repeat modes can't spin forever), then returns the executed files.
+        """
+        executed = []
+
+        async def fake_run_pattern(file_path, **kwargs):
+            executed.append(file_path)
+            if len(executed) >= stop_after:
+                mock_state.stop_requested = True
+
+        with patch("modules.core.pattern_manager.state", mock_state), \
+             patch("modules.core.pattern_manager.run_theta_rho_file",
+                   AsyncMock(side_effect=fake_run_pattern)), \
+             patch("modules.core.pattern_manager.broadcast_progress", AsyncMock()), \
+             patch("modules.core.pattern_manager.start_idle_led_timeout", AsyncMock()):
+            from modules.core.pattern_manager import run_theta_rho_files
+
+            await run_theta_rho_files(list(self.PLAYLIST), run_mode=run_mode)
+
+        return executed
+
+    async def test_run_mode_single_stops_after_one_pass(self, mock_state):
+        """A "single" playlist runs each pattern once and completes."""
+        executed = await self._run_playlist(mock_state, "single", stop_after=10)
+
+        assert executed == ["a.thr", "b.thr"]
+
+    async def test_run_mode_indefinite_restarts_playlist(self, mock_state):
+        """An "indefinite" playlist restarts after the last pattern."""
+        executed = await self._run_playlist(mock_state, "indefinite", stop_after=3)
+
+        assert executed == ["a.thr", "b.thr", "a.thr"]
+
+    async def test_run_mode_loop_restarts_playlist(self, mock_state):
+        """A "loop" playlist (auto-play/MQTT) restarts after the last pattern."""
+        executed = await self._run_playlist(mock_state, "loop", stop_after=3)
+
+        assert executed == ["a.thr", "b.thr", "a.thr"]