diff --git a/src/invisible_playwright/constants.py b/src/invisible_playwright/constants.py index 95a150d..a4b998c 100644 --- a/src/invisible_playwright/constants.py +++ b/src/invisible_playwright/constants.py @@ -7,7 +7,14 @@ bugfixes don't force a multi-hour Firefox rebuild. from __future__ import annotations # Bump this when a new patched Firefox build is released on GitHub. -BINARY_VERSION: str = "firefox-8" +BINARY_VERSION: str = "firefox-9" + +# Releases known to be broken — ensure_binary() refuses them with a clear error +# instead of handing the user an unusable binary. firefox-8 was packaged without +# the juggler automation layer, so Playwright cannot drive it (TargetClosedError); +# fixed in firefox-9 (package-manifest.in now ships chrome/juggler). A cached +# firefox-8 from before the bump would otherwise keep being used silently. +BROKEN_VERSIONS: frozenset[str] = frozenset({"firefox-8"}) # Underlying Firefox version (for display only; does not drive downloads). FIREFOX_UPSTREAM_VERSION: str = "150.0.1" diff --git a/src/invisible_playwright/download.py b/src/invisible_playwright/download.py index 13dd62c..acb5d49 100644 --- a/src/invisible_playwright/download.py +++ b/src/invisible_playwright/download.py @@ -21,6 +21,7 @@ from .constants import ( ARCHIVE_NAME, BINARY_ENTRY_REL, BINARY_VERSION, + BROKEN_VERSIONS, GEOIP_ASSET, GEOIP_MMDB_NAME, GEOIP_MMDB_VERSION, @@ -148,6 +149,12 @@ def _post_extract_darwin(app_root: Path, entry: Path) -> None: def ensure_binary(version: str = BINARY_VERSION) -> Path: """Return a path to a runnable Firefox executable. Download if needed.""" + if version in BROKEN_VERSIONS: + raise RuntimeError( + f"{version} is a known-broken release (the juggler automation layer is " + f"missing, so Playwright cannot drive it). Upgrade invisible_playwright " + f"(current BINARY_VERSION={BINARY_VERSION}) or pass a newer version." + ) plat = sys.platform mach = platform.machine() asset = ARCHIVE_NAME(plat, mach) diff --git a/tests/test_constants.py b/tests/test_constants.py index 995fb62..911ad70 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -5,11 +5,26 @@ from invisible_playwright.constants import ( BINARY_BASENAME, BINARY_ENTRY_REL, BINARY_VERSION, + BROKEN_VERSIONS, FIREFOX_UPSTREAM_VERSION, RELEASE_URL_TEMPLATE, ) +@pytest.mark.unit +def test_broken_versions_excludes_current(): + """The current BINARY_VERSION must NEVER be in BROKEN_VERSIONS — otherwise + every default ensure_binary() call would raise and the wrapper is unusable.""" + assert BINARY_VERSION not in BROKEN_VERSIONS + + +@pytest.mark.unit +def test_firefox_8_is_marked_broken(): + """firefox-8 shipped without the juggler layer (undrivable by Playwright); + it must stay flagged so a stale cache can't silently hand it to a user.""" + assert "firefox-8" in BROKEN_VERSIONS + + @pytest.mark.unit def test_binary_version_format(): assert BINARY_VERSION.startswith("firefox-") diff --git a/tests/test_download.py b/tests/test_download.py index 41096ff..e4159ca 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -832,3 +832,11 @@ def test_parse_owner_repo_handles_repos_with_dashes_and_underscores(): ) assert owner == "my-org" assert repo == "my_cool.repo" + + +@pytest.mark.unit +def test_ensure_binary_refuses_known_broken_version(): + """A known-broken release (firefox-8, no juggler) must be refused with a + clear error BEFORE any download — never silently handed to the user.""" + with pytest.raises(RuntimeError, match="known-broken"): + ensure_binary("firefox-8")