mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
The upstream cua-driver installer resolves the latest release and attempts to download an architecture-specific asset. When the release only ships arm64 builds (as of v0.1.6), the installer fails with a raw 404 on Intel macOS with no clear path forward. Add _check_cua_driver_asset_for_arch() that probes the GitHub Releases API before running the installer. If the latest release has no x86_64/amd64 asset, print a clear warning and link to the upstream issue. On arm64 or API failure, fail open and let the installer proceed as before. Fixes #24530
212 lines
9.9 KiB
Python
212 lines
9.9 KiB
Python
"""Tests for ``install_cua_driver`` upgrade semantics and architecture pre-check.
|
|
|
|
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 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 non-macOS.
|
|
* Pre-check architecture compatibility before downloading to avoid raw 404
|
|
errors on Intel macOS when the upstream release lacks x86_64 assets.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
class TestInstallCuaDriverUpgrade:
|
|
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="Linux"):
|
|
assert tools_config.install_cua_driver(upgrade=True) is False
|
|
warn.assert_not_called()
|
|
|
|
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="Linux"):
|
|
assert tools_config.install_cua_driver(upgrade=False) is False
|
|
warn.assert_called()
|
|
|
|
def test_upgrade_on_macos_with_binary_runs_installer(self):
|
|
from hermes_cli import tools_config
|
|
|
|
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.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()
|
|
kwargs = runner.call_args.kwargs
|
|
assert kwargs.get("verbose") is False
|
|
|
|
def test_upgrade_on_macos_without_binary_runs_installer(self):
|
|
from hermes_cli import tools_config
|
|
|
|
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
|
|
runner.assert_called_once()
|
|
|
|
def test_non_upgrade_on_macos_with_binary_skips_install(self):
|
|
from hermes_cli import tools_config
|
|
|
|
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.object(tools_config, "_run_cua_driver_installer") as runner, \
|
|
patch("subprocess.run"):
|
|
assert tools_config.install_cua_driver(upgrade=False) is True
|
|
runner.assert_not_called()
|
|
|
|
def test_non_upgrade_on_macos_without_binary_runs_installer(self):
|
|
from hermes_cli import tools_config
|
|
|
|
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
|
|
|
|
|
|
class TestCheckCuaDriverAssetForArch:
|
|
def test_arm64_always_returns_true(self):
|
|
from hermes_cli import tools_config
|
|
|
|
with 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):
|
|
from hermes_cli import tools_config
|
|
|
|
release = {
|
|
"tag_name": "cua-driver-v0.1.6",
|
|
"assets": [
|
|
{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"},
|
|
{"name": "cua-driver-0.1.6-darwin-x86_64.tar.gz"},
|
|
],
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(release).encode()
|
|
mock_resp.__enter__ = lambda s: s
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
|
|
with 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
|
|
|
|
release = {
|
|
"tag_name": "cua-driver-v0.1.6",
|
|
"assets": [
|
|
{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"},
|
|
{"name": "cua-driver.tar.gz"},
|
|
],
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(release).encode()
|
|
mock_resp.__enter__ = lambda s: s
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
|
|
with 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
|
|
|
|
release = {
|
|
"tag_name": "cua-driver-v0.1.6",
|
|
"assets": [{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"}],
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(release).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()
|
|
|
|
def test_upgrade_x86_64_no_asset_returns_existing_status(self):
|
|
"""On upgrade with no Intel asset, return whether binary existed."""
|
|
from hermes_cli import tools_config
|
|
|
|
release = {
|
|
"tag_name": "cua-driver-v0.1.6",
|
|
"assets": [{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"}],
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(release).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()
|