Files
wehub-resource-sync 4cd2d4af2b
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:32 +08:00

1298 lines
40 KiB
Python

"""Tests for AI summary generation during rerun"""
from unittest.mock import AsyncMock
from browser_use.agent.service import Agent
from browser_use.agent.views import ActionResult, AgentHistory, AgentHistoryList, RerunSummaryAction, StepMetadata
from browser_use.browser.views import BrowserStateHistory
from browser_use.dom.views import DOMRect, NodeType
from tests.ci.conftest import create_mock_llm
async def test_generate_rerun_summary_success():
"""Test that _generate_rerun_summary generates an AI summary for successful rerun"""
# Create mock LLM that returns RerunSummaryAction
summary_action = RerunSummaryAction(
summary='Form filled successfully',
success=True,
completion_status='complete',
)
async def custom_ainvoke(*args, **kwargs):
# Get output_format from second positional arg or kwargs
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
assert output_format is RerunSummaryAction
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
# Mock ChatOpenAI class
mock_openai = AsyncMock()
mock_openai.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
await agent.browser_session.start()
try:
# Create some successful results
results = [
ActionResult(long_term_memory='Step 1 completed'),
ActionResult(long_term_memory='Step 2 completed'),
]
# Pass the mock LLM directly as summary_llm
summary = await agent._generate_rerun_summary('Test task', results, summary_llm=mock_openai)
# Check that result is the AI summary
assert summary.is_done is True
assert summary.success is True
assert summary.extracted_content == 'Form filled successfully'
assert 'Rerun completed' in (summary.long_term_memory or '')
finally:
await agent.close()
async def test_generate_rerun_summary_with_errors():
"""Test that AI summary correctly reflects errors in execution"""
# Create mock LLM for summary
summary_action = RerunSummaryAction(
summary='Rerun had errors',
success=False,
completion_status='failed',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
assert output_format is RerunSummaryAction
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
mock_openai = AsyncMock()
mock_openai.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
await agent.browser_session.start()
try:
# Create results with errors
results_with_errors = [
ActionResult(error='Failed to find element'),
ActionResult(error='Timeout'),
]
# Pass the mock LLM directly as summary_llm
summary = await agent._generate_rerun_summary('Test task', results_with_errors, summary_llm=mock_openai)
# Verify summary reflects errors
assert summary.is_done is True
assert summary.success is False
assert summary.extracted_content == 'Rerun had errors'
finally:
await agent.close()
async def test_generate_rerun_summary_fallback_on_error():
"""Test that a fallback summary is generated if LLM fails"""
# Mock ChatOpenAI to throw an error
mock_openai = AsyncMock()
mock_openai.ainvoke.side_effect = Exception('LLM service unavailable')
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
await agent.browser_session.start()
try:
# Create some results
results = [
ActionResult(long_term_memory='Step 1 completed'),
ActionResult(long_term_memory='Step 2 completed'),
]
# Pass the mock LLM directly as summary_llm
summary = await agent._generate_rerun_summary('Test task', results, summary_llm=mock_openai)
# Verify fallback summary
assert summary.is_done is True
assert summary.success is True # No errors, so success=True
assert 'Rerun completed' in (summary.extracted_content or '')
assert '2/2' in (summary.extracted_content or '') # Should show stats
finally:
await agent.close()
async def test_generate_rerun_summary_statistics():
"""Test that summary includes execution statistics in the prompt"""
# Create mock LLM
summary_action = RerunSummaryAction(
summary='3 of 5 steps succeeded',
success=False,
completion_status='partial',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
assert output_format is RerunSummaryAction
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
mock_openai = AsyncMock()
mock_openai.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
await agent.browser_session.start()
try:
# Create results with mix of success and errors
results = [
ActionResult(long_term_memory='Step 1 completed'),
ActionResult(error='Step 2 failed'),
ActionResult(long_term_memory='Step 3 completed'),
ActionResult(error='Step 4 failed'),
ActionResult(long_term_memory='Step 5 completed'),
]
# Pass the mock LLM directly as summary_llm
summary = await agent._generate_rerun_summary('Test task', results, summary_llm=mock_openai)
# Verify summary
assert summary.is_done is True
assert summary.success is False # partial completion
assert '3 of 5' in (summary.extracted_content or '')
finally:
await agent.close()
async def test_rerun_skips_steps_with_original_errors():
"""Test that rerun_history skips steps that had errors in the original run when skip_failures=True"""
# Create a mock LLM for summary
summary_action = RerunSummaryAction(
summary='Rerun completed with skipped steps',
success=True,
completion_status='complete',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
if output_format is RerunSummaryAction:
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
raise ValueError('Unexpected output_format')
mock_summary_llm = AsyncMock()
mock_summary_llm.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
# Create mock history with a step that has an error
mock_state = BrowserStateHistory(
url='https://example.com',
title='Test Page',
tabs=[],
interacted_element=[None],
)
# Get the dynamically created AgentOutput type from the agent
AgentOutput = agent.AgentOutput
# Create a step that originally had an error (using navigate action which doesn't require element matching)
failed_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Trying to navigate',
next_goal=None,
action=[{'navigate': {'url': 'https://example.com/page'}}], # type: ignore[arg-type]
),
result=[ActionResult(error='Navigation failed - network error')],
state=mock_state,
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=1,
step_interval=1.0,
),
)
# Create history with the failed step
history = AgentHistoryList(history=[failed_step])
try:
# Run rerun with skip_failures=True - should skip the step with original error
results = await agent.rerun_history(
history,
skip_failures=True,
summary_llm=mock_summary_llm,
)
# The step should have been skipped (not retried) because it originally had an error
# We should have 2 results: the skipped step result and the AI summary
assert len(results) == 2
# First result should indicate the step was skipped
skipped_result = results[0]
assert skipped_result.error is not None
assert 'Skipped - original step had error' in skipped_result.error
# Second result should be the AI summary
summary_result = results[1]
assert summary_result.is_done is True
finally:
await agent.close()
async def test_rerun_does_not_skip_originally_failed_when_skip_failures_false():
"""Test that rerun_history does NOT skip steps with original errors when skip_failures=False.
When skip_failures=False, the step should be attempted (and will succeed since navigate doesn't need element matching)."""
# Create a mock LLM for summary (will be reached after the step succeeds)
summary_action = RerunSummaryAction(
summary='Rerun completed',
success=True,
completion_status='complete',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
if output_format is RerunSummaryAction:
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
raise ValueError('Unexpected output_format')
mock_summary_llm = AsyncMock()
mock_summary_llm.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
# Create mock history with a step that has an error
mock_state = BrowserStateHistory(
url='https://example.com',
title='Test Page',
tabs=[],
interacted_element=[None],
)
# Get the dynamically created AgentOutput type from the agent
AgentOutput = agent.AgentOutput
# Create a step that originally had an error but uses navigate (which will work on rerun)
failed_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Trying to navigate',
next_goal=None,
action=[{'navigate': {'url': 'https://example.com/page'}}], # type: ignore[arg-type]
),
result=[ActionResult(error='Navigation failed - network error')],
state=mock_state,
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=1,
step_interval=1.0,
),
)
# Create history with the failed step
history = AgentHistoryList(history=[failed_step])
try:
# Run rerun with skip_failures=False - should attempt to replay (and succeed since navigate works)
results = await agent.rerun_history(
history,
skip_failures=False,
max_retries=1,
summary_llm=mock_summary_llm,
)
# With skip_failures=False, the step should NOT be skipped even if original had error
# The navigate action should succeed
assert len(results) == 2
# First result should be the successful navigation (not skipped)
nav_result = results[0]
# It should NOT contain "Skipped" since skip_failures=False
if nav_result.error:
assert 'Skipped' not in nav_result.error
finally:
await agent.close()
async def test_rerun_cleanup_on_failure(httpserver):
"""Test that rerun_history properly cleans up resources (closes browser/connections) even when it fails.
This test verifies the try/finally cleanup logic by creating a step that will fail
(element matching fails) and checking that the browser session is properly closed afterward.
"""
from browser_use.dom.views import DOMInteractedElement
# Set up a test page with a button that has DIFFERENT attributes than our historical element
test_html = """<!DOCTYPE html>
<html>
<body>
<button id="real-button" aria-label="real-button">Click me</button>
</body>
</html>"""
httpserver.expect_request('/test').respond_with_data(test_html, content_type='text/html')
test_url = httpserver.url_for('/test')
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
AgentOutput = agent.AgentOutput
# Step 1: Navigate to test page
navigate_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Navigate to test page',
next_goal=None,
action=[{'navigate': {'url': test_url}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Navigated')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=1,
step_interval=0.1,
),
)
# Step 2: Click on element that won't be found (different identifiers)
failing_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Trying to click non-existent button',
next_goal=None,
action=[{'click': {'index': 100}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked button')], # Original succeeded
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[
DOMInteractedElement(
node_id=1,
backend_node_id=9999,
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='BUTTON',
attributes={'aria-label': 'non-existent-button', 'id': 'fake-id'},
x_path='html/body/button[999]',
element_hash=123456789,
stable_hash=987654321,
bounds=DOMRect(x=0, y=0, width=100, height=50),
ax_name='non-existent',
)
],
),
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=2,
step_interval=0.1,
),
)
history = AgentHistoryList(history=[navigate_step, failing_step])
# Run rerun with skip_failures=False - should fail and raise RuntimeError
# but the try/finally should ensure cleanup happens
try:
await agent.rerun_history(
history,
skip_failures=False,
max_retries=1, # Fail quickly
)
assert False, 'Expected RuntimeError to be raised'
except RuntimeError as e:
# Expected - the step should fail on element matching
assert 'failed after 1 attempts' in str(e)
# If we get here without hanging, the cleanup worked
# The browser session should be closed by the finally block in rerun_history
# We can verify by checking that calling close again doesn't cause issues
# (close() is idempotent - calling it multiple times should be safe)
await agent.close() # Should not hang or error since already closed
async def test_rerun_records_errors_when_skip_failures_true(httpserver):
"""Test that rerun_history records errors in results even when skip_failures=True.
This ensures the AI summary correctly counts failures. Previously, when skip_failures=True
and a step failed after all retries, no error result was appended, causing the AI summary
to incorrectly report success=True even with multiple failures.
"""
from browser_use.dom.views import DOMInteractedElement
# Set up a test page with a button that has DIFFERENT attributes than our historical element
# This ensures element matching will fail (the historical element won't be found)
test_html = """<!DOCTYPE html>
<html>
<body>
<button id="real-button" aria-label="real-button">Click me</button>
</body>
</html>"""
httpserver.expect_request('/test').respond_with_data(test_html, content_type='text/html')
test_url = httpserver.url_for('/test')
# Create a mock LLM for summary that returns partial success
summary_action = RerunSummaryAction(
summary='Some steps failed',
success=False,
completion_status='partial',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
if output_format is RerunSummaryAction:
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
raise ValueError('Unexpected output_format')
mock_summary_llm = AsyncMock()
mock_summary_llm.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
# Create history with:
# 1. First step navigates to test page (will succeed)
# 2. Second step tries to click a non-existent element (will fail on element matching)
AgentOutput = agent.AgentOutput
# Step 1: Navigate to test page
navigate_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Navigate to test page',
next_goal=None,
action=[{'navigate': {'url': test_url}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Navigated')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=1,
step_interval=0.1,
),
)
# Step 2: Click on element that won't exist on current page (different hash/attributes)
failing_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Trying to click non-existent button',
next_goal=None,
action=[{'click': {'index': 100}}], # type: ignore[arg-type] # Original index doesn't matter, matching will fail
),
result=[ActionResult(long_term_memory='Clicked button')], # Original succeeded
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[
DOMInteractedElement(
node_id=1,
backend_node_id=9999,
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='BUTTON',
# This element has completely different identifiers than the real button
attributes={'aria-label': 'non-existent-button', 'id': 'fake-id'},
x_path='html/body/button[999]', # XPath that doesn't exist
element_hash=123456789, # Hash that won't match
stable_hash=987654321, # Stable hash that won't match
bounds=DOMRect(x=0, y=0, width=100, height=50),
ax_name='non-existent',
)
],
),
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=2,
step_interval=0.1,
),
)
history = AgentHistoryList(history=[navigate_step, failing_step])
try:
# Run rerun with skip_failures=True - should NOT raise but should record the error
results = await agent.rerun_history(
history,
skip_failures=True,
max_retries=1, # Fail quickly
summary_llm=mock_summary_llm,
)
# Should have 3 results: navigation success + error from failed step + AI summary
assert len(results) == 3
# First result should be successful navigation
nav_result = results[0]
assert nav_result.error is None
# Second result should be the error (element matching failed)
error_result = results[1]
assert error_result.error is not None
assert 'failed after 1 attempts' in error_result.error
# Third result should be the AI summary
summary_result = results[2]
assert summary_result.is_done is True
finally:
await agent.close()
async def test_rerun_skips_redundant_retry_steps(httpserver):
"""Test that rerun_history skips redundant retry steps.
This handles cases where the original run needed to click the same element multiple
times due to slow page response, but during replay the first click already succeeded.
When consecutive steps target the same element with the same action, the second step
should be skipped as a redundant retry.
"""
from browser_use.dom.views import DOMInteractedElement
# Set up a test page with a button
test_html = """<!DOCTYPE html>
<html>
<body>
<button id="login-btn" aria-label="Log In">Log In</button>
</body>
</html>"""
httpserver.expect_request('/test').respond_with_data(test_html, content_type='text/html')
test_url = httpserver.url_for('/test')
# Create a mock LLM for summary
summary_action = RerunSummaryAction(
summary='Rerun completed with skipped redundant step',
success=True,
completion_status='complete',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
if output_format is RerunSummaryAction:
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
raise ValueError('Unexpected output_format')
mock_summary_llm = AsyncMock()
mock_summary_llm.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
AgentOutput = agent.AgentOutput
# Create an interacted element that matches the button on the page
login_button_element = DOMInteractedElement(
node_id=1,
backend_node_id=1,
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='BUTTON',
attributes={'aria-label': 'Log In', 'id': 'login-btn'},
x_path='html/body/button',
element_hash=12345, # Same hash for both steps (same element)
stable_hash=12345,
bounds=DOMRect(x=0, y=0, width=100, height=50),
)
# Step 1: Navigate to test page
navigate_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Navigate to test page',
next_goal=None,
action=[{'navigate': {'url': test_url}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Navigated')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=1,
step_interval=0.1,
),
)
# Step 2: Click login button (first click)
click_step_1 = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Click login button',
next_goal=None,
action=[{'click': {'index': 1}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked login button')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[login_button_element],
),
metadata=StepMetadata(
step_start_time=1,
step_end_time=2,
step_number=2,
step_interval=0.1,
),
)
# Step 3: Click login button AGAIN (redundant retry - same element, same action)
click_step_2 = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Page did not change, clicking login button again',
next_goal=None,
action=[{'click': {'index': 1}}], # type: ignore[arg-type] # Same action type
),
result=[ActionResult(long_term_memory='Clicked login button')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[login_button_element], # Same element!
),
metadata=StepMetadata(
step_start_time=2,
step_end_time=3,
step_number=3,
step_interval=0.1,
),
)
history = AgentHistoryList(history=[navigate_step, click_step_1, click_step_2])
try:
results = await agent.rerun_history(
history,
skip_failures=True,
summary_llm=mock_summary_llm,
)
# Should have 4 results: navigate + click + skipped redundant + AI summary
assert len(results) == 4
# First result: navigation succeeded
nav_result = results[0]
assert nav_result.error is None
# Second result: first click succeeded
click_result = results[1]
assert click_result.error is None
# Third result: redundant retry was SKIPPED (not an error)
skipped_result = results[2]
assert skipped_result.error is None # Not an error - intentionally skipped
assert 'Skipped - redundant retry' in (skipped_result.extracted_content or '')
# Fourth result: AI summary
summary_result = results[3]
assert summary_result.is_done is True
finally:
await agent.close()
async def test_is_redundant_retry_step_detection():
"""Test the _is_redundant_retry_step method directly."""
from browser_use.dom.views import DOMInteractedElement
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
AgentOutput = agent.AgentOutput
# Create an interacted element
button_element = DOMInteractedElement(
node_id=1,
backend_node_id=1,
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='BUTTON',
attributes={'aria-label': 'Submit'},
x_path='html/body/button',
element_hash=12345,
stable_hash=12345,
bounds=DOMRect(x=0, y=0, width=100, height=50),
)
different_element = DOMInteractedElement(
node_id=2,
backend_node_id=2,
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='INPUT',
attributes={'name': 'email'},
x_path='html/body/input',
element_hash=99999, # Different hash
stable_hash=99999,
bounds=DOMRect(x=0, y=0, width=200, height=30),
)
# Step with click on button
click_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Click button',
next_goal=None,
action=[{'click': {'index': 1}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[button_element],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=1, step_interval=0.1),
)
# Same click on same button (redundant retry)
retry_click_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Click button again',
next_goal=None,
action=[{'click': {'index': 1}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[button_element], # Same element
),
metadata=StepMetadata(step_start_time=1, step_end_time=2, step_number=2, step_interval=0.1),
)
# Different action type on same element (not redundant)
input_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Type in button (weird but valid)',
next_goal=None,
action=[{'input': {'index': 1, 'text': 'hello'}}], # type: ignore[arg-type] # Different action type
),
result=[ActionResult(long_term_memory='Typed')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[button_element],
),
metadata=StepMetadata(step_start_time=2, step_end_time=3, step_number=3, step_interval=0.1),
)
# Same action type but different element (not redundant)
different_element_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Click different element',
next_goal=None,
action=[{'click': {'index': 2}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[different_element], # Different element
),
metadata=StepMetadata(step_start_time=3, step_end_time=4, step_number=4, step_interval=0.1),
)
try:
# Test 1: Same element, same action, previous succeeded -> redundant
assert agent._is_redundant_retry_step(retry_click_step, click_step, True) is True
# Test 2: Same element, same action, previous FAILED -> NOT redundant
assert agent._is_redundant_retry_step(retry_click_step, click_step, False) is False
# Test 3: Same element, different action type -> NOT redundant
assert agent._is_redundant_retry_step(input_step, click_step, True) is False
# Test 4: Different element, same action type -> NOT redundant
assert agent._is_redundant_retry_step(different_element_step, click_step, True) is False
# Test 5: No previous step -> NOT redundant
assert agent._is_redundant_retry_step(click_step, None, True) is False
finally:
await agent.close()
async def test_count_expected_elements_from_history():
"""Test that _count_expected_elements_from_history correctly estimates element count based on action indices."""
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
AgentOutput = agent.AgentOutput
# Test 1: Action with low index (5) -> needs at least 6 elements (index + 1)
step_low_index = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Test',
next_goal=None,
action=[{'input': {'index': 5, 'text': 'test'}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Done')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=1, step_interval=0.1),
)
# Test 2: Action with higher index (25) -> needs at least 26 elements
step_high_index = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Test',
next_goal=None,
action=[{'click': {'index': 25}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Done')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=2, step_interval=0.1),
)
# Test 3: Action with very high index (100) -> capped at 50
step_very_high_index = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Test',
next_goal=None,
action=[{'click': {'index': 100}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Done')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=3, step_interval=0.1),
)
# Test 4: Navigate action (no index) -> returns 0
step_no_index = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Test',
next_goal=None,
action=[{'navigate': {'url': 'http://test.com'}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Done')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=4, step_interval=0.1),
)
# Test 5: Multiple actions - uses max index
step_multiple_actions = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Test',
next_goal=None,
action=[
{'click': {'index': 3}}, # type: ignore[arg-type]
{'input': {'index': 10, 'text': 'test'}}, # type: ignore[arg-type]
],
),
result=[ActionResult(long_term_memory='Done'), ActionResult(long_term_memory='Done')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[None, None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=5, step_interval=0.1),
)
# Test 6: Action with index 0 (edge case) -> needs at least 1 element
# Using input action because it allows index 0 (click requires ge=1)
step_index_zero = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Test',
next_goal=None,
action=[{'input': {'index': 0, 'text': 'test'}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Done')],
state=BrowserStateHistory(
url='http://test.com',
title='Test',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=6, step_interval=0.1),
)
try:
# Test 1: Action index 5 -> needs 6 elements (index + 1)
assert agent._count_expected_elements_from_history(step_low_index) == 6
# Test 2: Action index 25 -> needs 26 elements
assert agent._count_expected_elements_from_history(step_high_index) == 26
# Test 3: Action index 100 -> capped at 50
assert agent._count_expected_elements_from_history(step_very_high_index) == 50
# Test 4: Navigate has no index -> returns 0
assert agent._count_expected_elements_from_history(step_no_index) == 0
# Test 5: Multiple actions -> uses max index (10) + 1 = 11
assert agent._count_expected_elements_from_history(step_multiple_actions) == 11
# Test 6: Action index 0 (edge case) -> needs 1 element (0 + 1)
assert agent._count_expected_elements_from_history(step_index_zero) == 1
finally:
await agent.close()
async def test_wait_for_minimum_elements(httpserver):
"""Test that _wait_for_minimum_elements waits for elements to appear."""
# Set up a simple test page with a button
test_html = """<!DOCTYPE html>
<html>
<body>
<button id="btn1">Button 1</button>
<button id="btn2">Button 2</button>
<input type="text" id="input1" />
</body>
</html>"""
httpserver.expect_request('/test').respond_with_data(test_html, content_type='text/html')
test_url = httpserver.url_for('/test')
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
try:
await agent.browser_session.start()
# Navigate to the test page first
from browser_use.browser.events import NavigateToUrlEvent
await agent.browser_session.event_bus.dispatch(NavigateToUrlEvent(url=test_url, new_tab=False))
# Wait a bit for navigation
import asyncio
await asyncio.sleep(1.0)
# Test 1: Wait for 1 element (should succeed quickly)
state = await agent._wait_for_minimum_elements(min_elements=1, timeout=5.0, poll_interval=0.5)
assert state is not None
assert state.dom_state.selector_map is not None
assert len(state.dom_state.selector_map) >= 1
# Test 2: Wait for reasonable number of elements (should succeed)
state = await agent._wait_for_minimum_elements(min_elements=2, timeout=5.0, poll_interval=0.5)
assert state is not None
assert len(state.dom_state.selector_map) >= 2
# Test 3: Wait for too many elements (should timeout but still return state)
state = await agent._wait_for_minimum_elements(min_elements=100, timeout=2.0, poll_interval=0.5)
assert state is not None # Should still return a state even on timeout
finally:
await agent.close()
async def test_rerun_waits_for_elements_before_matching(httpserver):
"""Test that rerun_history waits for elements before attempting element matching.
This test verifies that for actions needing element matching (like click),
the rerun logic waits for the page to have enough elements before proceeding.
"""
from browser_use.dom.views import DOMInteractedElement
# Set up a test page with elements
test_html = """<!DOCTYPE html>
<html>
<body>
<button id="test-btn" aria-label="Test Button">Click me</button>
</body>
</html>"""
httpserver.expect_request('/test').respond_with_data(test_html, content_type='text/html')
test_url = httpserver.url_for('/test')
# Create a mock LLM for summary
summary_action = RerunSummaryAction(
summary='Rerun completed',
success=True,
completion_status='complete',
)
async def custom_ainvoke(*args, **kwargs):
output_format = args[1] if len(args) > 1 else kwargs.get('output_format')
if output_format is RerunSummaryAction:
from browser_use.llm.views import ChatInvokeCompletion
return ChatInvokeCompletion(completion=summary_action, usage=None)
raise ValueError('Unexpected output_format')
mock_summary_llm = AsyncMock()
mock_summary_llm.ainvoke.side_effect = custom_ainvoke
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
AgentOutput = agent.AgentOutput
# Create an element that matches the page
button_element = DOMInteractedElement(
node_id=1,
backend_node_id=5, # This will trigger waiting for at least 5 elements
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='BUTTON',
attributes={'aria-label': 'Test Button', 'id': 'test-btn'},
x_path='html/body/button',
element_hash=12345,
stable_hash=12345,
bounds=DOMRect(x=0, y=0, width=100, height=50),
)
# Step 1: Navigate to test page
navigate_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Navigate to test page',
next_goal=None,
action=[{'navigate': {'url': test_url}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Navigated')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(
step_start_time=0,
step_end_time=1,
step_number=1,
step_interval=0.1,
),
)
# Step 2: Click button (needs element matching, should wait for elements)
click_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Click button',
next_goal=None,
action=[{'click': {'index': 5}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked')],
state=BrowserStateHistory(
url=test_url,
title='Test Page',
tabs=[],
interacted_element=[button_element],
),
metadata=StepMetadata(
step_start_time=1,
step_end_time=2,
step_number=2,
step_interval=0.1,
),
)
history = AgentHistoryList(history=[navigate_step, click_step])
try:
# Run rerun with wait_for_elements=True - should wait for elements before trying to match
results = await agent.rerun_history(
history,
skip_failures=True,
max_retries=1,
summary_llm=mock_summary_llm,
wait_for_elements=True, # Enable element waiting
)
# Should have results: navigate + click (or error if element not found) + summary
assert len(results) >= 2
# First result should be navigation success
nav_result = results[0]
assert nav_result.error is None
finally:
await agent.close()
async def test_rerun_uses_exponential_backoff_retry_delays(httpserver):
"""Test that rerun uses exponential backoff delays between retries (5s, 10s, 20s, capped at 30s)."""
import time
from browser_use.dom.views import DOMInteractedElement
# Set up a test page with a button that won't match
test_html = """<!DOCTYPE html>
<html>
<body>
<button id="real-btn">Real Button</button>
</body>
</html>"""
httpserver.expect_request('/test').respond_with_data(test_html, content_type='text/html')
test_url = httpserver.url_for('/test')
llm = create_mock_llm(actions=None)
agent = Agent(task='Test task', llm=llm)
AgentOutput = agent.AgentOutput
# Create an element that WON'T match (different identifiers)
non_matching_element = DOMInteractedElement(
node_id=1,
backend_node_id=1, # Low to avoid long element waiting
frame_id=None,
node_type=NodeType.ELEMENT_NODE,
node_value='',
node_name='BUTTON',
attributes={'aria-label': 'Non-existent', 'id': 'fake-id'},
x_path='html/body/button[999]',
element_hash=99999,
stable_hash=99999,
bounds=DOMRect(x=0, y=0, width=100, height=50),
)
# Step 1: Navigate
navigate_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Navigate',
next_goal=None,
action=[{'navigate': {'url': test_url}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Navigated')],
state=BrowserStateHistory(
url=test_url,
title='Test',
tabs=[],
interacted_element=[None],
),
metadata=StepMetadata(step_start_time=0, step_end_time=1, step_number=1, step_interval=0.1),
)
# Step 2: Click non-matching element (will fail and retry)
failing_step = AgentHistory(
model_output=AgentOutput(
evaluation_previous_goal=None,
memory='Click',
next_goal=None,
action=[{'click': {'index': 1}}], # type: ignore[arg-type]
),
result=[ActionResult(long_term_memory='Clicked')],
state=BrowserStateHistory(
url=test_url,
title='Test',
tabs=[],
interacted_element=[non_matching_element],
),
metadata=StepMetadata(step_start_time=1, step_end_time=2, step_number=2, step_interval=0.1),
)
history = AgentHistoryList(history=[navigate_step, failing_step])
try:
start_time = time.time()
# Run rerun with 2 retries - should use exponential backoff (5s for first retry)
# Attempt 1 fails -> wait 5s -> Attempt 2 fails -> done
try:
await agent.rerun_history(
history,
skip_failures=False,
max_retries=2, # Will fail twice with 5s delay between (exponential: 5s * 2^0 = 5s)
)
except RuntimeError:
pass # Expected to fail
elapsed = time.time() - start_time
# Should have taken at least 5 seconds (the first retry delay with exponential backoff)
# Exponential backoff formula: base_delay * 2^(retry_count-1) = 5 * 2^0 = 5s
assert elapsed >= 4.5, f'Expected at least 4.5s elapsed (5s exponential backoff), got {elapsed:.1f}s'
finally:
await agent.close()
async def test_exponential_backoff_calculation():
"""Test that exponential backoff correctly calculates delays: 5s, 10s, 20s, capped at 30s."""
# Verify the exponential backoff formula: min(5 * 2^(retry-1), 30)
base_delay = 5.0
max_delay = 30.0
# Retry 1: 5 * 2^0 = 5s
assert min(base_delay * (2**0), max_delay) == 5.0
# Retry 2: 5 * 2^1 = 10s
assert min(base_delay * (2**1), max_delay) == 10.0
# Retry 3: 5 * 2^2 = 20s
assert min(base_delay * (2**2), max_delay) == 20.0
# Retry 4: 5 * 2^3 = 40s -> capped at 30s
assert min(base_delay * (2**3), max_delay) == 30.0
# Retry 5: 5 * 2^4 = 80s -> capped at 30s
assert min(base_delay * (2**4), max_delay) == 30.0