"""Test CrashWatchdog functionality.""" import asyncio from typing import cast import pytest from browser_use.browser.events import ( BrowserConnectedEvent, BrowserErrorEvent, BrowserStartEvent, BrowserStopEvent, BrowserStoppedEvent, NavigateToUrlEvent, ) from browser_use.browser.profile import BrowserProfile from browser_use.browser.session import BrowserSession from browser_use.utils import logger @pytest.mark.asyncio @pytest.mark.skip('CrashWatchdog not implemented in current CDP architecture') async def test_crash_watchdog_network_timeout(): """Test that CrashWatchdog detects network timeouts by monitoring actual network requests.""" # Create browser session profile = BrowserProfile(headless=True) session = BrowserSession(browser_profile=profile) try: # Start browser using event system session.event_bus.dispatch(BrowserStartEvent()) await session.event_bus.expect(BrowserConnectedEvent, timeout=10.0) logger.info('[TEST] Browser started, configuring watchdog timeout') # Configure crash watchdog with very short timeout for testing if hasattr(session, '_crash_watchdog') and session._crash_watchdog: session._crash_watchdog.network_timeout_seconds = 1.0 # Very short timeout session._crash_watchdog.check_interval_seconds = 0.2 # Check frequently # Try to navigate to a non-existent slow server that will hang # This will create a real network request that will timeout slow_url = 'http://192.0.2.1:4242/timeout-test' # RFC5737 TEST-NET-1 - non-routable logger.info(f'[TEST] Navigating to non-routable URL via events: {slow_url}') session.event_bus.dispatch(NavigateToUrlEvent(url=slow_url)) # Wait for the network timeout error via event bus logger.info('[TEST] Waiting for NetworkTimeout event via event bus...') try: timeout_error = cast( BrowserErrorEvent, await session.event_bus.expect( BrowserErrorEvent, predicate=lambda e: cast(BrowserErrorEvent, e).error_type == 'NetworkTimeout', timeout=8.0 ), ) # Verify the timeout event details assert 'timeout-test' in timeout_error.details['url'] assert timeout_error.details['elapsed_seconds'] >= 0.8 # Should be at least close to our timeout assert timeout_error.message.startswith('Network request timed out after') logger.info(f'[TEST] Successfully detected network timeout: {timeout_error.message}') except TimeoutError: # Network timeout detection can be flaky in test environment logger.warning('[TEST] NetworkTimeout event not received - this is expected in some test environments') # Verify the crash watchdog is running and configured correctly assert session._crash_watchdog is not None, 'CrashWatchdog should exist' assert session._crash_watchdog.network_timeout_seconds == 1.0, 'Network timeout should be configured' assert session._crash_watchdog._monitoring_task is not None, 'Monitoring task should be running' assert not session._crash_watchdog._monitoring_task.done(), 'Monitoring task should still be active' logger.info('[TEST] Crash watchdog is properly configured and running - test passes') finally: # Clean shutdown try: session.event_bus.dispatch(BrowserStopEvent()) await session.event_bus.expect(BrowserStoppedEvent, timeout=3.0) except Exception: # If graceful shutdown fails, force cleanup await session.kill() @pytest.mark.asyncio @pytest.mark.skip('CrashWatchdog not implemented in current CDP architecture') async def test_crash_watchdog_browser_disconnect(): """Test that CrashWatchdog detects browser disconnection through monitoring.""" profile = BrowserProfile(headless=True) session = BrowserSession(browser_profile=profile) try: # Start browser start_event = session.event_bus.dispatch(BrowserStartEvent()) await start_event # Ensure any exceptions from the event handler are propagated await start_event.event_result(raise_if_any=True, raise_if_none=False) # Wait for browser to be fully started await session.event_bus.expect(BrowserConnectedEvent, timeout=5.0) # Browser disconnection detection is now handled by the crash watchdog # No configuration needed # Mock browser disconnection by overriding is_connected # This simulates what would happen if the browser process crashed if session._cdp_client_root: # Simulate disconnection by setting _cdp_client_root to None original_cdp_client = session._cdp_client_root session._cdp_client_root = None try: # Wait for watchdog to detect disconnection disconnect_error: BrowserErrorEvent = cast( BrowserErrorEvent, await session.event_bus.expect( BrowserErrorEvent, predicate=lambda e: cast(BrowserErrorEvent, e).error_type == 'BrowserDisconnected', timeout=2.0, ), ) assert 'disconnected unexpectedly' in disconnect_error.message finally: # Restore original CDP client session._cdp_client_root = original_cdp_client finally: # Force stop even if browser is marked as disconnected try: session.event_bus.dispatch(BrowserStopEvent(force=True)) await asyncio.sleep(0.5) # Give it time to stop except Exception: pass # Browser might already be stopped @pytest.mark.asyncio @pytest.mark.skip('CrashWatchdog not implemented in current CDP architecture') async def test_crash_watchdog_lifecycle(): """Test that CrashWatchdog starts and stops with browser session.""" profile = BrowserProfile(headless=True) session = BrowserSession(browser_profile=profile) # Start browser via event and wait for BrowserConnectedEvent start_event = session.event_bus.dispatch(BrowserStartEvent()) await start_event # Wait for the event and all handlers to complete started_event: BrowserConnectedEvent = cast( BrowserConnectedEvent, await session.event_bus.expect(BrowserConnectedEvent, timeout=5.0) ) assert started_event.cdp_url is not None # Verify crash watchdog is running assert hasattr(session, '_crash_watchdog'), 'CrashWatchdog should be created' assert session._crash_watchdog is not None, 'CrashWatchdog should not be None' # Check monitoring task is active assert session._crash_watchdog._monitoring_task is not None assert not session._crash_watchdog._monitoring_task.done() # Stop browser via event session.event_bus.dispatch(BrowserStopEvent()) # Wait for browser stopped event try: stopped_event: BrowserStoppedEvent = cast( BrowserStoppedEvent, await session.event_bus.expect(BrowserStoppedEvent, timeout=3.0) ) assert stopped_event.reason is not None except TimeoutError: # Browser stop can be flaky in test environment logger.warning('[TEST] BrowserStoppedEvent timeout - this is expected in some test environments') # Just verify the crash watchdog exists assert session._crash_watchdog is not None # Verify monitoring task was stopped await asyncio.sleep(0.1) # Give it a moment to clean up if session._crash_watchdog._monitoring_task: assert session._crash_watchdog._monitoring_task.done() @pytest.mark.asyncio @pytest.mark.skip(reason='Browser initialization timeout in test environment - timing issue') async def test_infinite_loop_page_blocking(): """Test that pages with infinite JavaScript loops are detected as unresponsive.""" from pytest_httpserver import HTTPServer # Create HTTP server with blocking page httpserver = HTTPServer() httpserver.start() # Add route that serves permanently blocking JavaScript httpserver.expect_request('/infinite-loop').respond_with_data( '

Loading...

', content_type='text/html' ) profile = BrowserProfile(headless=True) session = BrowserSession(browser_profile=profile) try: # Start browser session.event_bus.dispatch(BrowserStartEvent()) await session.event_bus.expect(BrowserConnectedEvent, timeout=5.0) # Navigate to blocking page blocking_url = httpserver.url_for('/infinite-loop') session.event_bus.dispatch(NavigateToUrlEvent(url=blocking_url)) # The navigation should timeout or trigger an error # We don't expect NavigationCompleteEvent since the page blocks await asyncio.sleep(2) # Give it time to detect the issue # Try to interact with the page via CDP - should still work at protocol level cdp_session = await session.get_or_create_cdp_session() # CDP commands should still work even if page is blocked version_result = await session.cdp_client.send.Browser.getVersion() assert version_result is not None # Close the blocking tab to recover await session.cdp_client.send.Target.closeTarget(params={'targetId': cdp_session.target_id}) finally: httpserver.stop() session.event_bus.dispatch(BrowserStopEvent()) await asyncio.sleep(0.5) # @pytest.mark.asyncio # async def test_transient_blocking_recovery(): # """Test recovery from temporarily blocking JavaScript.""" # from pytest_httpserver import HTTPServer # httpserver = HTTPServer() # httpserver.start() # # Page that blocks for 1 second then recovers # httpserver.expect_request('/transient-block').respond_with_data( # """ #

Blocking...

# # """, # content_type='text/html', # ) # profile = BrowserProfile(headless=True) # session = BrowserSession(browser_profile=profile) # try: # # Start browser # session.event_bus.dispatch(BrowserStartEvent()) # await session.event_bus.expect(BrowserConnectedEvent, timeout=5.0) # # Navigate to transiently blocking page # url = httpserver.url_for('/transient-block') # session.event_bus.dispatch(NavigateToUrlEvent(url=url)) # # Wait for the blocking to end # await asyncio.sleep(2) # # Verify page recovered and we can interact with it # cdp_session = await session.get_or_create_cdp_session() # result = await session.cdp_client.send.Runtime.evaluate( # params={'expression': 'document.getElementById("status").textContent', 'returnByValue': True}, # session_id=cdp_session.session_id, # ) # status_text = result.get('result', {}).get('value', '') # assert status_text == 'Recovered!', f"Expected 'Recovered!' but got '{status_text}'" # finally: # httpserver.stop() # session.event_bus.dispatch(BrowserStopEvent()) # await asyncio.sleep(0.5) @pytest.mark.asyncio @pytest.mark.skip(reason='Browser initialization timeout in test environment - timing issue') async def test_browser_process_kill_detection(): """Test that killing the browser process is detected.""" import os import signal profile = BrowserProfile(headless=True) session = BrowserSession(browser_profile=profile) try: # Start browser session.event_bus.dispatch(BrowserStartEvent()) await session.event_bus.expect(BrowserConnectedEvent, timeout=5.0) # Get browser process PID browser_pid = None if session._local_browser_watchdog and session._local_browser_watchdog._subprocess: browser_pid = session._local_browser_watchdog._subprocess.pid if browser_pid: # Kill the browser process try: os.kill(browser_pid, signal.SIGKILL) except ProcessLookupError: pass # Process might already be gone # Wait for crash detection try: error_event = cast( BrowserErrorEvent, await session.event_bus.expect( BrowserErrorEvent, predicate=lambda e: 'disconnect' in cast(BrowserErrorEvent, e).message.lower(), timeout=5.0, ), ) assert error_event is not None except TimeoutError: # Crash detection might not trigger in all environments pass finally: # Force cleanup try: await session.kill() except Exception: pass