"""Tests for deploy/prune_releases.py -- PEP 440-aware release pruning. See MuseHub staging issue #128: `sort -V` doesn't understand PEP 440 channel precedence (dev < a < b < rc < final), so it deleted the newest nightly from S3 immediately after upload. `stale_releases()` replaces `sort -V` with `packaging.version.Version`, which sorts by actual PEP 440 precedence. Test IDs (pinned in issue #128's plan): PRUNE_01 exact incident reproduction (dev1 survives, rc14 pruned) PRUNE_02 mixed channels, full precedence coverage PRUNE_03 keep >= len(filenames) prunes nothing PRUNE_04 unparseable filename raises, never silently mis-sorts PRUNE_05 stable releases sort after all pre-release channels """ from __future__ import annotations import pytest from deploy.prune_releases import stale_releases class TestStaleReleases: def test_PRUNE_01_same_base_version_dev_is_lowest_precedence_not_newest(self) -> None: """Correct PEP 440 precedence for the exact incident's four filenames. dev1 and rc14-16 all share the base version "0.2.0" -- PEP 440 defines dev-releases as the *lowest*-precedence pre-release stage for a given version (dev < a < b < rc < final), so 0.2.0.dev1 correctly sorts BEFORE 0.2.0rc16 despite being built later in wall-clock time. This is not the bug: sort -V's bug was comparing "dev1" against "rc14" as plain text and getting 'd' < 'r' right by accident-adjacent means but wrong reasoning for the general case. The actual root cause of the incident is that the channel-pivot reused "0.2.0" for the new nightly instead of bumping the base version (see docs/versioning.md) -- once that discipline is followed, this exact ambiguity can't recur, because a same-day pivot bumps to e.g. 0.2.1.dev1, which correctly outranks every 0.2.0 build regardless of pre-release tag. """ filenames = [ "muse-0.2.0rc14.tar.gz", "muse-0.2.0rc15.tar.gz", "muse-0.2.0rc16.tar.gz", "muse-0.2.0.dev1.tar.gz", ] assert stale_releases(filenames, keep=3) == ["muse-0.2.0.dev1.tar.gz"] def test_PRUNE_01b_pivot_with_bumped_version_keeps_new_nightly(self) -> None: """The correct fix for the incident: bump the base version on a pivot. Once the next nightly is labeled against a NEW base version (0.2.1, not 0.2.0), PEP 440 precedence unambiguously ranks it above every 0.2.0 build, pre-release or not -- this is the behavior docs/versioning.md's new "channel pivot" rule exists to guarantee. """ filenames = [ "muse-0.2.0rc14.tar.gz", "muse-0.2.0rc15.tar.gz", "muse-0.2.0rc16.tar.gz", "muse-0.2.1.dev1.tar.gz", ] assert stale_releases(filenames, keep=3) == ["muse-0.2.0rc14.tar.gz"] def test_PRUNE_02_mixed_channels_full_precedence_coverage(self) -> None: """dev < a < b < rc precedence across all four pre-release channels.""" filenames = [ "muse-0.2.0.dev1.tar.gz", "muse-0.2.0a1.tar.gz", "muse-0.2.0b1.tar.gz", "muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz", ] stale = stale_releases(filenames, keep=2) assert sorted(stale) == sorted([ "muse-0.2.0.dev1.tar.gz", "muse-0.2.0a1.tar.gz", "muse-0.2.0b1.tar.gz", ]) def test_PRUNE_03_keep_greater_than_len_prunes_nothing(self) -> None: filenames = ["muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz"] assert stale_releases(filenames, keep=5) == [] def test_PRUNE_03b_keep_equal_to_len_prunes_nothing(self) -> None: filenames = ["muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz"] assert stale_releases(filenames, keep=2) == [] def test_PRUNE_04_unparseable_filename_raises(self) -> None: filenames = ["muse-0.2.0rc1.tar.gz", "muse-not-a-version.tar.gz"] with pytest.raises(ValueError): stale_releases(filenames, keep=1) def test_PRUNE_05_stable_sorts_after_all_prerelease_channels(self) -> None: """A stable release must never be pruned ahead of any pre-release build.""" filenames = [ "muse-0.2.0.dev1.tar.gz", "muse-0.2.0rc1.tar.gz", "muse-0.2.0.tar.gz", ] stale = stale_releases(filenames, keep=1) assert stale == ["muse-0.2.0.dev1.tar.gz", "muse-0.2.0rc1.tar.gz"] def test_PRUNE_08_empty_filename_list_returns_empty(self) -> None: assert stale_releases([], keep=3) == [] def test_PRUNE_09_returns_oldest_first_stable_ordering(self) -> None: """Stale list should be ordered oldest-first, for readable log output.""" filenames = [ "muse-0.2.0rc3.tar.gz", "muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz", ] assert stale_releases(filenames, keep=1) == [ "muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz", ]