Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 67 additions & 7 deletions tests/tools/test_browser_use_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,23 +299,29 @@ def _env(self):

def test_existing_bu_env_wins(self, monkeypatch):
env = {"BU_CDP_WS": "ws://operator-override:9222"}
assert bu_cli._resolve_backend_cdp(env, "t1") is None
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(env, "t1")
assert err is None
assert cloud_session_task_id is None
assert env["BU_CDP_WS"] == "ws://operator-override:9222"

def test_cdp_override_exported(self, monkeypatch):
import tools.browser_tool as bt

monkeypatch.setattr(bt, "_get_cdp_override", lambda: "http://127.0.0.1:9222")
env = self._env()
assert bu_cli._resolve_backend_cdp(env, "t1") is None
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(env, "t1")
assert err is None
assert cloud_session_task_id is None
assert env["BU_CDP_URL"] == "http://127.0.0.1:9222"

def test_ws_override_uses_bu_cdp_ws(self, monkeypatch):
import tools.browser_tool as bt

monkeypatch.setattr(bt, "_get_cdp_override", lambda: "wss://connect.example/x")
env = self._env()
assert bu_cli._resolve_backend_cdp(env, "t1") is None
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(env, "t1")
assert err is None
assert cloud_session_task_id is None
assert env["BU_CDP_WS"] == "wss://connect.example/x"

def test_cloud_provider_session_exported(self, monkeypatch):
Expand All @@ -328,7 +334,13 @@ def test_cloud_provider_session_exported(self, monkeypatch):
lambda task_id: {"cdp_url": "wss://browser.example/cdp/abc"},
)
env = self._env()
assert bu_cli._resolve_backend_cdp(env, "t1") is None
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(env, "t1")
assert err is None
# M1 regression (browser_exec activity heartbeat): a resolved
# cloud-provider session must report the key it registered activity
# for, so the caller can keep it fresh for the duration of a
# long-running exec instead of only at resolution time.
assert cloud_session_task_id == "t1"
assert env["BU_CDP_WS"] == "wss://browser.example/cdp/abc"

def test_no_provider_leaves_env_untouched(self, monkeypatch):
Expand All @@ -337,7 +349,9 @@ def test_no_provider_leaves_env_untouched(self, monkeypatch):
monkeypatch.setattr(bt, "_get_cdp_override", lambda: "")
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: None)
env = self._env()
assert bu_cli._resolve_backend_cdp(env, "t1") is None
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(env, "t1")
assert err is None
assert cloud_session_task_id is None
assert "BU_CDP_WS" not in env and "BU_CDP_URL" not in env

def test_provider_failure_returns_error(self, monkeypatch):
Expand All @@ -349,17 +363,19 @@ def boom(task_id):
monkeypatch.setattr(bt, "_get_cdp_override", lambda: "")
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: object())
monkeypatch.setattr(bt, "_get_session_info", boom)
err = bu_cli._resolve_backend_cdp(self._env(), "t1")
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(self._env(), "t1")
assert err and "api down" in err
assert cloud_session_task_id is None

def test_provider_without_cdp_returns_error(self, monkeypatch):
import tools.browser_tool as bt

monkeypatch.setattr(bt, "_get_cdp_override", lambda: "")
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: object())
monkeypatch.setattr(bt, "_get_session_info", lambda task_id: {"cdp_url": None})
err = bu_cli._resolve_backend_cdp(self._env(), "t1")
err, cloud_session_task_id = bu_cli._resolve_backend_cdp(self._env(), "t1")
assert err and "no" in err.lower() and "CDP" in err
assert cloud_session_task_id is None

def test_named_session_skips_backend_resolution(self, tmp_path, monkeypatch):
"""session=<name> (BU_NAME cloud browser) must not consume a backend
Expand Down Expand Up @@ -692,6 +708,50 @@ def test_timeout_returns_actionable_error(self, tmp_path, monkeypatch):
result = json.loads(bu_cli.browser_exec("print(1)", timeout_s=1))
assert "timed out" in result["error"]

def test_long_exec_keeps_cloud_session_activity_fresh(self, tmp_path, monkeypatch):
"""A long-running browser_exec (subprocess.run's blocking wait made
zero further calls into the session tracker) must not let the
inactivity reaper's clock run out from under the still-working CLI.

Regression: browser_exec's subprocess wait only refreshed the
resolved cloud-provider session's activity timestamp once, at
_resolve_backend_cdp()'s _get_session_info() call — never again for
the whole (up to 1800s) run. This drives a fake CLI that sleeps
across several heartbeat intervals and asserts
_update_session_activity is called repeatedly, not just once.
"""
import tools.browser_tool as bt

monkeypatch.setattr(bt, "_get_cdp_override", lambda: "")
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: object())
monkeypatch.setattr(
bt, "_get_session_info",
lambda task_id: {"cdp_url": "wss://browser.example/cdp/abc"},
)
heartbeat_calls = []
real_update = bt._update_session_activity

def spy_update(task_id):
heartbeat_calls.append(task_id)
return real_update(task_id)

monkeypatch.setattr(bt, "_update_session_activity", spy_update)
# Tiny heartbeat interval so the sleeping fake CLI spans several
# heartbeats without a slow test.
monkeypatch.setattr(bu_cli, "_CLOUD_SESSION_HEARTBEAT_INTERVAL_S", 0.05)

cli = _fake_cli(tmp_path, "cat > /dev/null\nsleep 0.3\necho done\n")
monkeypatch.setattr(bu_cli, "_find_cli", lambda: [cli])

result = json.loads(bu_cli.browser_exec("print(1)", task_id="t42"))

assert result["success"] is True
assert "done" in result["output"]
assert len(heartbeat_calls) >= 2, (
f"expected repeated heartbeats during the run, got {heartbeat_calls!r}"
)
assert all(tid == "t42" for tid in heartbeat_calls)


class TestFindCliManagedBin:
"""_find_cli probes $HERMES_HOME/bin after PATH (managed uv/uvx/browser-use)."""
Expand Down
Loading
Loading