/ tests / tools / test_browser_cleanup.py
test_browser_cleanup.py
  1  """Regression tests for browser session cleanup and screenshot recovery."""
  2  
  3  from unittest.mock import patch
  4  
  5  
  6  class TestScreenshotPathRecovery:
  7      def test_extracts_standard_absolute_path(self):
  8          from tools.browser_tool import _extract_screenshot_path_from_text
  9  
 10          assert (
 11              _extract_screenshot_path_from_text("Screenshot saved to /tmp/foo.png")
 12              == "/tmp/foo.png"
 13          )
 14  
 15      def test_extracts_quoted_absolute_path(self):
 16          from tools.browser_tool import _extract_screenshot_path_from_text
 17  
 18          assert (
 19              _extract_screenshot_path_from_text(
 20                  "Screenshot saved to '/Users/david/.hermes/browser_screenshots/shot.png'"
 21              )
 22              == "/Users/david/.hermes/browser_screenshots/shot.png"
 23          )
 24  
 25  
 26  class TestBrowserCleanup:
 27      def setup_method(self):
 28          from tools import browser_tool
 29  
 30          self.browser_tool = browser_tool
 31          self.orig_active_sessions = browser_tool._active_sessions.copy()
 32          self.orig_session_last_activity = browser_tool._session_last_activity.copy()
 33          self.orig_recording_sessions = browser_tool._recording_sessions.copy()
 34          self.orig_cleanup_done = browser_tool._cleanup_done
 35  
 36      def teardown_method(self):
 37          self.browser_tool._active_sessions.clear()
 38          self.browser_tool._active_sessions.update(self.orig_active_sessions)
 39          self.browser_tool._session_last_activity.clear()
 40          self.browser_tool._session_last_activity.update(self.orig_session_last_activity)
 41          self.browser_tool._recording_sessions.clear()
 42          self.browser_tool._recording_sessions.update(self.orig_recording_sessions)
 43          self.browser_tool._cleanup_done = self.orig_cleanup_done
 44  
 45      def test_cleanup_browser_clears_tracking_state(self):
 46          browser_tool = self.browser_tool
 47          browser_tool._active_sessions["task-1"] = {
 48              "session_name": "sess-1",
 49              "bb_session_id": None,
 50          }
 51          browser_tool._session_last_activity["task-1"] = 123.0
 52  
 53          with (
 54              patch("tools.browser_tool._maybe_stop_recording") as mock_stop,
 55              patch(
 56                  "tools.browser_tool._run_browser_command",
 57                  return_value={"success": True},
 58              ) as mock_run,
 59              patch("tools.browser_tool.os.path.exists", return_value=False),
 60          ):
 61              browser_tool.cleanup_browser("task-1")
 62  
 63          assert "task-1" not in browser_tool._active_sessions
 64          assert "task-1" not in browser_tool._session_last_activity
 65          mock_stop.assert_called_once_with("task-1")
 66          mock_run.assert_called_once_with("task-1", "close", [], timeout=10)
 67  
 68      def test_cleanup_camofox_managed_persistence_skips_close(self):
 69          """When camofox mode + managed persistence, soft_cleanup fires instead of close."""
 70          browser_tool = self.browser_tool
 71          browser_tool._active_sessions["task-1"] = {
 72              "session_name": "sess-1",
 73              "bb_session_id": None,
 74          }
 75          browser_tool._session_last_activity["task-1"] = 123.0
 76  
 77          with (
 78              patch("tools.browser_tool._is_camofox_mode", return_value=True),
 79              patch("tools.browser_tool._maybe_stop_recording") as mock_stop,
 80              patch(
 81                  "tools.browser_tool._run_browser_command",
 82                  return_value={"success": True},
 83              ),
 84              patch("tools.browser_tool.os.path.exists", return_value=False),
 85              patch(
 86                  "tools.browser_camofox.camofox_soft_cleanup",
 87                  return_value=True,
 88              ) as mock_soft,
 89              patch("tools.browser_camofox.camofox_close") as mock_close,
 90          ):
 91              browser_tool.cleanup_browser("task-1")
 92  
 93          mock_soft.assert_called_once_with("task-1")
 94          mock_close.assert_not_called()
 95  
 96      def test_cleanup_camofox_no_persistence_calls_close(self):
 97          """When camofox mode but managed persistence is off, camofox_close fires."""
 98          browser_tool = self.browser_tool
 99          browser_tool._active_sessions["task-1"] = {
100              "session_name": "sess-1",
101              "bb_session_id": None,
102          }
103          browser_tool._session_last_activity["task-1"] = 123.0
104  
105          with (
106              patch("tools.browser_tool._is_camofox_mode", return_value=True),
107              patch("tools.browser_tool._maybe_stop_recording") as mock_stop,
108              patch(
109                  "tools.browser_tool._run_browser_command",
110                  return_value={"success": True},
111              ),
112              patch("tools.browser_tool.os.path.exists", return_value=False),
113              patch(
114                  "tools.browser_camofox.camofox_soft_cleanup",
115                  return_value=False,
116              ) as mock_soft,
117              patch("tools.browser_camofox.camofox_close") as mock_close,
118          ):
119              browser_tool.cleanup_browser("task-1")
120  
121          mock_soft.assert_called_once_with("task-1")
122          mock_close.assert_called_once_with("task-1")
123  
124      def test_emergency_cleanup_clears_all_tracking_state(self):
125          browser_tool = self.browser_tool
126          browser_tool._cleanup_done = False
127          browser_tool._active_sessions["task-1"] = {"session_name": "sess-1"}
128          browser_tool._active_sessions["task-2"] = {"session_name": "sess-2"}
129          browser_tool._session_last_activity["task-1"] = 1.0
130          browser_tool._session_last_activity["task-2"] = 2.0
131          browser_tool._recording_sessions.update({"task-1", "task-2"})
132  
133          with patch("tools.browser_tool.cleanup_all_browsers") as mock_cleanup_all:
134              browser_tool._emergency_cleanup_all_sessions()
135  
136          mock_cleanup_all.assert_called_once_with()
137          assert browser_tool._active_sessions == {}
138          assert browser_tool._session_last_activity == {}
139          assert browser_tool._recording_sessions == set()
140          assert browser_tool._cleanup_done is True