gabriel / musehub public

test_prune_releases.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:e fix(9A-4 F7): env alias + staging secrets for overseer provenance gate.… · aaronrene · Jul 13, 2026
1 """Tests for deploy/prune_releases.py -- PEP 440-aware release pruning.
2
3 See MuseHub staging issue #128: `sort -V` doesn't understand PEP 440 channel
4 precedence (dev < a < b < rc < final), so it deleted the newest nightly from
5 S3 immediately after upload. `stale_releases()` replaces `sort -V` with
6 `packaging.version.Version`, which sorts by actual PEP 440 precedence.
7
8 Test IDs (pinned in issue #128's plan):
9 PRUNE_01 exact incident reproduction (dev1 survives, rc14 pruned)
10 PRUNE_02 mixed channels, full precedence coverage
11 PRUNE_03 keep >= len(filenames) prunes nothing
12 PRUNE_04 unparseable filename raises, never silently mis-sorts
13 PRUNE_05 stable releases sort after all pre-release channels
14 """
15 from __future__ import annotations
16
17 import pytest
18
19 from deploy.prune_releases import stale_releases
20
21
22 class TestStaleReleases:
23 def test_PRUNE_01_same_base_version_dev_is_lowest_precedence_not_newest(self) -> None:
24 """Correct PEP 440 precedence for the exact incident's four filenames.
25
26 dev1 and rc14-16 all share the base version "0.2.0" -- PEP 440 defines
27 dev-releases as the *lowest*-precedence pre-release stage for a given
28 version (dev < a < b < rc < final), so 0.2.0.dev1 correctly sorts
29 BEFORE 0.2.0rc16 despite being built later in wall-clock time. This is
30 not the bug: sort -V's bug was comparing "dev1" against "rc14" as
31 plain text and getting 'd' < 'r' right by accident-adjacent means but
32 wrong reasoning for the general case. The actual root cause of the
33 incident is that the channel-pivot reused "0.2.0" for the new nightly
34 instead of bumping the base version (see docs/versioning.md) -- once
35 that discipline is followed, this exact ambiguity can't recur, because
36 a same-day pivot bumps to e.g. 0.2.1.dev1, which correctly outranks
37 every 0.2.0 build regardless of pre-release tag.
38 """
39 filenames = [
40 "muse-0.2.0rc14.tar.gz",
41 "muse-0.2.0rc15.tar.gz",
42 "muse-0.2.0rc16.tar.gz",
43 "muse-0.2.0.dev1.tar.gz",
44 ]
45 assert stale_releases(filenames, keep=3) == ["muse-0.2.0.dev1.tar.gz"]
46
47 def test_PRUNE_01b_pivot_with_bumped_version_keeps_new_nightly(self) -> None:
48 """The correct fix for the incident: bump the base version on a pivot.
49
50 Once the next nightly is labeled against a NEW base version (0.2.1,
51 not 0.2.0), PEP 440 precedence unambiguously ranks it above every
52 0.2.0 build, pre-release or not -- this is the behavior
53 docs/versioning.md's new "channel pivot" rule exists to guarantee.
54 """
55 filenames = [
56 "muse-0.2.0rc14.tar.gz",
57 "muse-0.2.0rc15.tar.gz",
58 "muse-0.2.0rc16.tar.gz",
59 "muse-0.2.1.dev1.tar.gz",
60 ]
61 assert stale_releases(filenames, keep=3) == ["muse-0.2.0rc14.tar.gz"]
62
63 def test_PRUNE_02_mixed_channels_full_precedence_coverage(self) -> None:
64 """dev < a < b < rc precedence across all four pre-release channels."""
65 filenames = [
66 "muse-0.2.0.dev1.tar.gz",
67 "muse-0.2.0a1.tar.gz",
68 "muse-0.2.0b1.tar.gz",
69 "muse-0.2.0rc1.tar.gz",
70 "muse-0.2.0rc2.tar.gz",
71 ]
72 stale = stale_releases(filenames, keep=2)
73 assert sorted(stale) == sorted([
74 "muse-0.2.0.dev1.tar.gz",
75 "muse-0.2.0a1.tar.gz",
76 "muse-0.2.0b1.tar.gz",
77 ])
78
79 def test_PRUNE_03_keep_greater_than_len_prunes_nothing(self) -> None:
80 filenames = ["muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz"]
81 assert stale_releases(filenames, keep=5) == []
82
83 def test_PRUNE_03b_keep_equal_to_len_prunes_nothing(self) -> None:
84 filenames = ["muse-0.2.0rc1.tar.gz", "muse-0.2.0rc2.tar.gz"]
85 assert stale_releases(filenames, keep=2) == []
86
87 def test_PRUNE_04_unparseable_filename_raises(self) -> None:
88 filenames = ["muse-0.2.0rc1.tar.gz", "muse-not-a-version.tar.gz"]
89 with pytest.raises(ValueError):
90 stale_releases(filenames, keep=1)
91
92 def test_PRUNE_05_stable_sorts_after_all_prerelease_channels(self) -> None:
93 """A stable release must never be pruned ahead of any pre-release build."""
94 filenames = [
95 "muse-0.2.0.dev1.tar.gz",
96 "muse-0.2.0rc1.tar.gz",
97 "muse-0.2.0.tar.gz",
98 ]
99 stale = stale_releases(filenames, keep=1)
100 assert stale == ["muse-0.2.0.dev1.tar.gz", "muse-0.2.0rc1.tar.gz"]
101
102 def test_PRUNE_08_empty_filename_list_returns_empty(self) -> None:
103 assert stale_releases([], keep=3) == []
104
105 def test_PRUNE_09_returns_oldest_first_stable_ordering(self) -> None:
106 """Stale list should be ordered oldest-first, for readable log output."""
107 filenames = [
108 "muse-0.2.0rc3.tar.gz",
109 "muse-0.2.0rc1.tar.gz",
110 "muse-0.2.0rc2.tar.gz",
111 ]
112 assert stale_releases(filenames, keep=1) == [
113 "muse-0.2.0rc1.tar.gz",
114 "muse-0.2.0rc2.tar.gz",
115 ]