Skip to content

Commit 7557182

Browse files
Jammy2211Jammy2211claude
authored
test: Check B must assert the unpinned sub-3.12 install is refused (#155)
* test: Check B must assert the UNPINNED sub-3.12 install is refused Check B already knew about this gap and wrote it down — "An unpinned 3.11 install is not evidence because pip may select an older compatible release" — then tested only the pinned rejection. The unpinned path was the one users actually took, and until 2026-08-19 it silently installed the stale 2026.7.29.1 stack with no JAX and no warning. Adds check_b_unpinned_refused: an unpinned `pip install autolens` on 3.11 must fail, and fail for the right reason. A successful install is now a FAIL that says the sub-floor backtrack is back. verify_install_unpinned_refusal accepts the two shapes that mean refused — the tombstone raising during metadata preparation (the live path), and pip running out of candidates if the sub-floor catalogue is ever withdrawn. Anything else (network, resolver, dependency failure) is not floor evidence. Classifier verified against real captured pip output in both directions: a real 3.11 refusal classifies as a refusal, a real 3.12 success does not. Issue: PyAutoLabs/PyAutoHands#238 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: release_validation must describe both Check B refusals The page repeated the old caveat — "An unpinned install is not sufficient evidence because pip may select an older compatible release" — which was written when the unpinned path was untested. It now has its own required leg. Restates it as two distinct guarantees: a pinned rejection proves this release holds the floor, and the unpinned install must be refused as well. The second was not met until 2026-08-19, when pip on 3.11 backtracked to 2026.7.29.1 and installed a stale JAX-less stack silently. Issue: PyAutoLabs/PyAutoHands#238 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Jammy2211 <JNightingale2211@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c98d554 commit 7557182

5 files changed

Lines changed: 158 additions & 8 deletions

File tree

docs/release_validation.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,15 @@ carries `profile` and `commit_shas`, so the gate can enforce them):
117117
The integration run also performs `verify_install` A–F against the same wheels.
118118
Check B reuses that exact TestPyPI version: it must install and import on Python
119119
3.12 and 3.13, while Python 3.11 must reject it specifically because its
120-
`Requires-Python` metadata is `>=3.12`. An unpinned install is not sufficient
121-
evidence because pip may select an older compatible release.
120+
`Requires-Python` metadata is `>=3.12`. A *pinned* rejection is the only evidence
121+
that this release holds the floor, because an unpinned install may select an
122+
older compatible one instead.
123+
124+
Check B then requires the unpinned install to be refused as well. That is a
125+
separate guarantee, and it was not met until 2026-08-19: `pip install autolens`
126+
on 3.11 backtracked to `2026.7.29.1` and installed a stale, JAX-less stack
127+
silently. The `2026.7.29.1.post1` tombstone refuses it, and this leg is what
128+
stops the backtrack reopening.
122129

123130
That TestPyPI A–F result feeds the install-verification readiness leg (see
124131
"How the gate enforces these"), tagged `index: testpypi`. It proves the wheels

heart/checks/verify_install.sh

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# Runs a suite of independent install-path checks:
1212
#
1313
# A pip install autolens (default Python) + start_here.py + welcome.py
14-
# B one exact autolens release installs on 3.12/3.13 and rejects on 3.11
14+
# B one exact autolens release installs on 3.12/3.13, and 3.11 refuses it
15+
# both pinned (Requires-Python) and unpinned (the tombstone release)
1516
# C conda install flow (python=3.12) + start_here.py + welcome.py
1617
# D pip install "autolens[optional]" resolves
1718
# E pip install autolens==2026.2.26.4 on Python 3.12 by explicit pin
@@ -58,8 +59,9 @@ Usage:
5859
5960
Checks:
6061
A pip install autolens (default python3) + start_here.py + welcome.py
61-
B one exact autolens release installs on python3.12 and python3.13,
62-
while python3.11 rejects it because Requires-Python is >=3.12
62+
B one exact autolens release installs on python3.12 and python3.13, while
63+
python3.11 refuses it both pinned (Requires-Python >=3.12) and unpinned
64+
(the 2026.7.29.1.post1 tombstone, so pip cannot backtrack to a stale one)
6365
C conda install flow (python=3.12) + start_here.py + welcome.py
6466
D pip install "autolens[optional]" resolves and imports
6567
E pip install autolens==2026.2.26.4 (yanked) installs on python3.12 by explicit pin
@@ -449,12 +451,53 @@ check_b_rejected() {
449451
fi
450452
}
451453

454+
check_b_unpinned_refused() {
455+
local pybin="python3.11"
456+
457+
if ! command -v "$pybin" > /dev/null 2>&1; then
458+
step "$pybin not installed — FAIL (required floor-rejection interpreter)"
459+
RESULTS+=("B|FAIL|$pybin not installed")
460+
return
461+
fi
462+
463+
local venv="/tmp/autolens_verify_B_unpinned_3.11_$TS"
464+
ARTEFACTS+=("$venv")
465+
466+
step "$pybin: creating unpinned-refusal venv at $venv"
467+
if ! make_venv "$venv" "$pybin"; then
468+
RESULTS+=("B|FAIL|$pybin could not create venv")
469+
return
470+
fi
471+
472+
# shellcheck source=/dev/null
473+
source "$venv/bin/activate"
474+
pip install --upgrade pip > /dev/null 2>&1 || true
475+
476+
step "$pybin: expecting an UNPINNED install to be refused"
477+
local pip_out pip_rc=0
478+
pip_out=$(pip install "${PIP_INDEX_ARGS[@]}" autolens 2>&1) || pip_rc=$?
479+
deactivate
480+
481+
if [ "$pip_rc" -eq 0 ]; then
482+
RESULTS+=("B|FAIL|$pybin silently installed an unpinned autolens — the sub-floor backtrack is back")
483+
tail_log "Check B ($pybin) unpinned install unexpectedly succeeded" "$pip_out"
484+
return
485+
fi
486+
if verify_install_unpinned_refusal "$pip_out" autolens; then
487+
RESULTS+=("B|PASS|$pybin refused the unpinned install")
488+
else
489+
RESULTS+=("B|FAIL|$pybin unpinned install failed without a floor refusal (rc=$pip_rc)")
490+
tail_log "Check B ($pybin) unexpected unpinned pip output" "$pip_out"
491+
fi
492+
}
493+
452494
check_b() {
453495
echo
454-
echo "=== Check B: exact release on 3.12/3.13; rejected on 3.11 ==="
496+
echo "=== Check B: exact release on 3.12/3.13; pinned and unpinned refused on 3.11 ==="
455497
check_b_supported python3.12
456498
check_b_supported python3.13
457499
check_b_rejected
500+
check_b_unpinned_refused
458501
}
459502

460503
# ----- check C: conda flow -----

heart/checks/verify_install_helpers.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,32 @@ except InvalidVersion:
4444
raise SystemExit(0 if equivalent else 1)
4545
PY
4646
}
47+
48+
verify_install_unpinned_refusal() {
49+
local pip_output="$1"
50+
local package="$2"
51+
52+
# Tombstone path (the live one): releases at or below 2026.7.29.1 declare
53+
# Requires-Python >=3.9 and stay valid candidates forever, so an unpinned
54+
# install below the floor is refused by 2026.7.29.1.post1 raising during
55+
# metadata preparation. That text arrives from the build subprocess, not as
56+
# a pip resolver diagnostic, so it needs its own classifier.
57+
if printf '%s\n' "$pip_output" | grep -qE \
58+
"$package requires Python 3\.[0-9]+ or later" \
59+
&& printf '%s\n' "$pip_output" | grep -qE \
60+
"you are running Python 3\.[0-9]+"; then
61+
return 0
62+
fi
63+
64+
# Retraction path: if the sub-floor catalogue is ever withdrawn, pip runs
65+
# out of candidates instead and says so. Both shapes mean refused; anything
66+
# else (network, resolver, dependency failure) is not floor evidence.
67+
if printf '%s\n' "$pip_output" | grep -qi \
68+
"Ignored the following versions that require a different python version:" \
69+
&& printf '%s\n' "$pip_output" | grep -qiE \
70+
"No matching distribution found for $package"; then
71+
return 0
72+
fi
73+
74+
return 1
75+
}

skills/verify_install/verify_install.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ about cleanup if they ran with `--keep`.
1414
| Check | What it verifies |
1515
|-------|------------------|
1616
| A | `pip install autolens` in a venv on default `python3`; `start_here.py` and `welcome.py` both run cleanly. |
17-
| B | One exact `autolens` version installs and imports cleanly on `python3.12` and `python3.13`, then the same exact version is rejected by `python3.11` specifically because `Requires-Python` is `>=3.12`. An unpinned 3.11 install is not evidence because pip may select an older compatible release. |
17+
| B | One exact `autolens` version installs and imports cleanly on `python3.12` and `python3.13`, then the same exact version is rejected by `python3.11` specifically because `Requires-Python` is `>=3.12`. An **unpinned** `pip install autolens` on `python3.11` is then required to be refused too — it was not, until 2026-08-19: pip backtracked to `2026.7.29.1` and installed a stale JAX-less stack silently. The `2026.7.29.1.post1` tombstone closes that, and this leg is what stops it reopening. |
1818
| C | The conda flow from `installation/conda.rst` works end-to-end (`conda create … python=3.12``pip install autolens` → clone workspace → run `welcome.py` + `start_here.py`). |
1919
| D | `pip install "autolens[optional]"` resolves cleanly and imports. |
2020
| E | `pip install autolens==2026.2.26.4` (a yanked release the docs reference) still installs on `python3.12` by explicit pin. |

tests/test_verify_install_script.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,23 @@ def classify_rejection(output, version="2026.7.29.1"):
3939
)
4040

4141

42+
def classify_unpinned(output, package="autolens"):
43+
return subprocess.run(
44+
[
45+
"bash",
46+
"-c",
47+
'source "$1"; pip_output=$(cat); '
48+
'verify_install_unpinned_refusal "$pip_output" "$2"',
49+
"classifier",
50+
str(HELPERS),
51+
package,
52+
],
53+
input=output,
54+
capture_output=True,
55+
text=True,
56+
)
57+
58+
4259
def test_bash_syntax():
4360
for path in (SCRIPT, HELPERS):
4461
result = subprocess.run(
@@ -225,5 +242,59 @@ def test_help_describes_supported_success_and_below_floor_rejection():
225242

226243
assert result.returncode == 0
227244
assert "installs on python3.12 and python3.13" in result.stdout
228-
assert "python3.11 rejects it because Requires-Python is >=3.12" in result.stdout
245+
assert "pinned (Requires-Python >=3.12)" in result.stdout
246+
# The unpinned leg has to be advertised too, or the help understates what
247+
# Check B now guarantees.
248+
assert "unpinned" in result.stdout
249+
assert "2026.7.29.1.post1 tombstone" in result.stdout
229250
assert "applies to A, B, C, D" in result.stdout
251+
252+
253+
def test_unpinned_classifier_accepts_the_tombstone_and_retraction_forms():
254+
"""The two shapes that mean "refused below the floor"."""
255+
tombstone_output = (
256+
" autolens requires Python 3.12 or later — you are running "
257+
"Python 3.11.\n"
258+
"ERROR: Failed to build 'autolens' when getting requirements to "
259+
"build wheel\n"
260+
)
261+
retraction_output = (
262+
"ERROR: Ignored the following versions that require a different "
263+
"python version: 2026.8.17.1 Requires-Python >=3.12\n"
264+
"ERROR: No matching distribution found for autolens\n"
265+
)
266+
267+
assert classify_unpinned(tombstone_output).returncode == 0
268+
assert classify_unpinned(retraction_output).returncode == 0
269+
270+
271+
def test_unpinned_classifier_rejects_unrelated_pip_failures():
272+
"""A refusal for any other reason is not floor evidence."""
273+
network_failure = (
274+
"WARNING: Retrying after connection broken by NewConnectionError\n"
275+
"ERROR: No matching distribution found for autolens\n"
276+
)
277+
dependency_conflict = (
278+
"ERROR: Cannot install autolens because these package versions have "
279+
"conflicting dependencies.\n"
280+
)
281+
other_package_tombstone = (
282+
" autofit requires Python 3.12 or later — you are running "
283+
"Python 3.11.\n"
284+
)
285+
286+
for output in (network_failure, dependency_conflict, other_package_tombstone):
287+
assert classify_unpinned(output).returncode != 0
288+
289+
290+
def test_check_b_asserts_the_unpinned_install_is_refused():
291+
"""The gap Check B documented but never tested: until 2026-08-19 an
292+
unpinned 3.11 install silently resolved to the stale 2026.7.29.1 stack."""
293+
body = SCRIPT.read_text()
294+
295+
assert "check_b_unpinned_refused" in body
296+
# Wired into the runner, not merely defined.
297+
assert body.count("check_b_unpinned_refused") >= 2
298+
assert "verify_install_unpinned_refusal" in body
299+
# A successful unpinned install below the floor is the bug returning.
300+
assert "the sub-floor backtrack is back" in body

0 commit comments

Comments
 (0)