Files
openswarm/backend/tests/test_cases/browser/test_browser_command_timeout.py
T
haikdcandGitHub 9c95342704 Haik/feat/test runner (#86)
* [haik]: ckpt, added in the test runner skelton i made in another repo -> still gotta make tweaks to fold it onto the current repo

* [haik]: restructure backend test layout: move 37 test files from backend/tests/ into backend/tests/test_cases/, add backend/tests/run.sh one-command launcher that auto-provisions both runner and test venvs with stamp-based caching, update runner config.json to point test_paths at test_cases/ and fix venv_python path, revise runner README with run.sh usage and clearer setup instructions, and gitignore the .runner-venv directory

* [haik]: refactor: reorganize backend/tests/test_cases from flat structure into domain subdirectories — moved 36 test files into auth/, browser/, labeling/, service/, settings/, and web_search/ for better discoverability and grouping

* [haik]: overhaul test picker UI and add post-run rerun loop: replace checkbox glyphs with fzf-style row recolouring (coral=full, lighter coral=partial) and a right-pinned selection dot, add config-driven icon tiers (nerd/emoji/unicode/ascii) with per-glyph graceful degradation, replace the modal -k keyword screen with an inline live-filtering search bar that prunes the tree on every keystroke, add warm Anthropic-dark coral theme, toolbar flag chips replacing the old status line, and a floating help badge overlay; add rerun_prompt.py with inline Textual pill prompt (rerun all/failed/passed/exit) shown after each TTY run, wire it into main.py as a post-run loop; change run_tests to return (exit_code, RunSummary) tracking collected/passed/failed node IDs via new Dashboard methods; add icons field to config.py and config.json (set to nerd), document icon tiers and Nerd Font setup in README, set Hack Nerd Font in .vscode/settings.json, add .runner-venv to linter excludes
2026-06-14 07:02:30 -07:00

101 lines
4.0 KiB
Python

"""Per-action browser-command timeouts.
A hung tab makes every command block to its timeout; a flat 30s let one wedged
page spin ~20 minutes across retries. These pin that the bound is now short and
per-action, so a freeze surfaces in seconds.
"""
import asyncio
import time
import pytest
from backend.apps.agents.core import ws_manager as wsm
class p_FakeSock:
async def send_text(self, _):
return None
def p_mgr():
m = wsm.ConnectionManager()
m.global_connections = [p_FakeSock()] # get past the 'no dashboard' guard
return m
def test_timeout_map_reads_are_short_navigation_longer():
# reads/clicks act on a loaded page -> short; navigation loads network -> longer
assert wsm.P_BROWSER_CMD_TIMEOUT_DEFAULT <= 15 # p-private-ignore: P_BROWSER_CMD_TIMEOUT_DEFAULT
assert wsm.P_BROWSER_CMD_TIMEOUTS["navigate"] <= 25 # p-private-ignore: P_BROWSER_CMD_TIMEOUTS
assert wsm.P_BROWSER_CMD_TIMEOUTS["navigate"] > wsm.P_BROWSER_CMD_TIMEOUT_DEFAULT # p-private-ignore: P_BROWSER_CMD_TIMEOUTS, P_BROWSER_CMD_TIMEOUT_DEFAULT
# the old flat 30s is gone for the common path
assert wsm.P_BROWSER_CMD_TIMEOUT_DEFAULT < 30 # p-private-ignore: P_BROWSER_CMD_TIMEOUT_DEFAULT
@pytest.mark.asyncio
async def test_hung_command_returns_fast_at_the_bound(monkeypatch):
# shrink the bounds so the test is quick, then never resolve the future:
# the command must return a timeout error at ~the (default) bound, not hang.
monkeypatch.setattr(wsm, "P_BROWSER_CMD_TIMEOUT_DEFAULT", 0.3)
monkeypatch.setattr(wsm, "P_BROWSER_CMD_TIMEOUTS", {"navigate": 0.6})
m = p_mgr()
t0 = time.monotonic()
res = await m.send_browser_command("rid1", "get_text", "b1", {}) # never resolved
elapsed = time.monotonic() - t0
assert res == {"error": "Browser command timed out"}
assert 0.25 < elapsed < 1.0, f"a read should time out near its 0.3s bound, took {elapsed:.2f}s"
@pytest.mark.asyncio
async def test_navigate_gets_the_longer_leash(monkeypatch):
monkeypatch.setattr(wsm, "P_BROWSER_CMD_TIMEOUT_DEFAULT", 0.3)
monkeypatch.setattr(wsm, "P_BROWSER_CMD_TIMEOUTS", {"navigate": 0.7})
m = p_mgr()
t0 = time.monotonic()
await m.send_browser_command("rid2", "navigate", "b1", {"url": "x"})
elapsed = time.monotonic() - t0
assert elapsed > 0.5, "navigate should use its longer bound, not the default"
@pytest.mark.asyncio
async def test_lost_first_delivery_heals_via_rebroadcast(monkeypatch):
# a silently-dead socket eats the first broadcast; the re-send after the
# rebroadcast interval must reach the (reconnected) client and succeed
monkeypatch.setattr(wsm, "P_BROWSER_CMD_TIMEOUT_DEFAULT", 5.0)
monkeypatch.setattr(wsm, "P_BROWSER_CMD_REBROADCAST_S", 0.1)
m = p_mgr()
sends = []
class p_CountingSock:
async def send_text(self, payload):
sends.append(payload)
if len(sends) >= 2: # first delivery "lost", second lands
rid = next(iter(m.browser_futures))
m.resolve_browser_command(rid, {"text": "ok"})
m.global_connections = [p_CountingSock()]
res = await m.send_browser_command("rid4", "get_text", "b1", {})
assert res == {"text": "ok"}
assert len(sends) >= 2, "command must be re-broadcast until a client answers"
@pytest.mark.asyncio
async def test_a_resolved_command_returns_immediately(monkeypatch):
# a healthy command returns the moment the renderer resolves it, not at the bound
monkeypatch.setattr(wsm, "P_BROWSER_CMD_TIMEOUT_DEFAULT", 5.0)
m = p_mgr()
async def p_resolve_soon():
await asyncio.sleep(0.05)
# find the pending future and resolve it like the renderer would
rid = next(iter(m.browser_futures))
m.resolve_browser_command(rid, {"text": "ok", "url": "u"})
asyncio.create_task(p_resolve_soon())
t0 = time.monotonic()
res = await m.send_browser_command("rid3", "get_text", "b1", {})
elapsed = time.monotonic() - t0
assert res == {"text": "ok", "url": "u"}
assert elapsed < 1.0, "healthy command returns on resolve, not at the timeout"