chore: import upstream snapshot with attribution
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""Regression test: the cua-driver CLI-fallback transport must sanitize the
|
||||
subprocess environment like every other cua-driver spawn site.
|
||||
|
||||
``_CuaDriverSession._call_tool_via_cli()`` (the EAGAIN/silent-empty MCP
|
||||
fallback) invoked ``subprocess.run`` with no ``env=`` at all, so the
|
||||
third-party ``cua-driver`` binary inherited the full, unsanitized parent
|
||||
environment — including provider API keys and other Hermes-managed
|
||||
secrets that ``_lifecycle_coro``'s primary MCP spawn already strips via
|
||||
``_sanitize_subprocess_env(cua_driver_child_env())``.
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from tools.computer_use.cua_backend import _CuaDriverSession
|
||||
|
||||
|
||||
def _make_session() -> _CuaDriverSession:
|
||||
# _call_tool_via_cli() doesn't touch any instance state (bridge/session/
|
||||
# capabilities); bypass __init__ so the test doesn't need a real
|
||||
# _AsyncBridge.
|
||||
return object.__new__(_CuaDriverSession)
|
||||
|
||||
|
||||
def _fake_completed_process(stdout: str) -> MagicMock:
|
||||
proc = MagicMock()
|
||||
proc.stdout = stdout
|
||||
proc.stderr = ""
|
||||
proc.returncode = 0
|
||||
return proc
|
||||
|
||||
|
||||
def test_cli_fallback_strips_provider_secret_from_subprocess_env(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-super-secret-should-not-leak")
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin")
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["env"] = kwargs.get("env")
|
||||
return _fake_completed_process(json.dumps({"tree_markdown": "root"}))
|
||||
|
||||
monkeypatch.setattr("subprocess.run", fake_run)
|
||||
|
||||
session = _make_session()
|
||||
result = session._call_tool_via_cli("list_windows", {}, timeout=5.0)
|
||||
|
||||
assert result["isError"] is False
|
||||
assert captured["env"] is not None, "subprocess.run must receive an explicit env="
|
||||
assert "ANTHROPIC_API_KEY" not in captured["env"]
|
||||
# Sanitization filters secrets, not everything — an ordinary var survives.
|
||||
assert captured["env"].get("PATH") == "/usr/bin:/bin"
|
||||
|
||||
|
||||
def test_cli_fallback_applies_telemetry_policy(monkeypatch):
|
||||
"""The env should also go through cua_driver_child_env(), like every
|
||||
other cua-driver spawn site, not just _sanitize_subprocess_env alone."""
|
||||
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
|
||||
captured = {}
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["env"] = kwargs.get("env")
|
||||
return _fake_completed_process(json.dumps({"tree_markdown": "root"}))
|
||||
|
||||
monkeypatch.setattr("subprocess.run", fake_run)
|
||||
|
||||
session = _make_session()
|
||||
session._call_tool_via_cli("list_windows", {}, timeout=5.0)
|
||||
|
||||
# cua_driver_child_env() injects this when telemetry is disabled
|
||||
# (the default) — confirms the fallback goes through the same helper
|
||||
# the sanctioned spawn site uses, not an ad hoc env dict.
|
||||
assert captured["env"].get("CUA_DRIVER_RS_TELEMETRY_ENABLED") == "0"
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Regression tests: every remaining cua-driver spawn site must sanitize the
|
||||
subprocess environment.
|
||||
|
||||
PR #58889 fixed the CLI-fallback transport; review of that fix found four
|
||||
sibling spawn sites still handing the third-party ``cua-driver`` binary the
|
||||
full parent environment (provider API keys included):
|
||||
|
||||
- ``cua_backend._resolve_mcp_invocation`` (``cua-driver manifest``) — no
|
||||
``env=`` at all
|
||||
- ``cua_backend.cua_driver_update_check`` (``check-update --json``) —
|
||||
telemetry env but no secret sanitization
|
||||
- ``doctor._drive_health_report`` (``<binary> mcp``) — telemetry env only
|
||||
- ``permissions._run`` (every permission probe) — telemetry env only
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
SECRET = "sk-super-secret-should-not-leak"
|
||||
|
||||
|
||||
def _fake_completed_process(stdout: str) -> MagicMock:
|
||||
proc = MagicMock()
|
||||
proc.stdout = stdout
|
||||
proc.stderr = ""
|
||||
proc.returncode = 0
|
||||
return proc
|
||||
|
||||
|
||||
def _capture_run(captured, stdout=""):
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
captured["env"] = kwargs.get("env")
|
||||
return _fake_completed_process(stdout)
|
||||
return fake_run
|
||||
|
||||
|
||||
def _assert_sanitized(captured):
|
||||
env = captured["env"]
|
||||
assert env is not None, "subprocess must receive an explicit env="
|
||||
assert "ANTHROPIC_API_KEY" not in env
|
||||
# Sanitization filters secrets, not everything — ordinary vars survive.
|
||||
assert env.get("PATH") == "/usr/bin:/bin"
|
||||
# Confirms the telemetry helper still ran (default: telemetry disabled).
|
||||
assert env.get("CUA_DRIVER_RS_TELEMETRY_ENABLED") == "0"
|
||||
|
||||
|
||||
def test_resolve_mcp_invocation_sanitizes_env(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin")
|
||||
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
|
||||
|
||||
from tools.computer_use import cua_backend
|
||||
|
||||
captured = {}
|
||||
manifest = json.dumps({"mcp_invocation": {"command": "cua-driver", "args": ["mcp"]}})
|
||||
monkeypatch.setattr(
|
||||
cua_backend.subprocess, "run", _capture_run(captured, stdout=manifest)
|
||||
)
|
||||
|
||||
cmd, args = cua_backend._resolve_mcp_invocation("cua-driver")
|
||||
assert cmd == "cua-driver"
|
||||
_assert_sanitized(captured)
|
||||
|
||||
|
||||
def test_update_check_sanitizes_env(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin")
|
||||
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
|
||||
|
||||
from tools.computer_use import cua_backend
|
||||
|
||||
captured = {}
|
||||
payload = json.dumps({
|
||||
"current_version": "1.0.0",
|
||||
"latest_version": "1.0.0",
|
||||
"update_available": False,
|
||||
})
|
||||
monkeypatch.setattr(
|
||||
cua_backend.subprocess, "run", _capture_run(captured, stdout=payload)
|
||||
)
|
||||
|
||||
cua_backend.cua_driver_update_check(timeout=1.0)
|
||||
_assert_sanitized(captured)
|
||||
|
||||
|
||||
def test_permissions_run_sanitizes_env(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin")
|
||||
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
|
||||
|
||||
from tools.computer_use import permissions
|
||||
|
||||
captured = {}
|
||||
monkeypatch.setattr(
|
||||
permissions.subprocess, "run", _capture_run(captured, stdout="{}")
|
||||
)
|
||||
|
||||
permissions._run("cua-driver", "doctor", "--json", timeout=1.0)
|
||||
_assert_sanitized(captured)
|
||||
|
||||
|
||||
def test_doctor_sanitized_env_helper(monkeypatch):
|
||||
"""_drive_health_report spawns via Popen; assert the env helper it uses
|
||||
strips secrets (mocking the whole JSON-RPC handshake is not worth it)."""
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin")
|
||||
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
|
||||
|
||||
from tools.computer_use import doctor
|
||||
import inspect
|
||||
|
||||
env = doctor._sanitized_cua_env()
|
||||
assert "ANTHROPIC_API_KEY" not in env
|
||||
assert env.get("PATH") == "/usr/bin:/bin"
|
||||
assert env.get("CUA_DRIVER_RS_TELEMETRY_ENABLED") == "0"
|
||||
|
||||
# The Popen spawn site must actually use the sanitized helper.
|
||||
src = inspect.getsource(doctor._drive_health_report)
|
||||
assert "_sanitized_cua_env()" in src
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for the cua-driver telemetry opt-in policy.
|
||||
|
||||
cua-driver ships anonymous PostHog telemetry ENABLED by default upstream.
|
||||
Hermes disables it unless the user opts in via
|
||||
``computer_use.cua_telemetry: true``. The policy is applied by injecting
|
||||
``CUA_DRIVER_RS_TELEMETRY_ENABLED=0`` into every cua-driver child env.
|
||||
|
||||
These assert the behavior contract (default disables, opt-in leaves the var
|
||||
untouched, config failure fails safe toward disabled), not specific config
|
||||
snapshots.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from tools.computer_use import cua_backend
|
||||
|
||||
|
||||
_VAR = "CUA_DRIVER_RS_TELEMETRY_ENABLED"
|
||||
|
||||
|
||||
class TestTelemetryDisabledFlag:
|
||||
def test_default_config_disables(self):
|
||||
# cua_telemetry absent / False => telemetry disabled.
|
||||
with patch("hermes_cli.config.load_config", return_value={}):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
def test_explicit_false_disables(self):
|
||||
with patch("hermes_cli.config.load_config",
|
||||
return_value={"computer_use": {"cua_telemetry": False}}):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
def test_opt_in_true_does_not_disable(self):
|
||||
with patch("hermes_cli.config.load_config",
|
||||
return_value={"computer_use": {"cua_telemetry": True}}):
|
||||
assert cua_backend._cua_telemetry_disabled() is False
|
||||
|
||||
def test_config_load_failure_fails_safe(self):
|
||||
# Unreadable config => default to disabling telemetry (privacy-safe).
|
||||
with patch("hermes_cli.config.load_config", side_effect=RuntimeError("boom")):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
def test_missing_section_disables(self):
|
||||
with patch("hermes_cli.config.load_config", return_value={"other": {}}):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
|
||||
class TestChildEnv:
|
||||
def test_disabled_injects_var_zero(self):
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True):
|
||||
env = cua_backend.cua_driver_child_env({"PATH": "/usr/bin"})
|
||||
assert env[_VAR] == "0"
|
||||
# base env is preserved
|
||||
assert env["PATH"] == "/usr/bin"
|
||||
|
||||
def test_opt_in_leaves_var_untouched(self):
|
||||
# When the user opts in, we must NOT set the var — the driver uses its
|
||||
# own default. If the base env already has a value, it is preserved.
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=False):
|
||||
env = cua_backend.cua_driver_child_env({"PATH": "/usr/bin"})
|
||||
assert _VAR not in env
|
||||
|
||||
def test_opt_in_preserves_user_set_var(self):
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=False):
|
||||
env = cua_backend.cua_driver_child_env({_VAR: "1", "PATH": "/usr/bin"})
|
||||
# user opted in and explicitly set it — don't clobber.
|
||||
assert env[_VAR] == "1"
|
||||
|
||||
def test_disabled_overrides_inherited_enabled(self):
|
||||
# Even if the parent process had telemetry enabled, the default policy
|
||||
# forces it off in the child.
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True):
|
||||
env = cua_backend.cua_driver_child_env({_VAR: "1"})
|
||||
assert env[_VAR] == "0"
|
||||
|
||||
def test_defaults_to_os_environ_when_no_base(self):
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True), \
|
||||
patch.dict("os.environ", {"SOME_MARKER": "yes"}, clear=False):
|
||||
env = cua_backend.cua_driver_child_env()
|
||||
assert env.get("SOME_MARKER") == "yes"
|
||||
assert env[_VAR] == "0"
|
||||
@@ -0,0 +1,325 @@
|
||||
"""Tests for ``tools.computer_use.doctor``.
|
||||
|
||||
The doctor module drives cua-driver's stable ``health_report`` MCP tool over
|
||||
stdio JSON-RPC and renders the structured response. Most of the surface is
|
||||
about parsing what cua-driver hands back, plus the exit-code contract
|
||||
downstream consumers (CI / `hermes update`) rely on:
|
||||
|
||||
* Exit 0 when overall == "ok"
|
||||
* Exit 1 when overall in ("degraded", "failed") — at least one check
|
||||
failed but the tool itself ran successfully
|
||||
* Exit 2 when the cua-driver binary is missing or the protocol breaks
|
||||
|
||||
We do NOT spin up a real cua-driver — that lives in the cua-driver
|
||||
integration test suite (libs/cua-driver/rust/tests/integration/
|
||||
test_health_report_mcp.py). Here we mock the subprocess and assert the
|
||||
Hermes-side adapter behaves correctly against the documented response
|
||||
shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
# ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _fake_proc_with_responses(*responses: dict) -> MagicMock:
|
||||
"""Build a MagicMock subprocess.Popen handle that yields one JSON-RPC
|
||||
response per `readline()` call, then returns "" (EOF)."""
|
||||
lines = [json.dumps(r) + "\n" for r in responses] + [""]
|
||||
proc = MagicMock()
|
||||
proc.stdin = MagicMock()
|
||||
proc.stdout = MagicMock()
|
||||
proc.stdout.readline = MagicMock(side_effect=lines)
|
||||
proc.stderr = MagicMock()
|
||||
proc.stderr.read = MagicMock(return_value="")
|
||||
proc.wait = MagicMock(return_value=0)
|
||||
proc.kill = MagicMock()
|
||||
return proc
|
||||
|
||||
|
||||
def _ok_report() -> dict:
|
||||
"""Minimal well-formed health_report response."""
|
||||
return {
|
||||
"schema_version": "1",
|
||||
"platform": "darwin",
|
||||
"driver_version": "0.5.8",
|
||||
"overall": "ok",
|
||||
"checks": [
|
||||
{"name": "binary_version", "status": "pass", "message": "cua-driver 0.5.8"},
|
||||
{"name": "tcc_accessibility", "status": "pass", "message": "Accessibility is granted."},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _degraded_report() -> dict:
|
||||
"""Report with one failing check — overall=degraded."""
|
||||
return {
|
||||
"schema_version": "1",
|
||||
"platform": "darwin",
|
||||
"driver_version": "0.5.8",
|
||||
"overall": "degraded",
|
||||
"checks": [
|
||||
{"name": "binary_version", "status": "pass", "message": "cua-driver 0.5.8"},
|
||||
{
|
||||
"name": "bundle_identity",
|
||||
"status": "fail",
|
||||
"message": "Process has no CFBundleIdentifier.",
|
||||
"hint": "Run inside CuaDriver.app",
|
||||
"data": {"executable_path": "/tmp/cua-driver"},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ── exit codes ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDoctorExitCodes:
|
||||
def test_ok_exits_0(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
code = doctor.run_doctor()
|
||||
assert code == 0
|
||||
|
||||
def test_degraded_exits_1(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _degraded_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
code = doctor.run_doctor()
|
||||
assert code == 1
|
||||
|
||||
def test_failed_overall_exits_1(self):
|
||||
"""`failed` overall (every check failed) is also exit 1, not 2 —
|
||||
the tool ran successfully; the diagnosis was bad."""
|
||||
from tools.computer_use import doctor
|
||||
|
||||
report = _degraded_report()
|
||||
report["overall"] = "failed"
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": report}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
code = doctor.run_doctor()
|
||||
assert code == 1
|
||||
|
||||
def test_missing_binary_exits_2(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
with patch("shutil.which", return_value=None), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
code = doctor.run_doctor()
|
||||
assert code == 2
|
||||
|
||||
def test_protocol_error_exits_2(self, capsys):
|
||||
"""An empty stdout response (driver crashed during handshake) is a
|
||||
protocol failure → exit 2."""
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = MagicMock()
|
||||
proc.stdin = MagicMock()
|
||||
proc.stdout = MagicMock()
|
||||
proc.stdout.readline = MagicMock(return_value="") # EOF on initialize
|
||||
proc.stderr = MagicMock()
|
||||
proc.stderr.read = MagicMock(return_value="boom\n")
|
||||
proc.wait = MagicMock(return_value=0)
|
||||
proc.kill = MagicMock()
|
||||
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc):
|
||||
code = doctor.run_doctor()
|
||||
assert code == 2
|
||||
# stderr should mention the failure
|
||||
captured = capsys.readouterr()
|
||||
assert "cua-driver" in captured.err.lower() or "health_report" in captured.err.lower()
|
||||
|
||||
|
||||
# ── response-shape parsing ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestResponseShapeParsing:
|
||||
def test_prefers_structuredContent(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO) as out:
|
||||
doctor.run_doctor()
|
||||
# Header line includes driver version + platform + overall.
|
||||
text = out.getvalue()
|
||||
assert "darwin" in text
|
||||
assert "ok" in text
|
||||
|
||||
def test_falls_back_to_text_content_when_structuredContent_absent(self):
|
||||
"""Older cua-driver builds may emit health_report as a text content
|
||||
item carrying the JSON — the doctor should still parse it."""
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{
|
||||
"jsonrpc": "2.0", "id": 2,
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": json.dumps(_ok_report())},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO) as out:
|
||||
code = doctor.run_doctor()
|
||||
assert code == 0
|
||||
assert "ok" in out.getvalue()
|
||||
|
||||
def test_jsonrpc_error_response_exits_2(self, capsys):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "error": {"code": -32601, "message": "method not found"}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc):
|
||||
code = doctor.run_doctor()
|
||||
assert code == 2
|
||||
assert "method not found" in capsys.readouterr().err
|
||||
|
||||
|
||||
# ── args / arg passthrough ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestArgPassthrough:
|
||||
def test_include_passed_through_to_tools_call(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
doctor.run_doctor(include=["binary_version", "tcc_accessibility"])
|
||||
|
||||
# Inspect the second write to stdin — the tools/call payload.
|
||||
writes = [call.args[0] for call in proc.stdin.write.call_args_list]
|
||||
call_payload = next(json.loads(w) for w in writes if "tools/call" in w)
|
||||
assert call_payload["params"]["arguments"]["include"] == [
|
||||
"binary_version", "tcc_accessibility",
|
||||
]
|
||||
|
||||
def test_skip_passed_through(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
doctor.run_doctor(skip=["bundle_identity"])
|
||||
writes = [call.args[0] for call in proc.stdin.write.call_args_list]
|
||||
call_payload = next(json.loads(w) for w in writes if "tools/call" in w)
|
||||
assert call_payload["params"]["arguments"]["skip"] == ["bundle_identity"]
|
||||
|
||||
def test_no_filters_sends_empty_arguments(self):
|
||||
"""When neither include nor skip is given, the arguments object is
|
||||
empty — not present-but-null — so the driver's default 'run every
|
||||
check' branch fires."""
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
doctor.run_doctor()
|
||||
writes = [call.args[0] for call in proc.stdin.write.call_args_list]
|
||||
call_payload = next(json.loads(w) for w in writes if "tools/call" in w)
|
||||
assert call_payload["params"]["arguments"] == {}
|
||||
|
||||
|
||||
# ── json output ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestJsonOutput:
|
||||
def test_json_output_is_parseable_round_trip(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/cua-driver"), \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO) as out:
|
||||
doctor.run_doctor(json_output=True)
|
||||
# Verify the captured text round-trips through json.loads and matches
|
||||
# the input report (the contract: --json passes the structured payload
|
||||
# through unchanged so downstream tooling can consume it directly).
|
||||
parsed = json.loads(out.getvalue())
|
||||
assert parsed == _ok_report()
|
||||
|
||||
|
||||
# ── HERMES_CUA_DRIVER_CMD resolution ───────────────────────────────────────
|
||||
|
||||
|
||||
class TestDriverCmdResolution:
|
||||
def test_explicit_driver_cmd_arg_wins(self):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/fake/explicit-binary") as which_mock, \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
doctor.run_doctor(driver_cmd="/custom/path/cua-driver")
|
||||
# shutil.which should have been called with the explicit arg, not
|
||||
# the env-var / default resolver.
|
||||
which_mock.assert_called_with("/custom/path/cua-driver")
|
||||
|
||||
def test_env_var_used_when_no_arg_given(self, monkeypatch):
|
||||
from tools.computer_use import doctor
|
||||
|
||||
monkeypatch.setenv("HERMES_CUA_DRIVER_CMD", "/env/path/cua-driver")
|
||||
proc = _fake_proc_with_responses(
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {}},
|
||||
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
|
||||
)
|
||||
with patch("shutil.which", return_value="/env/path/cua-driver") as which_mock, \
|
||||
patch("subprocess.Popen", return_value=proc), \
|
||||
patch("sys.stdout", new_callable=StringIO):
|
||||
doctor.run_doctor()
|
||||
# First (and only) which call should have used the env var.
|
||||
which_mock.assert_called_with("/env/path/cua-driver")
|
||||
Reference in New Issue
Block a user