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
38 changes: 32 additions & 6 deletions src/browser_harness/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,37 @@ def _load_env_file(p):
NAME = os.environ.get("BU_NAME", "default")
SOCK = ipc.sock_addr(NAME)
INTERNAL = ("chrome://", "chrome-untrusted://", "devtools://", "chrome-extension://", "about:")
IPC_CONNECT_TIMEOUT_SECONDS = 5.0
DEFAULT_IPC_RESPONSE_TIMEOUT_SECONDS = 5.0
# Cloud screenshots routinely take longer than ordinary CDP round trips. Keep
# their IPC socket alive within the caller's existing 90-second process budget.
SCREENSHOT_IPC_RESPONSE_TIMEOUT_SECONDS = 60.0


def _send(req):
c, token = ipc.connect(NAME, timeout=5.0)
class _IPCResponseTimeout(TimeoutError):
pass


def _send(req, response_timeout=DEFAULT_IPC_RESPONSE_TIMEOUT_SECONDS):
c, token = ipc.connect(NAME, timeout=IPC_CONNECT_TIMEOUT_SECONDS)
try:
r = ipc.request(c, token, req)
c.settimeout(response_timeout)
try:
r = ipc.request(c, token, req)
except TimeoutError as e:
raise _IPCResponseTimeout from e
finally:
c.close()
if "error" in r: raise RuntimeError(r["error"])
return r


def cdp(method, session_id=None, **params):
def cdp(method, session_id=None, _response_timeout=DEFAULT_IPC_RESPONSE_TIMEOUT_SECONDS, **params):
"""Raw CDP. cdp('Page.navigate', url='...'), cdp('DOM.getDocument', depth=-1)."""
return _send({"method": method, "params": params, "session_id": session_id}).get("result", {})
return _send(
{"method": method, "params": params, "session_id": session_id},
response_timeout=_response_timeout,
).get("result", {})


def drain_events(): return _send({"meta": "drain_events"})["events"]
Expand Down Expand Up @@ -243,7 +259,17 @@ def capture_screenshot(path=None, full=False, max_dim=None):
"""Save a PNG of the current viewport. Set max_dim=1800 on a 2× display to
keep the file under the 2000px-per-side limit some image-aware LLMs enforce."""
path = path or str(ipc._TMP / "shot.png")
r = cdp("Page.captureScreenshot", format="png", captureBeyondViewport=full)
try:
r = cdp(
"Page.captureScreenshot",
_response_timeout=SCREENSHOT_IPC_RESPONSE_TIMEOUT_SECONDS,
format="png",
captureBeyondViewport=full,
)
except _IPCResponseTimeout as e:
raise RuntimeError(
f"Page.captureScreenshot timed out after {SCREENSHOT_IPC_RESPONSE_TIMEOUT_SECONDS:g}s"
) from e
open(path, "wb").write(base64.b64decode(r["data"]))
if max_dim:
from PIL import Image
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,50 @@ def test_max_dim_default_is_no_resize(fake_png):
assert _run(fake_png, 4592, 2286) == (4592, 2286)


def test_send_keeps_connect_timeout_short_and_sets_response_budget():
class FakeSocket:
def __init__(self):
self.timeouts = []

def settimeout(self, value):
self.timeouts.append(value)

def close(self):
pass

socket = FakeSocket()
with patch("browser_harness.helpers.ipc.connect", return_value=(socket, None)) as connect, \
patch("browser_harness.helpers.ipc.request", return_value={}):
helpers._send({"meta": "ping"}, response_timeout=60.0)

connect.assert_called_once_with(helpers.NAME, timeout=helpers.IPC_CONNECT_TIMEOUT_SECONDS)
assert socket.timeouts == [60.0]


def test_screenshot_uses_long_response_timeout_without_forwarding_it_to_cdp(fake_png, tmp_path):
with patch(
"browser_harness.helpers._send",
return_value={"result": {"data": fake_png(800, 400)}},
) as send:
helpers.capture_screenshot(str(tmp_path / "shot.png"))

request = send.call_args.args[0]
assert request == {
"method": "Page.captureScreenshot",
"params": {"format": "png", "captureBeyondViewport": False},
"session_id": None,
}
assert send.call_args.kwargs == {
"response_timeout": helpers.SCREENSHOT_IPC_RESPONSE_TIMEOUT_SECONDS
}


def test_screenshot_timeout_has_context(tmp_path):
with patch("browser_harness.helpers._send", side_effect=helpers._IPCResponseTimeout):
with pytest.raises(RuntimeError, match="Page.captureScreenshot timed out after 60s"):
helpers.capture_screenshot(str(tmp_path / "shot.png"))


def _seed_skill(tmp_path):
site = tmp_path / "domain-skills" / "example"
site.mkdir(parents=True)
Expand Down