import runpy from pathlib import Path import pytest from serato_doctor.web import ( STATIC_FILES, _file_token, _verified_audio, analyze_paths, duplicate_repair, duplicate_repair_batch, ) def test_web_analysis_uses_production_health_pipeline(tmp_path): generator = runpy.run_path( str(Path(__file__).parents[1] / "samples/small-library/generate.py") ) sample = generator["build_sample"](tmp_path / "sample") result = analyze_paths( sample / "Serato" / "_Serato_", sample / "Music" ) assert result["score"] == 77.8 assert result["disk_tracks"] == 10 assert result["missing_references"] == 2 assert result["static_crates"] == 5 assert result["smart_crates"] == 2 assert result["database_entries"] == 10 assert result["database_library_matches"] == 10 assert result["database_missing_paths"] == 0 assert result["database_missing_unique_filenames"] == 0 assert result["tracks_missing_from_database"] == 0 assert result["details"]["old_crate_references"]["total"] == 2 assert result["details"]["old_crate_references"]["items"][0]["crate"] assert result["details"]["suggested_matches"]["total"] == 0 assert result["details"]["unused_tracks"]["total"] == 1 def test_web_analysis_rejects_missing_folders(tmp_path): with pytest.raises(ValueError, match="Serato folder does not exist"): analyze_paths(tmp_path / "missing", tmp_path) def test_web_static_assets_are_declared_and_packaged(): asset_root = Path(__file__).parents[1] / "serato_doctor" / "webui" assert set(STATIC_FILES) == { "/", "/app.css", "/recovery.css", "/layout-fixes.css", "/app.js", } assert all((asset_root / filename).is_file() for filename, _ in STATIC_FILES.values()) html = (asset_root / "index.html").read_text(encoding="utf-8") assert "Missing tracks in Serato" in html assert "Old crate references" in html assert "Choose a diagnostic" in html assert "Backup recovery" in html assert "Here’s exactly what will happen" in html assert 'id="batch-preview-modal"' in html assert 'data-detail="database_missing_tracks"' in html assert 'data-detail="old_crate_references"' in html assert html.count('class="info-button"') >= 10 def test_duplicate_repair_preview_does_not_change_files(tmp_path): serato = tmp_path / "_Serato_" serato.mkdir() music = tmp_path / "Music" first = music / "A" / "Track.mp3" second = music / "B" / "Track.mp3" first.parent.mkdir(parents=True) second.parent.mkdir(parents=True) first.write_bytes(b"first") second.write_bytes(b"second") result = duplicate_repair( serato, music, first, (first, second), backup_limit=10 ) assert result["applied"] is False assert result["database_v2_modified"] is False assert second.read_bytes() == b"second" assert not second.is_symlink() def test_audio_preview_tokens_only_open_signed_audio(tmp_path): track = tmp_path / "Track.mp3" track.write_bytes(b"audio") token = _file_token(track) assert _verified_audio(token) == track with pytest.raises(ValueError, match="Invalid or expired"): _verified_audio(token + "changed") def test_duplicate_details_include_preview_and_finder_controls(tmp_path): serato = tmp_path / "_Serato_" serato.mkdir() music = tmp_path / "Music" first = music / "A" / "Track.mp3" second = music / "B" / "Track.mp3" first.parent.mkdir(parents=True) second.parent.mkdir(parents=True) first.write_bytes(b"first") second.write_bytes(b"second") result = analyze_paths(serato, music) group = result["details"]["duplicate_filenames"]["items"][0] assert len(group["file_previews"]) == 2 assert group["file_previews"][0]["audio_url"].startswith("/api/audio?") assert group["file_previews"][0]["reveal_token"] def test_batch_preview_combines_approved_groups_without_changes(tmp_path): serato = tmp_path / "_Serato_" serato.mkdir() music = tmp_path / "Music" choices = [] for filename in ("First.mp3", "Second.mp3"): keeper = music / "A" / filename duplicate = music / "B" / filename keeper.parent.mkdir(parents=True, exist_ok=True) duplicate.parent.mkdir(parents=True, exist_ok=True) keeper.write_bytes(b"keeper") duplicate.write_bytes(b"duplicate") choices.append( {"keeper": str(keeper), "group_files": [str(keeper), str(duplicate)]} ) result = duplicate_repair_batch( serato, music, choices, backup_limit=10 ) assert result["applied"] is False assert result["choice_count"] == 2 assert len(result["replaced"]) == 2 assert len(result["decisions"]) == 2 assert result["decisions"][0]["keeper"].endswith("First.mp3") assert len(result["decisions"][0]["replaced"]) == 1 assert all(not Path(choice["group_files"][1]).is_symlink() for choice in choices)