Files
alembic/tests/test_dedup_gating.py
andrew e49d12a72b Add test suite (T1)
First automated coverage. 38 tests, run against the built image (which has the
app deps); tests/ and requirements-dev.txt are not baked into the runtime image.

- tests/test_dedup_gating.py: the safety-critical one. Drives dedup-library.sh's
  process_group with --apply + --only-paths and asserts ONLY the confirmed path
  is deleted and the higher-ranked FLAC is kept.
- tests/test_pipeline_runner.py: lock skip, use_lock=False bypass, lock release,
  success/failure/timeout recording.
- tests/test_db.py: schema version + idempotent migrations, FK-safe job-run
  pruning, stale-run reconciliation.
- tests/test_credential_service.py: conf-field substitution incl. newline-
  injection safety, secret/non-secret classification, cookie-expiry parsing.
- tests/test_playlist_service.py: cron round-trips, name validation, render.
- tests/test_diagnostics.py: /health + /setup checks.
- conftest.py points every path setting at throwaway temp dirs and a temp DB, so
  tests never touch a real library, /config, or the network. See tests/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:00:07 -06:00

54 lines
2.3 KiB
Python

"""The safety-critical test: dedup-library.sh must delete ONLY the files a
human confirmed via --only-paths, and must keep the higher-ranked copy.
Rather than stand up a full beets fixture, this sources the script's setup +
function definitions (everything before the Pass 1 top-level code) and drives
process_group directly with "ghost" (bare-path) entries, which skip the beets
path translation and are deleted with plain rm -- exactly the code path that
enforces --only-paths gating and FLAC-over-MP3 ranking."""
import subprocess
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SCRIPT = REPO / "pipeline" / "lib" / "dedup-library.sh"
def _drive_process_group(work: Path) -> subprocess.CompletedProcess:
keep = work / "keep.flac"
dupe = work / "dupe.mp3"
other = work / "other.mp3"
for f, size in ((keep, 5000), (dupe, 1000), (other, 1000)):
f.write_bytes(b"\0" * size)
confirm = work / "confirm.txt"
confirm.write_text(str(dupe) + "\n") # only dupe.mp3 is confirmed for deletion
script = r"""
set -euo pipefail
export ALEMBIC_CONFIG_DIR="{work}" MUSIC_DATA_DIR="{work}"
mkdir -p "{work}/logs"
# Source only setup + functions (stop before the Pass 1 top-level code).
end=$(grep -n '^# Pass 1: numbered siblings' "{script}" | head -1 | cut -d: -f1)
source <(head -n $((end - 2)) "{script}")
APPLY=1
ONLY_PATHS_FILE="{confirm}"
declare -A ONLY_PATHS=(["{dupe}"]=1)
# Group 1: keep.flac vs dupe.mp3 (dupe confirmed -> deleted)
process_group "P2 case-insensitive: g1" "{keep}" "{dupe}"
# Group 2: keep.flac vs other.mp3 (other NOT confirmed -> must survive)
process_group "P2 case-insensitive: g2" "{keep}" "{other}"
""".format(work=work, script=SCRIPT, confirm=confirm, dupe=dupe, keep=keep, other=other)
return subprocess.run(["bash", "-c", script], capture_output=True, text=True)
def test_only_confirmed_paths_are_deleted(tmp_path):
proc = _drive_process_group(tmp_path)
assert proc.returncode == 0, proc.stderr
keep = tmp_path / "keep.flac"
dupe = tmp_path / "dupe.mp3"
other = tmp_path / "other.mp3"
assert keep.exists(), "FLAC (higher rank) must be kept"
assert not dupe.exists(), "confirmed duplicate must be deleted"
assert other.exists(), "an unconfirmed delete-candidate must NOT be deleted"