From ce87cf7dabee33685efb8548a73e24c0a73620dd Mon Sep 17 00:00:00 2001 From: Jake Writer Date: Mon, 31 Aug 2026 14:08:41 -0600 Subject: [PATCH] fix(python): report an unsatisfiable version floor instead of recursing camoufox_path() ended in `return camoufox_path()` after a fetch. When the newest published build is still below CONSTRAINTS.MIN_VERSION, install() is a no-op ("already installed") and that tail recursed ~1000 times -- each iteration firing another GitHub API call, which exhausts the unauthenticated rate limit (60/hr) long before the RecursionError lands. That is precisely the state a library published ahead of its browser release puts every user in, and it is reachable now that the floor is raised. It also hits permanently for anyone using a repos.yml source that does not carry the required build. Re-check after the fetch instead, and raise UnsupportedVersion naming the required minimum. Co-Authored-By: Claude Opus 5 (1M context) --- pythonlib/camoufox/pkgman.py | 19 ++++++++++++++- pythonlib/tests/test_version_floor_upgrade.py | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pythonlib/camoufox/pkgman.py b/pythonlib/camoufox/pkgman.py index d988f0c..485bf8d 100644 --- a/pythonlib/camoufox/pkgman.py +++ b/pythonlib/camoufox/pkgman.py @@ -811,7 +811,24 @@ def camoufox_path(download_if_missing: bool = True) -> Path: raise UnsupportedVersion("Camoufox executable is outdated.") CamoufoxFetcher().install() - return camoufox_path() + + # Re-check rather than recurse. + # + # If the newest published build is still below the floor -- a library + # published ahead of its browser release, or a repos.yml source that does + # not carry it -- install() is a no-op ("already installed") and recursing + # here spun ~1000 fetch attempts into a RecursionError, having hammered the + # GitHub API into a rate limit on the way. Say what is actually wrong. + active = get_active_path() + if active and Version.from_path(active).is_supported(): + return active + if os.path.exists(INSTALL_DIR) and _root_install_supported(): + return INSTALL_DIR + raise UnsupportedVersion( + f"No available Camoufox build satisfies this library's minimum " + f"({CONSTRAINTS.MIN_VERSION}). The matching browser release may not be " + f"published yet; wait for it, or install an older camoufox release." + ) def get_path(file: str) -> str: diff --git a/pythonlib/tests/test_version_floor_upgrade.py b/pythonlib/tests/test_version_floor_upgrade.py index 831f6eb..8c59175 100644 --- a/pythonlib/tests/test_version_floor_upgrade.py +++ b/pythonlib/tests/test_version_floor_upgrade.py @@ -95,3 +95,27 @@ def test_root_probe_tolerates_the_versioned_layout(tmp_path, monkeypatch): monkeypatch.setattr(pkgman, "INSTALL_DIR", root) assert pkgman._root_install_supported() is False + + +def test_unsatisfiable_floor_reports_instead_of_recursing(tmp_path, monkeypatch): + """A floor no published build satisfies must report, not spin. + + install() is a no-op when the newest release is already installed, so the + old `return camoufox_path()` tail recursed ~1000 times -- each iteration + firing another GitHub API call, which exhausts the unauthenticated rate + limit long before the RecursionError lands. That is the state a library + published ahead of its browser release puts every user in. + """ + _install(tmp_path, monkeypatch, "versioned", build="beta.29", floor="beta.30") + attempts = [] + + class StubFetcher: + def install(self): + attempts.append(True) # newest published build is still below the floor + + monkeypatch.setattr(pkgman, "CamoufoxFetcher", StubFetcher) + + with pytest.raises(UnsupportedVersion): + pkgman.camoufox_path() + + assert attempts == [True], "should fetch once, not spin"