from pathlib import Path from unittest.mock import MagicMock import pytest from tonie_api.models import Chapter, CreativeTonie from toni_sync.config import ToniMapping from toni_sync.sync import apply_plan, build_plan, list_local_tracks, title_from_filename def make_tonie(chapters: list[Chapter]) -> CreativeTonie: return CreativeTonie( id="t1", householdId="h1", name="Test Tonie", imageUrl="", secondsRemaining=100, secondsPresent=0, chaptersRemaining=10, chaptersPresent=len(chapters), transcoding=False, lastUpdate=None, chapters=chapters, ) def make_chapter(title: str, chapter_id: str = "c1") -> Chapter: return Chapter(id=chapter_id, title=title, file="file1", seconds=10, transcoding=False) @pytest.fixture def folder(tmp_path: Path) -> Path: (tmp_path / "01 - First.mp3").write_bytes(b"data") (tmp_path / "02 - Second.mp3").write_bytes(b"data") (tmp_path / "notes.txt").write_text("ignore me") return tmp_path def test_list_local_tracks_filters_by_extension(folder: Path): tracks = list_local_tracks(folder) assert [t.name for t in tracks] == ["01 - First.mp3", "02 - Second.mp3"] def test_title_from_filename_strips_extension(folder: Path): track = folder / "01 - First.mp3" assert title_from_filename(track) == "01 - First" def test_list_local_tracks_missing_folder_raises(tmp_path: Path): with pytest.raises(FileNotFoundError): list_local_tracks(tmp_path / "missing") def test_build_plan_detects_upload_and_no_removal(folder: Path): tonie = make_tonie([make_chapter("01 - First")]) api = MagicMock() api.get_all_creative_tonies.return_value = [tonie] mapping = ToniMapping(name="m", household_id="h1", tonie_id="t1", folder=str(folder)) plan = build_plan(api, mapping) assert [p.name for p in plan.to_upload] == ["02 - Second.mp3"] assert plan.to_remove == [] assert plan.needs_changes is True def test_build_plan_prunes_removed_chapters(folder: Path): tonie = make_tonie([make_chapter("01 - First"), make_chapter("stale", "c2")]) api = MagicMock() api.get_all_creative_tonies.return_value = [tonie] mapping = ToniMapping(name="m", household_id="h1", tonie_id="t1", folder=str(folder)) plan = build_plan(api, mapping) assert [c.title for c in plan.to_remove] == ["stale"] def test_build_plan_no_prune_keeps_stale_chapters(folder: Path): tonie = make_tonie([make_chapter("01 - First"), make_chapter("stale", "c2")]) api = MagicMock() api.get_all_creative_tonies.return_value = [tonie] mapping = ToniMapping(name="m", household_id="h1", tonie_id="t1", folder=str(folder), prune=False) plan = build_plan(api, mapping) assert plan.to_remove == [] assert plan.final_order_titles == ["01 - First", "02 - Second", "stale"] def test_apply_plan_uploads_and_reorders(folder: Path): initial = make_tonie([make_chapter("01 - First", "c1")]) api = MagicMock() # get_all_creative_tonies is called twice: once in build_plan, once in apply_plan (refetch) after_upload = make_tonie( [make_chapter("01 - First", "c1"), make_chapter("02 - Second", "c2")] ) api.get_all_creative_tonies.side_effect = [[initial], [after_upload]] mapping = ToniMapping(name="m", household_id="h1", tonie_id="t1", folder=str(folder)) plan = build_plan(api, mapping) apply_plan(api, mapping, plan) api.upload_file_to_tonie.assert_called_once() args, _ = api.upload_file_to_tonie.call_args assert args[1].name == "02 - Second.mp3" assert args[2] == "02 - Second" # order already matches after upload -> no sort call needed in this case api.sort_chapter_of_tonie.assert_not_called() def test_apply_plan_reorders_when_order_differs(folder: Path): # Existing chapters are in reverse order relative to local files. initial = make_tonie( [make_chapter("02 - Second", "c2"), make_chapter("01 - First", "c1")] ) api = MagicMock() api.get_all_creative_tonies.side_effect = [[initial], [initial]] mapping = ToniMapping(name="m", household_id="h1", tonie_id="t1", folder=str(folder)) plan = build_plan(api, mapping) apply_plan(api, mapping, plan) api.upload_file_to_tonie.assert_not_called() api.sort_chapter_of_tonie.assert_called_once() _, ordered = api.sort_chapter_of_tonie.call_args[0] assert [c.title for c in ordered] == ["01 - First", "02 - Second"]