Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""Regression tests for browser session cleanup and screenshot recovery."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestScreenshotPathRecovery:
|
||||
def test_extracts_standard_absolute_path(self):
|
||||
from tools.browser_tool import _extract_screenshot_path_from_text
|
||||
|
||||
assert (
|
||||
_extract_screenshot_path_from_text("Screenshot saved to /tmp/foo.png")
|
||||
== "/tmp/foo.png"
|
||||
)
|
||||
|
||||
def test_extracts_quoted_absolute_path(self):
|
||||
from tools.browser_tool import _extract_screenshot_path_from_text
|
||||
|
||||
assert (
|
||||
_extract_screenshot_path_from_text(
|
||||
"Screenshot saved to '/Users/david/.hermes/browser_screenshots/shot.png'"
|
||||
)
|
||||
== "/Users/david/.hermes/browser_screenshots/shot.png"
|
||||
)
|
||||
|
||||
|
||||
class TestBrowserCleanup:
|
||||
def setup_method(self):
|
||||
from tools import browser_tool
|
||||
|
||||
self.browser_tool = browser_tool
|
||||
self.orig_active_sessions = browser_tool._active_sessions.copy()
|
||||
self.orig_session_last_activity = browser_tool._session_last_activity.copy()
|
||||
self.orig_recording_sessions = browser_tool._recording_sessions.copy()
|
||||
self.orig_cleanup_done = browser_tool._cleanup_done
|
||||
|
||||
def teardown_method(self):
|
||||
self.browser_tool._active_sessions.clear()
|
||||
self.browser_tool._active_sessions.update(self.orig_active_sessions)
|
||||
self.browser_tool._session_last_activity.clear()
|
||||
self.browser_tool._session_last_activity.update(self.orig_session_last_activity)
|
||||
self.browser_tool._recording_sessions.clear()
|
||||
self.browser_tool._recording_sessions.update(self.orig_recording_sessions)
|
||||
self.browser_tool._cleanup_done = self.orig_cleanup_done
|
||||
|
||||
def test_cleanup_browser_clears_tracking_state(self):
|
||||
browser_tool = self.browser_tool
|
||||
browser_tool._active_sessions["task-1"] = {
|
||||
"session_name": "sess-1",
|
||||
"bb_session_id": None,
|
||||
}
|
||||
browser_tool._session_last_activity["task-1"] = 123.0
|
||||
|
||||
with (
|
||||
patch("tools.browser_tool._maybe_stop_recording") as mock_stop,
|
||||
patch(
|
||||
"tools.browser_tool._run_browser_command",
|
||||
return_value={"success": True},
|
||||
) as mock_run,
|
||||
patch("tools.browser_tool.os.path.exists", return_value=False),
|
||||
):
|
||||
browser_tool.cleanup_browser("task-1")
|
||||
|
||||
assert "task-1" not in browser_tool._active_sessions
|
||||
assert "task-1" not in browser_tool._session_last_activity
|
||||
mock_stop.assert_called_once_with("task-1")
|
||||
mock_run.assert_called_once_with("task-1", "close", [], timeout=10)
|
||||
|
||||
|
||||
def test_emergency_cleanup_clears_all_tracking_state(self):
|
||||
browser_tool = self.browser_tool
|
||||
browser_tool._cleanup_done = False
|
||||
browser_tool._active_sessions["task-1"] = {"session_name": "sess-1"}
|
||||
browser_tool._active_sessions["task-2"] = {"session_name": "sess-2"}
|
||||
browser_tool._session_last_activity["task-1"] = 1.0
|
||||
browser_tool._session_last_activity["task-2"] = 2.0
|
||||
browser_tool._recording_sessions.update({"task-1", "task-2"})
|
||||
|
||||
with patch("tools.browser_tool.cleanup_all_browsers") as mock_cleanup_all:
|
||||
browser_tool._emergency_cleanup_all_sessions()
|
||||
|
||||
mock_cleanup_all.assert_called_once_with()
|
||||
assert browser_tool._active_sessions == {}
|
||||
assert browser_tool._session_last_activity == {}
|
||||
assert browser_tool._recording_sessions == set()
|
||||
assert browser_tool._cleanup_done is True
|
||||
|
||||
|
||||
class TestInactivityJanitorMultiplex:
|
||||
"""#86402 / #100738: the process-global janitor thread has no profile scope."""
|
||||
|
||||
def setup_method(self):
|
||||
from agent import secret_scope
|
||||
from tools import browser_tool
|
||||
|
||||
self.bt = browser_tool
|
||||
self.saved = {
|
||||
name: getattr(browser_tool, name).copy()
|
||||
for name in (
|
||||
"_active_sessions", "_session_last_activity",
|
||||
"_session_owner_homes", "_cleanup_failures", "_recording_sessions",
|
||||
)
|
||||
}
|
||||
self.orig_timeout = browser_tool.BROWSER_SESSION_INACTIVITY_TIMEOUT
|
||||
browser_tool.BROWSER_SESSION_INACTIVITY_TIMEOUT = 0
|
||||
for name in self.saved:
|
||||
getattr(browser_tool, name).clear()
|
||||
secret_scope.set_multiplex_active(True)
|
||||
|
||||
def teardown_method(self):
|
||||
from agent import secret_scope
|
||||
|
||||
secret_scope.set_multiplex_active(False)
|
||||
self.bt.BROWSER_SESSION_INACTIVITY_TIMEOUT = self.orig_timeout
|
||||
for name, saved in self.saved.items():
|
||||
live = getattr(self.bt, name)
|
||||
live.clear()
|
||||
live.update(saved)
|
||||
|
||||
def test_janitor_tears_down_under_owner_profile_scope(self, tmp_path, monkeypatch):
|
||||
from agent import secret_scope
|
||||
from hermes_constants import (
|
||||
get_hermes_home, reset_hermes_home_override, set_hermes_home_override,
|
||||
)
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.delenv("CAMOFOX_URL", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CDP_URL", raising=False)
|
||||
p1 = tmp_path / "profiles" / "p1"
|
||||
p1.mkdir(parents=True)
|
||||
(p1 / ".env").write_text("CAMOFOX_URL=http://127.0.0.1:1\n")
|
||||
|
||||
# Profile p1's turn opens the session; the janitor later runs unscoped.
|
||||
home_tok = set_hermes_home_override(str(p1))
|
||||
scope_tok = secret_scope.set_secret_scope(secret_scope.build_profile_secret_scope(p1))
|
||||
try:
|
||||
self.bt._update_session_activity("t1")
|
||||
self.bt._active_sessions["t1"] = {"session_name": "s1", "bb_session_id": None}
|
||||
finally:
|
||||
secret_scope.reset_secret_scope(scope_tok)
|
||||
reset_hermes_home_override(home_tok)
|
||||
self.bt._session_last_activity["t1"] -= 10
|
||||
|
||||
seen = {}
|
||||
|
||||
def fake_close(task_id, cmd, args, timeout=None):
|
||||
seen["home"] = str(get_hermes_home())
|
||||
seen["url"] = secret_scope.get_secret("CAMOFOX_URL")
|
||||
return {"success": True}
|
||||
|
||||
with (
|
||||
patch("tools.browser_tool._run_browser_command", side_effect=fake_close),
|
||||
patch("tools.browser_camofox._delete", return_value={}),
|
||||
patch("tools.browser_tool.os.path.exists", return_value=False),
|
||||
):
|
||||
self.bt._cleanup_inactive_browser_sessions()
|
||||
|
||||
assert seen == {"home": str(p1), "url": "http://127.0.0.1:1"}
|
||||
assert "t1" not in self.bt._session_last_activity
|
||||
assert "t1" not in self.bt._active_sessions
|
||||
assert "t1" not in self.bt._session_owner_homes
|
||||
|
||||
def test_repeated_failures_force_reap_and_close_cloud_session(self):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
self.bt._active_sessions["t1"] = {"session_name": "s1", "bb_session_id": "bb-1"}
|
||||
self.bt._session_last_activity["t1"] = 1.0
|
||||
provider = MagicMock()
|
||||
|
||||
with (
|
||||
patch("tools.browser_tool.cleanup_browser", side_effect=RuntimeError("boom")),
|
||||
patch("tools.browser_tool._get_cloud_provider", return_value=provider),
|
||||
patch("tools.browser_tool.os.path.exists", return_value=False),
|
||||
):
|
||||
for _ in range(self.bt.MAX_INACTIVITY_CLEANUP_FAILURES - 1):
|
||||
self.bt._cleanup_inactive_browser_sessions()
|
||||
# An activity touch must NOT reset the failure budget.
|
||||
self.bt._update_session_activity("t1")
|
||||
self.bt._session_last_activity["t1"] = 1.0
|
||||
assert self.bt._cleanup_failures["t1"] == self.bt.MAX_INACTIVITY_CLEANUP_FAILURES - 1
|
||||
assert "t1" in self.bt._active_sessions
|
||||
provider.close_session.assert_not_called()
|
||||
|
||||
self.bt._cleanup_inactive_browser_sessions()
|
||||
|
||||
provider.close_session.assert_called_once_with("bb-1")
|
||||
assert "t1" not in self.bt._active_sessions
|
||||
assert "t1" not in self.bt._session_last_activity
|
||||
assert "t1" not in self.bt._cleanup_failures
|
||||
Reference in New Issue
Block a user