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,434 @@
|
||||
"""
|
||||
Test script for BrowserSession.start() method to ensure proper initialization,
|
||||
concurrency handling, and error handling.
|
||||
|
||||
Tests cover:
|
||||
- Calling .start() on a session that's already started
|
||||
- Simultaneously calling .start() from two parallel coroutines
|
||||
- Calling .start() on a session that's started but has a closed browser connection
|
||||
- Calling .close() on a session that hasn't been started yet
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_use.browser.profile import (
|
||||
BROWSERUSE_DEFAULT_CHANNEL,
|
||||
BrowserChannel,
|
||||
BrowserProfile,
|
||||
)
|
||||
from browser_use.browser.session import BrowserSession
|
||||
from browser_use.config import CONFIG
|
||||
|
||||
# Set up test logging
|
||||
logger = logging.getLogger('browser_session_start_tests')
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# run with pytest -k test_user_data_dir_not_allowed_to_corrupt_default_profile
|
||||
|
||||
|
||||
class TestBrowserSessionStart:
|
||||
"""Tests for BrowserSession.start() method initialization and concurrency."""
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
async def browser_profile(self):
|
||||
"""Create and provide a BrowserProfile with headless mode."""
|
||||
profile = BrowserProfile(headless=True, user_data_dir=None, keep_alive=False)
|
||||
yield profile
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
async def browser_session(self, browser_profile):
|
||||
"""Create a BrowserSession instance without starting it."""
|
||||
session = BrowserSession(browser_profile=browser_profile)
|
||||
yield session
|
||||
await session.kill()
|
||||
|
||||
async def test_start_already_started_session(self, browser_session):
|
||||
"""Test calling .start() on a session that's already started."""
|
||||
# logger.info('Testing start on already started session')
|
||||
|
||||
# Start the session for the first time
|
||||
await browser_session.start()
|
||||
assert browser_session._cdp_client_root is not None
|
||||
|
||||
# Start the session again - should return immediately without re-initialization
|
||||
await browser_session.start()
|
||||
assert browser_session._cdp_client_root is not None
|
||||
|
||||
# @pytest.mark.skip(reason="Race condition - DOMWatchdog tries to inject scripts into tab that's being closed")
|
||||
# async def test_page_lifecycle_management(self, browser_session: BrowserSession):
|
||||
# """Test session handles page lifecycle correctly."""
|
||||
# # logger.info('Testing page lifecycle management')
|
||||
|
||||
# # Start the session and get initial state
|
||||
# await browser_session.start()
|
||||
# initial_tabs = await browser_session.get_tabs()
|
||||
# initial_count = len(initial_tabs)
|
||||
|
||||
# # Get current tab info
|
||||
# current_url = await browser_session.get_current_page_url()
|
||||
# assert current_url is not None
|
||||
|
||||
# # Get current tab ID
|
||||
# current_tab_id = browser_session.agent_focus.target_id if browser_session.agent_focus else None
|
||||
# assert current_tab_id is not None
|
||||
|
||||
# # Close the current tab using the event system
|
||||
# from browser_use.browser.events import CloseTabEvent
|
||||
|
||||
# close_event = browser_session.event_bus.dispatch(CloseTabEvent(target_id=current_tab_id))
|
||||
# await close_event
|
||||
|
||||
# # Operations should still work - may create new page or use existing
|
||||
# tabs_after_close = await browser_session.get_tabs()
|
||||
# assert isinstance(tabs_after_close, list)
|
||||
|
||||
# # Create a new tab explicitly
|
||||
# event = browser_session.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
|
||||
# await event
|
||||
# await event.event_result(raise_if_any=True, raise_if_none=False)
|
||||
|
||||
# # Should have at least one tab now
|
||||
# final_tabs = await browser_session.get_tabs()
|
||||
# assert len(final_tabs) >= 1
|
||||
|
||||
async def test_user_data_dir_not_allowed_to_corrupt_default_profile(self):
|
||||
"""Test user_data_dir handling for different browser channels and version mismatches."""
|
||||
# Test 1: Chromium with default user_data_dir and default channel should work fine
|
||||
session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
headless=True,
|
||||
user_data_dir=CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR,
|
||||
channel=BROWSERUSE_DEFAULT_CHANNEL, # chromium
|
||||
keep_alive=False,
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await session.start()
|
||||
assert session._cdp_client_root is not None
|
||||
# Verify the user_data_dir wasn't changed
|
||||
assert session.browser_profile.user_data_dir == CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR
|
||||
finally:
|
||||
await session.kill()
|
||||
|
||||
# Test 2: Chrome with default user_data_dir should automatically change dir
|
||||
profile2 = BrowserProfile(
|
||||
headless=True,
|
||||
user_data_dir=CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR,
|
||||
channel=BrowserChannel.CHROME,
|
||||
keep_alive=False,
|
||||
)
|
||||
|
||||
# The validator should have changed the user_data_dir to avoid corruption
|
||||
assert profile2.user_data_dir != CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR
|
||||
assert profile2.user_data_dir == CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR.parent / 'default-chrome'
|
||||
|
||||
# Test 3: Edge with default user_data_dir should also change
|
||||
profile3 = BrowserProfile(
|
||||
headless=True,
|
||||
user_data_dir=CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR,
|
||||
channel=BrowserChannel.MSEDGE,
|
||||
keep_alive=False,
|
||||
)
|
||||
|
||||
assert profile3.user_data_dir != CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR
|
||||
assert profile3.user_data_dir == CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR.parent / 'default-msedge'
|
||||
|
||||
|
||||
class TestBrowserSessionReusePatterns:
|
||||
"""Tests for all browser re-use patterns documented in docs/customize/real-browser.mdx"""
|
||||
|
||||
async def test_sequential_agents_same_profile_different_browser(self, mock_llm):
|
||||
"""Test Sequential Agents, Same Profile, Different Browser pattern"""
|
||||
from browser_use import Agent
|
||||
from browser_use.browser.profile import BrowserProfile
|
||||
|
||||
# Create a reusable profile
|
||||
reused_profile = BrowserProfile(
|
||||
user_data_dir=None, # Use temp dir for testing
|
||||
headless=True,
|
||||
)
|
||||
|
||||
# First agent
|
||||
agent1 = Agent(
|
||||
task='The first task...',
|
||||
llm=mock_llm,
|
||||
browser_profile=reused_profile,
|
||||
)
|
||||
await agent1.run()
|
||||
|
||||
# Verify first agent's session is closed
|
||||
assert agent1.browser_session is not None
|
||||
assert not agent1.browser_session._cdp_client_root is not None
|
||||
|
||||
# Second agent with same profile
|
||||
agent2 = Agent(
|
||||
task='The second task...',
|
||||
llm=mock_llm,
|
||||
browser_profile=reused_profile,
|
||||
# Disable memory for tests
|
||||
)
|
||||
await agent2.run()
|
||||
|
||||
# Verify second agent created a new session
|
||||
assert agent2.browser_session is not None
|
||||
assert agent1.browser_session is not agent2.browser_session
|
||||
assert not agent2.browser_session._cdp_client_root is not None
|
||||
|
||||
async def test_sequential_agents_same_profile_same_browser(self, mock_llm):
|
||||
"""Test Sequential Agents, Same Profile, Same Browser pattern"""
|
||||
from browser_use import Agent, BrowserSession
|
||||
|
||||
# Create a reusable session with keep_alive
|
||||
reused_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
user_data_dir=None, # Use temp dir for testing
|
||||
headless=True,
|
||||
keep_alive=True, # Don't close browser after agent.run()
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
# Start the session manually (agents will reuse this initialized session)
|
||||
await reused_session.start()
|
||||
|
||||
# First agent
|
||||
agent1 = Agent(
|
||||
task='The first task...',
|
||||
llm=mock_llm,
|
||||
browser_session=reused_session,
|
||||
# Disable memory for tests
|
||||
)
|
||||
await agent1.run()
|
||||
|
||||
# Verify session is still alive
|
||||
assert reused_session._cdp_client_root is not None
|
||||
|
||||
# Second agent reusing the same session
|
||||
agent2 = Agent(
|
||||
task='The second task...',
|
||||
llm=mock_llm,
|
||||
browser_session=reused_session,
|
||||
# Disable memory for tests
|
||||
)
|
||||
await agent2.run()
|
||||
|
||||
# Verify same browser was used (using __eq__ to check browser_pid, cdp_url)
|
||||
assert agent1.browser_session == agent2.browser_session
|
||||
assert agent1.browser_session == reused_session
|
||||
assert reused_session._cdp_client_root is not None
|
||||
|
||||
finally:
|
||||
await reused_session.kill()
|
||||
|
||||
|
||||
class TestBrowserSessionEventSystem:
|
||||
"""Tests for the new event system integration in BrowserSession."""
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
async def browser_session(self):
|
||||
"""Create a BrowserSession instance for event system testing."""
|
||||
profile = BrowserProfile(headless=True, user_data_dir=None, keep_alive=False)
|
||||
session = BrowserSession(browser_profile=profile)
|
||||
yield session
|
||||
await session.kill()
|
||||
|
||||
async def test_event_bus_initialization(self, browser_session):
|
||||
"""Test that event bus is properly initialized with unique name."""
|
||||
# Event bus should be created during __init__
|
||||
assert browser_session.event_bus is not None
|
||||
assert browser_session.event_bus.name.startswith('EventBus_')
|
||||
# Event bus name format may vary, just check it exists
|
||||
|
||||
async def test_event_handlers_registration(self, browser_session: BrowserSession):
|
||||
"""Test that event handlers are properly registered."""
|
||||
# Attach all watchdogs to register their handlers
|
||||
await browser_session.attach_all_watchdogs()
|
||||
|
||||
# Check that handlers are registered in the event bus
|
||||
from browser_use.browser.events import (
|
||||
BrowserStartEvent,
|
||||
BrowserStateRequestEvent,
|
||||
BrowserStopEvent,
|
||||
ClickElementEvent,
|
||||
CloseTabEvent,
|
||||
ScreenshotEvent,
|
||||
ScrollEvent,
|
||||
TypeTextEvent,
|
||||
)
|
||||
|
||||
# These event types should have handlers registered
|
||||
event_types_with_handlers = [
|
||||
BrowserStartEvent,
|
||||
BrowserStopEvent,
|
||||
ClickElementEvent,
|
||||
TypeTextEvent,
|
||||
ScrollEvent,
|
||||
CloseTabEvent,
|
||||
BrowserStateRequestEvent,
|
||||
ScreenshotEvent,
|
||||
]
|
||||
|
||||
for event_type in event_types_with_handlers:
|
||||
handlers = browser_session.event_bus.handlers.get(event_type.__name__, [])
|
||||
assert len(handlers) > 0, f'No handlers registered for {event_type.__name__}'
|
||||
|
||||
async def test_direct_event_dispatching(self, browser_session):
|
||||
"""Test direct event dispatching without using the public API."""
|
||||
from browser_use.browser.events import BrowserConnectedEvent, BrowserStartEvent
|
||||
|
||||
# Dispatch BrowserStartEvent directly
|
||||
start_event = browser_session.event_bus.dispatch(BrowserStartEvent())
|
||||
|
||||
# Wait for event to complete
|
||||
await start_event
|
||||
|
||||
# Check if BrowserConnectedEvent was dispatched
|
||||
assert browser_session._cdp_client_root is not None
|
||||
|
||||
# Check event history
|
||||
event_history = list(browser_session.event_bus.event_history.values())
|
||||
assert len(event_history) >= 2 # BrowserStartEvent + BrowserConnectedEvent + others
|
||||
|
||||
# Find the BrowserConnectedEvent in history
|
||||
started_events = [e for e in event_history if isinstance(e, BrowserConnectedEvent)]
|
||||
assert len(started_events) >= 1
|
||||
assert started_events[0].cdp_url is not None
|
||||
|
||||
async def test_event_system_error_handling(self, browser_session):
|
||||
"""Test error handling in event system."""
|
||||
from browser_use.browser.events import BrowserStartEvent
|
||||
|
||||
# Create session with invalid CDP URL to trigger error
|
||||
error_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(headless=True),
|
||||
cdp_url='http://localhost:99999', # Invalid port
|
||||
)
|
||||
|
||||
try:
|
||||
# Dispatch start event directly - should trigger error handling
|
||||
start_event = error_session.event_bus.dispatch(BrowserStartEvent())
|
||||
|
||||
# The event bus catches and logs the error, but the event awaits successfully
|
||||
await start_event
|
||||
|
||||
# The session should not be initialized due to the error
|
||||
assert error_session._cdp_client_root is None, 'Session should not be initialized after connection error'
|
||||
|
||||
# Verify the error was logged in the event history (good enough for error handling test)
|
||||
assert len(error_session.event_bus.event_history) > 0, 'Event should be tracked even with errors'
|
||||
|
||||
finally:
|
||||
await error_session.kill()
|
||||
|
||||
async def test_concurrent_event_dispatching(self, browser_session: BrowserSession):
|
||||
"""Test that concurrent events are handled properly."""
|
||||
from browser_use.browser.events import ScreenshotEvent
|
||||
|
||||
# Start browser first
|
||||
await browser_session.start()
|
||||
|
||||
# Dispatch multiple events concurrently
|
||||
screenshot_event1 = browser_session.event_bus.dispatch(ScreenshotEvent())
|
||||
screenshot_event2 = browser_session.event_bus.dispatch(ScreenshotEvent())
|
||||
|
||||
# Both should complete successfully
|
||||
results = await asyncio.gather(screenshot_event1, screenshot_event2, return_exceptions=True)
|
||||
|
||||
# Check that no exceptions were raised
|
||||
for result in results:
|
||||
assert not isinstance(result, Exception), f'Event failed with: {result}'
|
||||
|
||||
# async def test_many_parallel_browser_sessions(self):
|
||||
# """Test spawning 12 parallel browser_sessions with different settings and ensure they all work"""
|
||||
# from browser_use import BrowserSession
|
||||
|
||||
# browser_sessions = []
|
||||
|
||||
# for i in range(3):
|
||||
# browser_sessions.append(
|
||||
# BrowserSession(
|
||||
# browser_profile=BrowserProfile(
|
||||
# user_data_dir=None,
|
||||
# headless=True,
|
||||
# keep_alive=True,
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
# for i in range(3):
|
||||
# browser_sessions.append(
|
||||
# BrowserSession(
|
||||
# browser_profile=BrowserProfile(
|
||||
# user_data_dir=Path(tempfile.mkdtemp(prefix=f'browseruse-tmp-{i}')),
|
||||
# headless=True,
|
||||
# keep_alive=True,
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
# for i in range(3):
|
||||
# browser_sessions.append(
|
||||
# BrowserSession(
|
||||
# browser_profile=BrowserProfile(
|
||||
# user_data_dir=None,
|
||||
# headless=True,
|
||||
# keep_alive=False,
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
# for i in range(3):
|
||||
# browser_sessions.append(
|
||||
# BrowserSession(
|
||||
# browser_profile=BrowserProfile(
|
||||
# user_data_dir=Path(tempfile.mkdtemp(prefix=f'browseruse-tmp-{i}')),
|
||||
# headless=True,
|
||||
# keep_alive=False,
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
|
||||
# print('Starting many parallel browser sessions...')
|
||||
# await asyncio.gather(*[browser_session.start() for browser_session in browser_sessions])
|
||||
|
||||
# print('Ensuring all parallel browser sessions are connected and usable...')
|
||||
# new_tab_tasks = []
|
||||
# for browser_session in browser_sessions:
|
||||
# assert browser_session._cdp_client_root is not None
|
||||
# assert browser_session._cdp_client_root is not None
|
||||
# new_tab_tasks.append(browser_session.create_new_tab('chrome://version'))
|
||||
# await asyncio.gather(*new_tab_tasks)
|
||||
|
||||
# print('killing every 3rd browser_session to test parallel shutdown')
|
||||
# kill_tasks = []
|
||||
# for i in range(0, len(browser_sessions), 3):
|
||||
# kill_tasks.append(browser_sessions[i].kill())
|
||||
# browser_sessions[i] = None
|
||||
# results = await asyncio.gather(*kill_tasks, return_exceptions=True)
|
||||
# # Check that no exceptions were raised during cleanup
|
||||
# for i, result in enumerate(results):
|
||||
# if isinstance(result, Exception):
|
||||
# print(f'Warning: Browser session kill raised exception: {type(result).__name__}: {result}')
|
||||
|
||||
# print('ensuring the remaining browser_sessions are still connected and usable')
|
||||
# new_tab_tasks = []
|
||||
# screenshot_tasks = []
|
||||
# for browser_session in filter(bool, browser_sessions):
|
||||
# assert browser_session._cdp_client_root is not None
|
||||
# assert browser_session._cdp_client_root is not None
|
||||
# new_tab_tasks.append(browser_session.create_new_tab('chrome://version'))
|
||||
# screenshot_tasks.append(browser_session.take_screenshot())
|
||||
# await asyncio.gather(*new_tab_tasks)
|
||||
# await asyncio.gather(*screenshot_tasks)
|
||||
|
||||
# kill_tasks = []
|
||||
# print('killing the remaining browser_sessions')
|
||||
# for browser_session in filter(bool, browser_sessions):
|
||||
# kill_tasks.append(browser_session.kill())
|
||||
# results = await asyncio.gather(*kill_tasks, return_exceptions=True)
|
||||
# # Check that no exceptions were raised during cleanup
|
||||
# for i, result in enumerate(results):
|
||||
# if isinstance(result, Exception):
|
||||
# print(f'Warning: Browser session kill raised exception: {type(result).__name__}: {result}')
|
||||
Reference in New Issue
Block a user