from pathlib import Path import pytest from serato_doctor.repair import ( BACKUP_FOLDER, apply_duplicate_repair, apply_duplicate_repair_batch, list_backups, plan_duplicate_repair, restore_backup, rotate_backups, ) def library(tmp_path): serato = tmp_path / "_Serato_" crate = serato / "Subcrates" / "House.crate" crate.parent.mkdir(parents=True) crate.write_bytes(b"crate-data") (serato / "database V2").write_bytes(b"database-data") music = tmp_path / "Music" keeper = music / "Main" / "Track.mp3" duplicate = music / "Old" / "Track.mp3" keeper.parent.mkdir(parents=True) duplicate.parent.mkdir(parents=True) keeper.write_bytes(b"keeper") duplicate.write_bytes(b"duplicate") return serato, keeper, duplicate def test_duplicate_repair_backs_up_then_preserves_old_path_as_link(tmp_path): serato, keeper, duplicate = library(tmp_path) plan = plan_duplicate_repair(keeper, (keeper, duplicate), serato) receipt = apply_duplicate_repair(plan, serato, backup_limit=3) assert duplicate.is_symlink() assert duplicate.resolve() == keeper.resolve() assert (receipt.backup / "manifest.json").is_file() assert any((receipt.backup / "serato-metadata").rglob("House.crate")) assert any((receipt.backup / "serato-metadata").rglob("database V2")) history = list_backups(serato) assert len(history) == 1 assert history[0].status == "ready" assert history[0].size_bytes > 0 restored = restore_backup(receipt.backup) assert restored == (duplicate.resolve(),) assert not duplicate.is_symlink() assert duplicate.read_bytes() == b"duplicate" assert list_backups(serato)[0].status == "restored" def test_plan_rejects_keeper_outside_duplicate_group(tmp_path): serato, keeper, duplicate = library(tmp_path) outsider = tmp_path / "outsider.mp3" outsider.write_bytes(b"other") with pytest.raises(ValueError, match="must belong"): plan_duplicate_repair(outsider, (keeper, duplicate), serato) def test_backup_rotation_can_be_limited_or_unlimited(tmp_path): root = tmp_path / BACKUP_FOLDER for name in ("001", "002", "003"): backup = root / name backup.mkdir(parents=True) (backup / "manifest.json").write_text("{}", encoding="utf-8") rotate_backups(root, None) assert len(list(root.iterdir())) == 3 rotate_backups(root, 2) assert {path.name for path in root.iterdir()} == {"002", "003"} def test_batch_repair_uses_one_backup_and_restores_every_group(tmp_path): serato, first_keeper, first_duplicate = library(tmp_path) second_keeper = tmp_path / "Music" / "Main" / "Other.mp3" second_duplicate = tmp_path / "Music" / "Old" / "Other.mp3" second_keeper.write_bytes(b"other-keeper") second_duplicate.write_bytes(b"other-duplicate") plans = ( plan_duplicate_repair( first_keeper, (first_keeper, first_duplicate), serato ), plan_duplicate_repair( second_keeper, (second_keeper, second_duplicate), serato ), ) receipt = apply_duplicate_repair_batch(plans, serato, backup_limit=10) assert first_duplicate.is_symlink() assert second_duplicate.is_symlink() assert len(list((serato / BACKUP_FOLDER).iterdir())) == 1 assert len(receipt.replaced) == 2 restore_backup(receipt.backup) assert first_duplicate.read_bytes() == b"duplicate" assert second_duplicate.read_bytes() == b"other-duplicate"