fix(update): also defer the missing-binary CUA install on Windows

Follow-up to the salvaged #94296: the two guards covered the repair and
confirmed-update branches, but when cua-driver is enabled yet not
installed at all, control still reached _run_cua_driver_installer() and
an automatic 'hermes update' would launch the interactive install.ps1
anyway. Add the same defer before the installer run, keep POSIX
behavior unchanged, and give the confirmed-update message a natural
fallback when latest_version is unknown.
This commit is contained in:
Teknium
2026-08-25 12:50:58 -07:00
parent 0c23bf19af
commit f751a8c546
2 changed files with 69 additions and 9 deletions

View File

@@ -1190,12 +1190,19 @@ def install_cua_driver(
return True
if _state is not None and _state.get("update_available"):
if is_windows and require_confirmed_update:
_latest = _state.get("latest_version") or "a newer version"
_print_info(
f" {driver_cmd} {_latest} is available; keeping the "
"installed version because its Windows installer may "
"require interactive consent."
)
_latest = _state.get("latest_version")
if _latest:
_print_info(
f" {driver_cmd} {_latest} is available; keeping the "
"installed version because its Windows installer may "
"require interactive consent."
)
else:
_print_info(
f" A newer {driver_cmd} release is available; "
"keeping the installed version because its Windows "
"installer may require interactive consent."
)
_print_info(
" Update it from an interactive terminal with: "
"hermes computer-use install --upgrade"
@@ -1214,6 +1221,22 @@ def install_cua_driver(
if _re.fullmatch(r"\d+(\.\d+)*", _latest):
confirmed_version = _latest
if is_windows and require_confirmed_update and not binary:
# Missing-binary path (driver enabled in config but never installed,
# or wiped by a failed install). Same rule as the repair and
# confirmed-update branches above: an automatic Windows update must
# never launch install.ps1, which can demand console/UAC consent the
# hidden updater cannot provide (#87703).
_print_info(
" cua-driver is not installed; automatic Windows updates "
"cannot safely run its interactive installer."
)
_print_info(
" Install it from an interactive terminal with: "
"hermes computer-use install --upgrade"
)
return False
if binary:
# Show before/after version when we have a baseline. Best-effort.
try:

View File

@@ -440,7 +440,8 @@ class TestRequireConfirmedUpdate:
still reinstall when the check can't answer.
"""
def _install(self, check_state, require_confirmed, contract_status=None):
def _install(self, check_state, require_confirmed, contract_status=None,
binary_missing=False):
"""Drive ``install_cua_driver`` on the host, whatever it is.
The old signature took a ``system`` string and faked
@@ -454,11 +455,15 @@ class TestRequireConfirmedUpdate:
from hermes_cli import tools_config
_which_names = {"curl", "powershell"}
if not binary_missing:
_which_names.add("cua-driver")
with patch.object(tools_config.shutil, "which",
side_effect=lambda n: "/x/" + n
if n in {"cua-driver", "curl", "powershell"} else None), \
if n in _which_names else None), \
patch.object(tools_config, "_resolved_cua_driver_cmd",
return_value="/x/cua-driver"), \
return_value=None if binary_missing
else "/x/cua-driver"), \
patch.object(tools_config, "_cua_install_target_writable",
return_value=True), \
patch.object(
@@ -546,6 +551,38 @@ class TestRequireConfirmedUpdate:
for call in info.call_args_list
)
@pytest.mark.windows_only
def test_windows_missing_binary_defers_interactive_install(self):
"""Driver enabled but never installed (or wiped by a failed install):
the automatic update must not launch install.ps1 either — this path
reached the installer before the top-level guard (#94296 review)."""
ok, runner, info = self._install(
None,
require_confirmed=True,
binary_missing=True,
)
assert ok is False
runner.assert_not_called()
assert any(
"computer-use install --upgrade" in call.args[0]
for call in info.call_args_list
)
@pytest.mark.skipif(
sys.platform == "win32",
reason="POSIX installers are non-interactive; missing binary installs",
)
def test_posix_missing_binary_still_installs(self):
ok, runner, _ = self._install(
None,
require_confirmed=True,
binary_missing=True,
)
assert ok is True
runner.assert_called_once()
def test_up_to_date_short_circuits(self):
state = {"current_version": "0.6.0", "latest_version": "0.6.0",
"update_available": False}