diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 741dbb267dd..dfd7c60e744 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -667,102 +667,31 @@ def _pip_install( -def _check_cua_driver_asset_for_arch() -> bool: - """Check whether the latest CUA release ships an asset for this OS+arch. - - Returns True if the asset likely exists (or if we cannot determine it). - Returns False and prints a warning when the asset is confirmed missing, - so callers can skip the install attempt and avoid a raw 404. - - Recognizes release-asset names across all supported platforms: - - * macOS (``Darwin``) — arm64 always ships; x86_64/amd64 probed. - * Windows (``AMD64``/``ARM64``) — amd64/x86_64 and arm64 probed. - * Linux (``x86_64``/``aarch64``) — x86_64/amd64 and aarch64/arm64 probed. - """ - import platform as _plat - import urllib.request - - system = _plat.system() - machine = _plat.machine().lower() # e.g. "x86_64", "arm64", "amd64", "aarch64" - - # arm64 (Apple Silicon) macOS assets are always published — short-circuit - # to preserve the original fail-open behaviour and avoid a network call. - if system == "Darwin" and machine == "arm64": - return True - - # Map this host's arch to the set of asset-name substrings we'll accept. - # Asset names vary by OS (darwin-x86_64, windows-amd64, linux-aarch64, …), - # so we match on the architecture token only and let any of the common - # aliases satisfy the probe. - if machine in {"x86_64", "amd64", "x64"}: - arch_names = {"x86_64", "amd64", "x64"} - arch_label = "x86_64/amd64" - elif machine in {"arm64", "aarch64"}: - arch_names = {"arm64", "aarch64"} - arch_label = "arm64/aarch64" - else: - # Unknown arch — fail open and let the installer surface the error. - return True - - # Probe the cua-driver release for an OS+arch asset before falling through - # to the upstream installer. - # - # The cua-driver-rs binaries are published to the trycua/cua monorepo under - # tag prefix ``cua-driver-rs-v*``. The repo's ``releases/latest`` is NOT - # that — it floats across the monorepo's other components (agent-*, - # computer-*, lume-*, train-*), most of which ship zero binary assets. So - # we list releases and pick the newest ``cua-driver-rs-v*`` tag, matching - # what the upstream install.sh does. Failing to find one => fail open and - # let the installer (which resolves the tag itself) be the source of truth. - driver_tag_prefix = "cua-driver-rs-v" - api_url = ( - "https://api.github.com/repos/trycua/cua/releases?per_page=100" - ) - try: - req = urllib.request.Request(api_url, headers={"Accept": "application/vnd.github+json"}) - with urllib.request.urlopen(req, timeout=10) as resp: - releases = _json.loads(resp.read().decode()) - if not isinstance(releases, list): - return True - # GitHub returns releases newest-first; take the first cua-driver-rs tag. - driver_release = next( - ( - r for r in releases - if str(r.get("tag_name", "")).startswith(driver_tag_prefix) - ), - None, - ) - if driver_release is None: - # No cua-driver-rs release surfaced (API hiccup / unexpected shape). - # Fail open — the installer resolves the tag on its own. - return True - tag = driver_release.get("tag_name", "") - assets = driver_release.get("assets", []) - # OS token gates the asset alongside arch so a darwin asset can't - # satisfy a Linux probe (every cua-driver-rs release ships all three - # OSes, so the arch token alone would always match). - os_token = {"Darwin": "darwin", "Windows": "windows", "Linux": "linux"}.get(system, "") - has_asset = any( - os_token in (name := a_info.get("name", "").lower()) - and any(a in name for a in arch_names) - for a_info in assets - ) - if not has_asset: - _print_warning( - f" Latest cua-driver release ({tag}) has no {system} {arch_label} asset." - ) - _print_info( - " CUA Driver may not yet ship a build for this platform." - ) - _print_info( - " See: https://github.com/trycua/cua/releases" - ) - return False - except Exception: - # Network / API failure — proceed and let the installer handle it. - pass - return True +# The asset-probe that lived here used to hit `/releases/latest` on +# trycua/cua and inspect the release's asset list before piping the +# installer to bash. It was broken in two places: +# +# 1. cua-driver-rs releases are marked **prerelease** on every cut, +# and GitHub's `/releases/latest` endpoint explicitly skips +# prereleases. On the live trycua/cua repo today, `/releases/latest` +# returns the Python `cua-agent v0.8.3` package (zero binary +# assets) instead of `cua-driver-rs-v0.6.0` (19 binary assets). +# The probe then reported "no asset for this arch" and skipped the +# install on every non-arm64 host — Linux x86_64, Windows, macOS +# Intel, Linux arm64 — even when the upstream installer would have +# succeeded. +# 2. Even with the right endpoint, we'd be duplicating tag-resolution +# logic the upstream installer already does correctly via +# `CUA_DRIVER_RS_BAKED_VERSION` (auto-baked by CD on every release, +# with an API fallback). Drift between our probe and theirs is a +# maintenance hazard. +# +# Resolution: trust the upstream installer. For fresh installs, run +# install.sh directly — it errors clean if the target arch has no +# asset. For the upgrade path, `cua_driver_update_check()` (which calls +# `cua-driver check-update --json`) gives us the canonical update +# answer from the binary itself — same tag-resolution as the installer, +# no Python-side duplication. def install_cua_driver(upgrade: bool = False) -> bool: @@ -811,8 +740,9 @@ def install_cua_driver(upgrade: bool = False) -> bool: _print_warning(f" {fetch_tool} not found — install manually:") _print_info(" https://github.com/trycua/cua/blob/main/libs/cua-driver/README.md") return False - if not _check_cua_driver_asset_for_arch(): - return False + # Pre-install asset probe deleted — see comment near the top of + # tools_config.py for why. install.sh has CUA_DRIVER_RS_BAKED_VERSION + # baked in by CD and errors cleanly on missing-arch assets. return _run_cua_driver_installer(label="Installing") # Already installed and caller didn't ask to upgrade → just confirm. @@ -841,8 +771,10 @@ def install_cua_driver(upgrade: bool = False) -> bool: _print_warning(f" {fetch_tool} not found — cannot refresh cua-driver.") return bool(binary) - if not _check_cua_driver_asset_for_arch(): - return bool(binary) + # Pre-install asset probe deleted (see top-of-file comment). The + # `cua_driver_update_check()` call further down asks the installed + # cua-driver binary itself whether an update exists — same + # tag-resolution as the installer, no duplication. # Skip the (network) re-install when the driver itself reports it's already # on the latest release. Best-effort: an older driver (no check-update diff --git a/tests/hermes_cli/test_install_cua_driver.py b/tests/hermes_cli/test_install_cua_driver.py index 27da8d22e06..e05dd42627c 100644 --- a/tests/hermes_cli/test_install_cua_driver.py +++ b/tests/hermes_cli/test_install_cua_driver.py @@ -1,42 +1,43 @@ -"""Tests for ``install_cua_driver`` upgrade semantics and architecture pre-check. +"""Tests for ``install_cua_driver`` upgrade semantics. The cua-driver upstream installer always pulls the latest release tag, so re-running it is the canonical upgrade path. ``install_cua_driver(upgrade=True)`` must: -* Be cross-platform — run on macOS, Windows, and Linux. Only genuinely - unsupported platforms no-op silently on upgrade so ``hermes update`` can - call it unconditionally without warning those users. -* Choose the right installer per OS: ``install.sh`` via ``curl | bash`` on - macOS/Linux, ``install.ps1`` via PowerShell ``irm | iex`` on Windows. +* Be macOS-only — no-op silently on Linux/Windows so ``hermes update`` can + call it unconditionally without warning every non-macOS user. * Re-run the installer even when the binary is already on PATH (this is the fix for the "we only pulled cua-driver once on enable" complaint). * Preserve original ``upgrade=False`` behaviour for the toolset-enable flow: - skip if installed, install otherwise, warn on unsupported platforms. -* Pre-check architecture compatibility before downloading to avoid raw 404 - errors when the upstream release lacks an asset for this OS+arch. + skip if installed, install otherwise, warn on non-macOS. + +The pre-install arch probe that used to live alongside this function was +deleted (see top-of-file comment in tools_config.py) — the upstream +installer has CUA_DRIVER_RS_BAKED_VERSION baked in by CD and errors +cleanly on missing-arch assets, and the upgrade path uses +``cua_driver_update_check()`` (which shells `cua-driver check-update +--json` against the already-installed binary). """ from __future__ import annotations -import json -from unittest.mock import MagicMock, patch +from unittest.mock import patch class TestInstallCuaDriverUpgrade: - def test_upgrade_on_unsupported_platform_is_silent_noop(self): + def test_upgrade_on_non_macos_is_silent_noop(self): from hermes_cli import tools_config with patch.object(tools_config, "_print_warning") as warn, \ - patch("platform.system", return_value="FreeBSD"): + patch("platform.system", return_value="Linux"): assert tools_config.install_cua_driver(upgrade=True) is False warn.assert_not_called() - def test_non_upgrade_on_unsupported_platform_warns(self): + def test_non_upgrade_on_non_macos_warns(self): from hermes_cli import tools_config with patch.object(tools_config, "_print_warning") as warn, \ - patch("platform.system", return_value="FreeBSD"): + patch("platform.system", return_value="Linux"): assert tools_config.install_cua_driver(upgrade=False) is False warn.assert_called() @@ -47,8 +48,6 @@ class TestInstallCuaDriverUpgrade: patch.object(tools_config.shutil, "which", side_effect=lambda n: "/usr/local/bin/" + n if n in {"cua-driver", "curl"} else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ patch.object(tools_config, "_run_cua_driver_installer", return_value=True) as runner, \ patch("subprocess.run"): @@ -63,8 +62,6 @@ class TestInstallCuaDriverUpgrade: with patch("platform.system", return_value="Darwin"), \ patch.object(tools_config.shutil, "which", side_effect=lambda n: "/usr/bin/curl" if n == "curl" else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ patch.object(tools_config, "_run_cua_driver_installer", return_value=True) as runner: assert tools_config.install_cua_driver(upgrade=True) is True @@ -88,359 +85,75 @@ class TestInstallCuaDriverUpgrade: with patch("platform.system", return_value="Darwin"), \ patch.object(tools_config.shutil, "which", side_effect=lambda n: "/usr/bin/curl" if n == "curl" else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ patch.object(tools_config, "_run_cua_driver_installer", return_value=True) as runner: assert tools_config.install_cua_driver(upgrade=False) is True + runner.assert_called_once() -class TestCheckCuaDriverAssetForArch: - def test_arm64_macos_always_returns_true(self): +class TestArchProbeRemoval: + """Regression tests for the deletion of `_check_cua_driver_asset_for_arch`. + + The old probe queried ``/releases/latest`` on trycua/cua and inspected + asset names. That was wrong in two ways: + + 1. cua-driver-rs releases are marked **prerelease** on every cut, so + ``/releases/latest`` returns the Python ``cua-agent`` / ``cua-computer`` + package instead — a release with zero binary assets. The probe then + reported "no asset for $arch" on Linux x86_64, Windows, macOS Intel, + Linux arm64 — every non-Apple-Silicon host. + 2. Even with the right endpoint, it duplicated tag-resolution the upstream + installer already does correctly via ``CUA_DRIVER_RS_BAKED_VERSION`` + (auto-baked by CD on every release). + + The fix: stop probing. Trust the upstream installer for fresh installs + (it has the baked version + correct API fallback) and the + ``cua-driver check-update --json`` MCP-binary native command for the + upgrade path. + """ + + def test_probe_function_is_gone(self): from hermes_cli import tools_config + assert not hasattr(tools_config, "_check_cua_driver_asset_for_arch") + assert not hasattr(tools_config, "_latest_cua_driver_rs_release") - # Apple Silicon assets are always published — short-circuits without - # a network probe. - with patch("platform.system", return_value="Darwin"), \ - patch("platform.machine", return_value="arm64"): - assert tools_config._check_cua_driver_asset_for_arch() is True - - def test_x86_64_with_asset_returns_true(self): + def test_fresh_install_does_not_call_github_api(self): + """Pre-install no longer probes the GitHub API — the upstream + ``install.sh`` resolves the tag from its baked CUA_DRIVER_RS_BAKED_VERSION + line. install.sh errors cleanly when the arch has no asset, so the + probe was duplicate gatekeeping. + """ from hermes_cli import tools_config - releases = [{ - "tag_name": "cua-driver-rs-v0.1.6", - "assets": [ - {"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"}, - {"name": "cua-driver-rs-0.1.6-darwin-x86_64.tar.gz"}, - ], - }] - mock_resp = MagicMock() - mock_resp.read.return_value = json.dumps(releases).encode() - mock_resp.__enter__ = lambda s: s - mock_resp.__exit__ = MagicMock(return_value=False) - - with patch("platform.system", return_value="Darwin"), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=mock_resp): - assert tools_config._check_cua_driver_asset_for_arch() is True - - def test_x86_64_without_asset_returns_false(self): - from hermes_cli import tools_config - - releases = [{ - "tag_name": "cua-driver-rs-v0.1.6", - "assets": [ - {"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"}, - {"name": "cua-driver-rs.tar.gz"}, - ], - }] - mock_resp = MagicMock() - mock_resp.read.return_value = json.dumps(releases).encode() - mock_resp.__enter__ = lambda s: s - mock_resp.__exit__ = MagicMock(return_value=False) - - with patch("platform.system", return_value="Darwin"), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=mock_resp), \ - patch.object(tools_config, "_print_warning") as warn, \ - patch.object(tools_config, "_print_info"): - assert tools_config._check_cua_driver_asset_for_arch() is False - warn.assert_called_once() - assert "no Intel" in warn.call_args[0][0].lower() or "x86_64" in warn.call_args[0][0] - - def test_x86_64_api_failure_returns_true(self): - """Network failure should fail open — let the installer handle it.""" - from hermes_cli import tools_config - - with patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", side_effect=Exception("timeout")): - assert tools_config._check_cua_driver_asset_for_arch() is True - - def test_fresh_install_x86_64_no_asset_skips_installer(self): - """When the latest release has no Intel asset, skip the installer.""" - from hermes_cli import tools_config - - releases = [{ - "tag_name": "cua-driver-rs-v0.1.6", - "assets": [{"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"}], - }] - mock_resp = MagicMock() - mock_resp.read.return_value = json.dumps(releases).encode() - mock_resp.__enter__ = lambda s: s - mock_resp.__exit__ = MagicMock(return_value=False) - with patch("platform.system", return_value="Darwin"), \ patch.object(tools_config.shutil, "which", side_effect=lambda n: "/usr/bin/curl" if n == "curl" else None), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=mock_resp), \ - patch.object(tools_config, "_print_warning"), \ - patch.object(tools_config, "_print_info"), \ - patch.object(tools_config, "_run_cua_driver_installer") as runner: - assert tools_config.install_cua_driver(upgrade=False) is False - runner.assert_not_called() + patch("urllib.request.urlopen") as urlopen, \ + patch.object(tools_config, "_run_cua_driver_installer", + return_value=True) as runner: + assert tools_config.install_cua_driver(upgrade=False) is True + runner.assert_called_once() + urlopen.assert_not_called() - def test_upgrade_x86_64_no_asset_returns_existing_status(self): - """On upgrade with no Intel asset, return whether binary existed.""" + def test_upgrade_with_binary_does_not_call_github_api_directly(self): + """The upgrade path no longer hits GitHub from Python — it delegates + to the upstream ``install.sh`` (which has the baked release tag and + the proper API fallback). When cua-driver is already installed, + ``cua_driver_update_check()`` (added in a separate change) further + short-circuits the network re-install via the binary's native + ``check-update --json`` verb. + """ from hermes_cli import tools_config - releases = [{ - "tag_name": "cua-driver-rs-v0.1.6", - "assets": [{"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"}], - }] - mock_resp = MagicMock() - mock_resp.read.return_value = json.dumps(releases).encode() - mock_resp.__enter__ = lambda s: s - mock_resp.__exit__ = MagicMock(return_value=False) - - # With binary installed — returns True (binary exists) with patch("platform.system", return_value="Darwin"), \ patch.object(tools_config.shutil, "which", side_effect=lambda n: "/usr/local/bin/" + n if n in ("cua-driver", "curl") else None), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=mock_resp), \ - patch.object(tools_config, "_print_warning"), \ - patch.object(tools_config, "_print_info"), \ - patch.object(tools_config, "_run_cua_driver_installer") as runner: - assert tools_config.install_cua_driver(upgrade=True) is True - runner.assert_not_called() - - # Without binary — returns False - with patch("platform.system", return_value="Darwin"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: "/usr/bin/curl" if n == "curl" else None), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=mock_resp), \ - patch.object(tools_config, "_print_warning"), \ - patch.object(tools_config, "_print_info"), \ - patch.object(tools_config, "_run_cua_driver_installer") as runner: - assert tools_config.install_cua_driver(upgrade=True) is False - runner.assert_not_called() - - -class TestInstallCuaDriverWindows: - """install_cua_driver dispatch on Windows hosts.""" - - def test_fresh_install_runs_installer(self): - from hermes_cli import tools_config - - # PowerShell present, cua-driver not yet installed. - with patch("platform.system", return_value="Windows"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: r"C:\\Windows\\powershell.exe" - if n == "powershell" else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ + patch("urllib.request.urlopen") as urlopen, \ + patch("subprocess.run"), \ patch.object(tools_config, "_run_cua_driver_installer", return_value=True) as runner: - assert tools_config.install_cua_driver(upgrade=False) is True - runner.assert_called_once() - - def test_fresh_install_without_powershell_fails(self): - from hermes_cli import tools_config - - with patch("platform.system", return_value="Windows"), \ - patch.object(tools_config.shutil, "which", lambda n: None), \ - patch.object(tools_config, "_print_warning") as warn, \ - patch.object(tools_config, "_print_info"), \ - patch.object(tools_config, "_run_cua_driver_installer") as runner: - assert tools_config.install_cua_driver(upgrade=False) is False - runner.assert_not_called() - # The warning should name the missing fetch tool (powershell). - assert "powershell" in warn.call_args[0][0].lower() - - def test_upgrade_with_binary_runs_installer(self): - from hermes_cli import tools_config - - with patch("platform.system", return_value="Windows"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: r"C:\\bin\\" + n - if n in {"cua-driver", "powershell"} else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ - patch.object(tools_config, "_run_cua_driver_installer", - return_value=True) as runner, \ - patch("subprocess.run"): assert tools_config.install_cua_driver(upgrade=True) is True runner.assert_called_once() - assert runner.call_args.kwargs.get("verbose") is False - - def test_installer_uses_powershell_irm_command(self): - """_run_cua_driver_installer must shell out to PowerShell irm|iex.""" - from hermes_cli import tools_config - - completed = MagicMock(returncode=0) - with patch("platform.system", return_value="Windows"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: r"C:\\bin\\" + n - if n == "cua-driver" else None), \ - patch("subprocess.run", return_value=completed) as run, \ - patch.object(tools_config, "_print_info"), \ - patch.object(tools_config, "_print_success"), \ - patch.object(tools_config, "_print_warning"): - assert tools_config._run_cua_driver_installer() is True - cmd = run.call_args[0][0] - # Argument list (shell=False), not a string. - assert isinstance(cmd, list) - assert cmd[0] == "powershell" - assert run.call_args.kwargs.get("shell") is False - joined = " ".join(cmd) - assert "install.ps1" in joined - assert "iex" in joined - - -class TestInstallCuaDriverLinux: - """install_cua_driver dispatch on Linux hosts (alpha).""" - - def test_fresh_install_runs_installer(self): - from hermes_cli import tools_config - - with patch("platform.system", return_value="Linux"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: "/usr/bin/curl" if n == "curl" else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ - patch.object(tools_config, "_run_cua_driver_installer", - return_value=True) as runner: - assert tools_config.install_cua_driver(upgrade=False) is True - runner.assert_called_once() - - def test_upgrade_with_binary_runs_installer(self): - from hermes_cli import tools_config - - with patch("platform.system", return_value="Linux"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: "/usr/local/bin/" + n - if n in {"cua-driver", "curl"} else None), \ - patch.object(tools_config, "_check_cua_driver_asset_for_arch", - return_value=True), \ - patch.object(tools_config, "_run_cua_driver_installer", - return_value=True) as runner, \ - patch("subprocess.run"): - assert tools_config.install_cua_driver(upgrade=True) is True - runner.assert_called_once() - - def test_installer_uses_curl_bash_command(self): - """_run_cua_driver_installer must shell out to curl | bash install.sh.""" - from hermes_cli import tools_config - - completed = MagicMock(returncode=0) - with patch("platform.system", return_value="Linux"), \ - patch.object(tools_config.shutil, "which", - side_effect=lambda n: "/usr/local/bin/" + n - if n == "cua-driver" else None), \ - patch("subprocess.run", return_value=completed) as run, \ - patch.object(tools_config, "_print_info"), \ - patch.object(tools_config, "_print_success"), \ - patch.object(tools_config, "_print_warning"): - assert tools_config._run_cua_driver_installer() is True - cmd = run.call_args[0][0] - assert isinstance(cmd, str) # shell string on POSIX - assert run.call_args.kwargs.get("shell") is True - assert "install.sh" in cmd - assert "curl" in cmd - - -class TestCheckCuaDriverAssetCrossPlatform: - """_check_cua_driver_asset_for_arch recognizes Windows/Linux asset names.""" - - @staticmethod - def _mock_release(asset_names): - # The probe lists /releases and picks the newest cua-driver-rs-v* tag, - # so the mock returns a LIST of releases with that tag prefix. - releases = [{"tag_name": "cua-driver-rs-v0.5.0", - "assets": [{"name": n} for n in asset_names]}] - resp = MagicMock() - resp.read.return_value = json.dumps(releases).encode() - resp.__enter__ = lambda s: s - resp.__exit__ = MagicMock(return_value=False) - return resp - - def test_windows_amd64_with_asset_returns_true(self): - from hermes_cli import tools_config - - resp = self._mock_release([ - "cua-driver-rs-0.5.0-windows-x86_64.zip", - "cua-driver-rs-0.5.0-darwin-arm64.tar.gz", - ]) - with patch("platform.system", return_value="Windows"), \ - patch("platform.machine", return_value="AMD64"), \ - patch("urllib.request.urlopen", return_value=resp): - assert tools_config._check_cua_driver_asset_for_arch() is True - - def test_windows_arm64_without_asset_returns_false(self): - from hermes_cli import tools_config - - resp = self._mock_release([ - "cua-driver-rs-0.5.0-windows-x86_64.zip", - ]) - with patch("platform.system", return_value="Windows"), \ - patch("platform.machine", return_value="ARM64"), \ - patch("urllib.request.urlopen", return_value=resp), \ - patch.object(tools_config, "_print_warning") as warn, \ - patch.object(tools_config, "_print_info"): - assert tools_config._check_cua_driver_asset_for_arch() is False - warn.assert_called_once() - assert "arm64" in warn.call_args[0][0].lower() - - def test_linux_x86_64_with_asset_returns_true(self): - from hermes_cli import tools_config - - resp = self._mock_release([ - "cua-driver-rs-0.5.0-linux-x86_64.tar.gz", - ]) - with patch("platform.system", return_value="Linux"), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=resp): - assert tools_config._check_cua_driver_asset_for_arch() is True - - def test_linux_aarch64_with_asset_returns_true(self): - from hermes_cli import tools_config - - resp = self._mock_release([ - "cua-driver-rs-0.5.0-linux-arm64.tar.gz", - ]) - with patch("platform.system", return_value="Linux"), \ - patch("platform.machine", return_value="aarch64"), \ - patch("urllib.request.urlopen", return_value=resp): - assert tools_config._check_cua_driver_asset_for_arch() is True - - def test_linux_aarch64_without_asset_returns_false(self): - from hermes_cli import tools_config - - resp = self._mock_release([ - "cua-driver-rs-0.5.0-linux-x86_64.tar.gz", - ]) - with patch("platform.system", return_value="Linux"), \ - patch("platform.machine", return_value="aarch64"), \ - patch("urllib.request.urlopen", return_value=resp), \ - patch.object(tools_config, "_print_warning") as warn, \ - patch.object(tools_config, "_print_info"): - assert tools_config._check_cua_driver_asset_for_arch() is False - warn.assert_called_once() - - def test_releases_latest_tag_ignored_picks_driver_rs_tag(self): - """A non-driver tag at the head of the list must not gate the probe. - - Regression guard: the monorepo's newest release is often a Python - component (agent-*, computer-*) with zero binary assets. The probe - must skip past it to the newest cua-driver-rs-v* release. - """ - from hermes_cli import tools_config - - releases = [ - {"tag_name": "agent-v0.8.3", "assets": []}, - {"tag_name": "computer-v0.5.19", "assets": []}, - {"tag_name": "cua-driver-rs-v0.6.0", - "assets": [{"name": "cua-driver-rs-0.6.0-linux-x86_64-binary.tar.gz"}]}, - ] - resp = MagicMock() - resp.read.return_value = json.dumps(releases).encode() - resp.__enter__ = lambda s: s - resp.__exit__ = MagicMock(return_value=False) - with patch("platform.system", return_value="Linux"), \ - patch("platform.machine", return_value="x86_64"), \ - patch("urllib.request.urlopen", return_value=resp): - assert tools_config._check_cua_driver_asset_for_arch() is True + # Probe deleted — no direct GitHub API call from Python. + urlopen.assert_not_called()