Competing installers and checkout-local venv assumptions bypassed PM selection, install consent, and generation lifetimes. Route consumers through PM and installation-bound launchers. Refresh source launchers before obsolete Python entries can be collected. Remove Node, browser, and CUA acquisition engines, obsolete venv-holder handling, detached sync, and unused PM APIs. Keep historical updater exports inert and preserve external tool ownership and native integration. Share product freshness and prepared inputs across builders. Align plugin admission, Docker provisioning, setup instructions, and behavioral tests. Verified targeted Python and JavaScript tests, desktop and web typechecks, scoped lint, real product builds, and the Docker frontend smoke test. The missed post-setup test cleanup is included and verified. Native Windows/macOS execution, full Rust compilation, and the complete repository suite remain unverified. Historical compatibility requirements were preserved and extended, not fully rescanned.
257 lines
11 KiB
Python
257 lines
11 KiB
Python
"""Tests for the cua-driver --no-overlay policy.
|
|
|
|
cua-driver's cursor overlay rendering loop can consume CPU indefinitely when
|
|
idle (#28152, #47032), and on Linux/X11 its fullscreen always-on-top overlay
|
|
window can wedge the desktop when a session ends uncleanly. Hermes passes
|
|
``--no-overlay`` to suppress it when the ``computer_use.no_overlay`` config is
|
|
enabled (or auto-detected on macOS, headless Linux / WSL2, and Linux X11).
|
|
|
|
These assert the behavior contract (auto-detect, explicit override, version
|
|
probe), not specific config snapshots.
|
|
"""
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
import pytest
|
|
|
|
from tools.computer_use import cua_backend
|
|
from tools.computer_use import cua_backend_driver
|
|
|
|
|
|
class TestNoOverlayFlag:
|
|
|
|
|
|
|
|
|
|
|
|
def test_explicit_true_overrides(self):
|
|
with patch("hermes_cli.config.load_config",
|
|
return_value={"computer_use": {"no_overlay": True}}):
|
|
assert cua_backend._cua_no_overlay() is True
|
|
|
|
|
|
@pytest.mark.platforms("macos")
|
|
def test_config_load_failure_falls_through_to_auto_detect_macos(self):
|
|
"""Unreadable config => auto-detect (macOS defaults to overlay off).
|
|
|
|
macOS-only: the auto-detect verdict IS ``sys.platform == "darwin"``,
|
|
so a patched platform would only re-assert the patch.
|
|
"""
|
|
with patch("hermes_cli.config.load_config",
|
|
side_effect=RuntimeError("boom")):
|
|
assert cua_backend._cua_no_overlay() is True
|
|
|
|
@pytest.mark.platforms("linux")
|
|
def test_config_load_failure_falls_through_to_auto_detect_linux(self, monkeypatch):
|
|
"""Unreadable config must not raise; headless Linux auto-detects off.
|
|
|
|
Linux-only: the auto-detect branch here keys off ``DISPLAY`` and
|
|
``/proc/version``, neither of which exists to be probed elsewhere.
|
|
"""
|
|
monkeypatch.delenv("DISPLAY", raising=False)
|
|
with patch("hermes_cli.config.load_config",
|
|
side_effect=RuntimeError("boom")):
|
|
assert cua_backend._cua_no_overlay() is True
|
|
|
|
@pytest.mark.platforms("linux")
|
|
def test_linux_x11_auto_detects_off(self, monkeypatch):
|
|
"""X11 desktop (DISPLAY set, no Wayland) defaults the overlay off.
|
|
|
|
The X11 overlay is a fullscreen always-on-top all-workspaces window
|
|
that can get stuck over every workspace after an unclean session end,
|
|
wedging desktop input until the app restarts. Config must not need to
|
|
opt out per-machine.
|
|
"""
|
|
monkeypatch.setenv("DISPLAY", ":0")
|
|
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
|
monkeypatch.delenv("XDG_SESSION_TYPE", raising=False)
|
|
with patch("hermes_cli.config.load_config", return_value={}):
|
|
assert cua_backend._cua_no_overlay() is True
|
|
|
|
@pytest.mark.platforms("linux")
|
|
def test_linux_x11_explicit_session_type_also_off(self, monkeypatch):
|
|
"""XDG_SESSION_TYPE=x11 without Wayland env is still X11."""
|
|
monkeypatch.setenv("DISPLAY", ":0")
|
|
monkeypatch.setenv("XDG_SESSION_TYPE", "x11")
|
|
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
|
with patch("hermes_cli.config.load_config", return_value={}):
|
|
assert cua_backend._cua_no_overlay() is True
|
|
|
|
@pytest.mark.platforms("linux")
|
|
def test_linux_wayland_keeps_overlay(self, monkeypatch):
|
|
"""Wayland desktop keeps the overlay: the compositor owns the
|
|
overlay surface lifecycle, so it cannot get stuck above every
|
|
workspace the way an X11 window can."""
|
|
monkeypatch.setenv("DISPLAY", ":0")
|
|
monkeypatch.setenv("WAYLAND_DISPLAY", "wayland-0")
|
|
monkeypatch.setenv("XDG_SESSION_TYPE", "wayland")
|
|
with patch("hermes_cli.config.load_config", return_value={}):
|
|
assert cua_backend._cua_no_overlay() is False
|
|
|
|
@pytest.mark.platforms("linux")
|
|
def test_linux_x11_explicit_false_overrides_auto_detect(self, monkeypatch):
|
|
"""An explicit ``no_overlay: false`` must restore the cursor even on
|
|
X11 — auto-detection is the default, never a hard lock."""
|
|
monkeypatch.setenv("DISPLAY", ":0")
|
|
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
|
monkeypatch.delenv("XDG_SESSION_TYPE", raising=False)
|
|
with patch("hermes_cli.config.load_config",
|
|
return_value={"computer_use": {"no_overlay": False}}):
|
|
assert cua_backend._cua_no_overlay() is False
|
|
|
|
|
|
|
|
|
|
class TestDriverSupportsNoOverlay:
|
|
def test_returns_true_when_help_shows_flag(self):
|
|
fake_help = "Usage: cua-driver [OPTIONS] COMMAND\n --no-overlay Disable cursor overlay\n"
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value.stdout = fake_help
|
|
mock_run.return_value.stderr = ""
|
|
assert cua_backend_driver._cua_driver_supports_no_overlay("cua-driver") is True
|
|
|
|
|
|
|
|
def test_help_probe_passes_sanitized_env(self):
|
|
"""The ``--help`` subprocess must not leak provider credentials
|
|
via the inherited parent environment (third-party binary; same
|
|
policy as the manifest probe and MCP spawn).
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(stdout="--no-overlay in help", stderr="")
|
|
cua_backend_driver._cua_driver_supports_no_overlay.cache_clear()
|
|
cua_backend_driver._cua_driver_supports_no_overlay("cua-driver")
|
|
kwargs = mock_run.call_args.kwargs
|
|
assert "env" in kwargs, (
|
|
"subprocess.run was called without env= — cua-driver is a "
|
|
"third-party binary and must not receive inherited secrets"
|
|
)
|
|
# The sanitized env must come from the same helper the MCP
|
|
# spawn uses, so the policy is consistent across every
|
|
# cua-driver invocation in this file.
|
|
assert kwargs["env"] is not None
|
|
|
|
|
|
class TestMcpInvocationUsesResolvedCommand:
|
|
"""Surface 8 (NousResearch/hermes-agent#47072) + sweeper feedback
|
|
#4701565902: when the manifest surfaces a relocated executable for
|
|
``mcp_invocation.command``, the support probe must run against THAT
|
|
binary, not the system-resolved ``_CUA_DRIVER_CMD``. Otherwise a
|
|
wrapper/relocation with a different feature set either crashes on
|
|
the unknown flag (when the probe falsely reports support) or
|
|
silently keeps an unwanted overlay (when the probe falsely reports
|
|
no support).
|
|
"""
|
|
|
|
@staticmethod
|
|
def _fake_run(stdout: str = "", returncode: int = 0):
|
|
from unittest.mock import MagicMock
|
|
def _run(*args, **kwargs):
|
|
proc = MagicMock()
|
|
proc.stdout = stdout
|
|
proc.returncode = returncode
|
|
return proc
|
|
return _run
|
|
|
|
def test_manifest_command_drives_support_probe(self):
|
|
"""When the manifest returns a distinct command, the support
|
|
probe runs against the manifest command, not the input
|
|
``driver_cmd`` parameter.
|
|
"""
|
|
from unittest.mock import patch
|
|
from tools.computer_use.cua_backend_driver import _resolve_mcp_invocation
|
|
|
|
manifest = (
|
|
'{"mcp_invocation":'
|
|
'{"command":"/opt/relocated/cua-driver","args":["mcp"]}}'
|
|
)
|
|
with patch("subprocess.run", new=self._fake_run(stdout=manifest)), \
|
|
patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
|
|
patch.object(
|
|
cua_backend_driver, "_cua_driver_supports_no_overlay",
|
|
return_value=True,
|
|
) as mock_probe:
|
|
cua_backend_driver._cua_driver_supports_no_overlay.cache_clear()
|
|
cmd, args = _resolve_mcp_invocation("/usr/bin/cua-driver")
|
|
assert cmd == "/opt/relocated/cua-driver"
|
|
# The support probe must be called with the manifest-resolved
|
|
# command, not the input driver_cmd argument.
|
|
mock_probe.assert_called_with("/opt/relocated/cua-driver")
|
|
|
|
|
|
def test_probe_distinguishes_support_between_binaries(self):
|
|
"""Different binaries must produce independent support verdicts.
|
|
The cache is keyed on ``driver_cmd``; the same cached result
|
|
must not leak between the system binary and a manifest-relocated
|
|
one.
|
|
"""
|
|
with patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
|
|
patch.object(
|
|
cua_backend_driver, "_cua_driver_supports_no_overlay",
|
|
side_effect=lambda cmd: cmd == "/opt/relocated/cua-driver",
|
|
):
|
|
# System binary does NOT support, manifest binary DOES.
|
|
args = cua_backend_driver._mcp_args_with_overlay_flag(
|
|
["mcp"], driver_cmd="/usr/bin/cua-driver",
|
|
)
|
|
assert "--no-overlay" not in args
|
|
args = cua_backend_driver._mcp_args_with_overlay_flag(
|
|
["mcp"], driver_cmd="/opt/relocated/cua-driver",
|
|
)
|
|
assert "--no-overlay" in args
|
|
|
|
|
|
class TestMcpArgsOverlayFlag:
|
|
def test_appended_when_enabled_and_supported(self):
|
|
with patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
|
|
patch.object(cua_backend_driver, "_cua_driver_supports_no_overlay", return_value=True):
|
|
result = cua_backend_driver._mcp_args_with_overlay_flag(["mcp"])
|
|
assert result == ["mcp", "--no-overlay"]
|
|
|
|
def test_not_appended_when_disabled(self):
|
|
with patch.object(cua_backend, "_cua_no_overlay", return_value=False), \
|
|
patch.object(cua_backend_driver, "_cua_driver_supports_no_overlay", return_value=True):
|
|
result = cua_backend_driver._mcp_args_with_overlay_flag(["mcp"])
|
|
assert result == ["mcp"]
|
|
|
|
|
|
def test_does_not_mutate_original_list(self):
|
|
original = ["mcp"]
|
|
with patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
|
|
patch.object(cua_backend_driver, "_cua_driver_supports_no_overlay", return_value=True):
|
|
result = cua_backend_driver._mcp_args_with_overlay_flag(original)
|
|
assert "--no-overlay" in result
|
|
assert "--no-overlay" not in original
|
|
|
|
|
|
class TestEmbeddedDaemonOverlayFlag:
|
|
@pytest.mark.platforms("not macos")
|
|
def test_serve_process_disables_overlay_when_policy_requires_it(self):
|
|
from tools.computer_use import cua_backend_daemon
|
|
|
|
daemon = cua_backend._EmbeddedCuaDaemon("/usr/bin/cua-driver", "unrestricted")
|
|
process = MagicMock()
|
|
process.poll.return_value = None
|
|
status = MagicMock(returncode=0)
|
|
|
|
with patch.object(
|
|
cua_backend_driver,
|
|
"_resolve_mcp_invocation",
|
|
return_value=("/usr/bin/cua-driver", ["mcp"]),
|
|
), patch.object(
|
|
cua_backend, "_cua_no_overlay", return_value=True,
|
|
), patch.object(
|
|
cua_backend_driver, "_cua_driver_supports_no_overlay", return_value=True,
|
|
), patch.object(
|
|
cua_backend.subprocess, "Popen", return_value=process,
|
|
) as popen, patch.object(
|
|
cua_backend.subprocess, "run", return_value=status,
|
|
), patch.object(cua_backend_daemon.threading, "Thread"):
|
|
daemon.start()
|
|
|
|
command = popen.call_args.args[0]
|
|
assert command[:2] == ["/usr/bin/cua-driver", "serve"]
|
|
assert "--no-overlay" in command
|