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