ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
"""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(
|
||||
'<html><body><h1>Loading...</h1><script>while(true){}</script></body></html>', 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(
|
||||
# """<html><body>
|
||||
# <h1 id="status">Blocking...</h1>
|
||||
# <script>
|
||||
# const start = Date.now();
|
||||
# while (Date.now() - start < 1000) {} // Block for 1 second
|
||||
# document.getElementById('status').textContent = 'Recovered!';
|
||||
# </script>
|
||||
# </body></html>""",
|
||||
# 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
|
||||
Reference in New Issue
Block a user