"""Release recursion regression (handoff 2026-09-11 Part C). The rule: a release bump commit MUST NOT trigger another version bump. The decision lives in version_convergence (ONE source of truth) — the workflow guard calls --is-release-subject; these tests exercise the same function the workflow uses, plus the CLI exit-code contract itself. """ import os import subprocess import sys import unittest from pathlib import Path REPO = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO / "scripts")) from version_convergence import is_release_commit # noqa: E402 class ReleaseRecursionRule(unittest.TestCase): def test_release_commit_is_recognized(self): # The EXACT subject shape the auto-version workflow generates. self.assertTrue( is_release_commit( "chore(release): auto-bump version to 0.66.2 — earned by # " "(patch: converged release (push-to-master bump))" ) ) self.assertTrue(is_release_commit("chore(release): auto-bump version to 0.61.0")) self.assertTrue(is_release_commit("chore: release v0.60.0")) def test_release_commit_never_warrants_a_bump(self): # The deeper invariant: classify_subject('other') does not feed # highest_warranted_bump, so a release commit alone cannot bump. from version_convergence import classify_subject, highest_warranted_bump subject = ( "chore(release): auto-bump version to 0.66.2 — earned by # (patch: converged release (push-to-master bump))" ) self.assertEqual(classify_subject(subject), "other") self.assertIsNone(highest_warranted_bump([subject])) self.assertIsNone(highest_warranted_bump([subject] * 5)) # the loop case def test_substantive_commits_still_bump(self): from version_convergence import classify_subject, highest_warranted_bump self.assertEqual(classify_subject("feat(ADAPT-LIVE): route discriminator"), "feat") self.assertEqual(classify_subject("fix(deploy): prefix-match"), "fix") self.assertEqual( highest_warranted_bump(["feat(x): a", "fix(y): b"]), "minor", ) def test_cli_exit_contract(self): # The workflow consumes the CLI, not the Python API — prove the # exit-code contract the guard depends on. r = subprocess.run( [ sys.executable, str(REPO / "scripts" / "version_convergence.py"), "--is-release-subject", "chore(release): auto-bump version to 0.66.2", ], capture_output=True, text=True, timeout=60, cwd=str(REPO), ) self.assertEqual(r.returncode, 0, "release subject → guard exits 0 (skip)") r2 = subprocess.run( [ sys.executable, str(REPO / "scripts" / "version_convergence.py"), "--is-release-subject", "feat(x): real feature", ], capture_output=True, text=True, timeout=60, cwd=str(REPO), ) self.assertEqual(r2.returncode, 1, "feat subject → guard exits 1 (bump)") if __name__ == "__main__": unittest.main()